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.js
CHANGED
|
@@ -19,7 +19,7 @@ var VERSION;
|
|
|
19
19
|
var init_header = __esm({
|
|
20
20
|
"src/types/_header.ts"() {
|
|
21
21
|
"use strict";
|
|
22
|
-
VERSION = "0.
|
|
22
|
+
VERSION = "0.41.0";
|
|
23
23
|
}
|
|
24
24
|
});
|
|
25
25
|
|
|
@@ -45804,6 +45804,15 @@ function formatCompositeScore(report) {
|
|
|
45804
45804
|
)
|
|
45805
45805
|
);
|
|
45806
45806
|
}
|
|
45807
|
+
const composite = report.compositeScore;
|
|
45808
|
+
if (composite !== void 0) {
|
|
45809
|
+
lines.push("");
|
|
45810
|
+
lines.push(
|
|
45811
|
+
chalk.dim(
|
|
45812
|
+
`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.`
|
|
45813
|
+
)
|
|
45814
|
+
);
|
|
45815
|
+
}
|
|
45807
45816
|
return lines.join("\n");
|
|
45808
45817
|
}
|
|
45809
45818
|
function formatCoherenceScores(report) {
|
|
@@ -54226,6 +54235,21 @@ function safeRelative(cwd, filePath) {
|
|
|
54226
54235
|
return filePath;
|
|
54227
54236
|
}
|
|
54228
54237
|
}
|
|
54238
|
+
function buildInventorySummary(inventory) {
|
|
54239
|
+
const patternCounts = {};
|
|
54240
|
+
const patternNames = {};
|
|
54241
|
+
for (const [category, matches] of Object.entries(inventory.patterns)) {
|
|
54242
|
+
if (matches.length === 0) continue;
|
|
54243
|
+
const names = Array.from(new Set(matches.map((m) => m.name))).sort();
|
|
54244
|
+
patternCounts[category] = names.length;
|
|
54245
|
+
patternNames[category] = names.slice(0, TELEMETRY_INVENTORY_NAME_CAP);
|
|
54246
|
+
}
|
|
54247
|
+
return {
|
|
54248
|
+
scannedFiles: inventory.scannedFiles,
|
|
54249
|
+
patternCounts,
|
|
54250
|
+
patternNames
|
|
54251
|
+
};
|
|
54252
|
+
}
|
|
54229
54253
|
function aggregateViolations(report) {
|
|
54230
54254
|
const counts = /* @__PURE__ */ new Map();
|
|
54231
54255
|
for (const issue of report.issues) {
|
|
@@ -54305,7 +54329,7 @@ function readTelemetry(cwd) {
|
|
|
54305
54329
|
}
|
|
54306
54330
|
return payloads;
|
|
54307
54331
|
}
|
|
54308
|
-
function recordTelemetry(cwd, report, results, config) {
|
|
54332
|
+
function recordTelemetry(cwd, report, results, config, inventory) {
|
|
54309
54333
|
if (config.telemetry === false) {
|
|
54310
54334
|
return void 0;
|
|
54311
54335
|
}
|
|
@@ -54326,7 +54350,14 @@ function recordTelemetry(cwd, report, results, config) {
|
|
|
54326
54350
|
framework: config.framework
|
|
54327
54351
|
},
|
|
54328
54352
|
violations: aggregateViolations(report),
|
|
54329
|
-
files: buildFileRecords(cwd, report, results)
|
|
54353
|
+
files: buildFileRecords(cwd, report, results),
|
|
54354
|
+
// v0.41.0 (Sprint 2, task 2a.1): the inventory summary is
|
|
54355
|
+
// additive — omitting it (legacy callers) keeps the JSONL line
|
|
54356
|
+
// shape identical to v0.40.x payloads, so old readers stay
|
|
54357
|
+
// green. New readers (`slopbrick drift --since <date>` in
|
|
54358
|
+
// Sprint 2a.2) treat the field as optional and fall back to
|
|
54359
|
+
// a re-scan when it's missing.
|
|
54360
|
+
...inventory ? { inventory: buildInventorySummary(inventory) } : {}
|
|
54330
54361
|
};
|
|
54331
54362
|
const path = telemetryPath2(cwd);
|
|
54332
54363
|
const dir = dirname11(path);
|
|
@@ -54337,7 +54368,7 @@ function recordTelemetry(cwd, report, results, config) {
|
|
|
54337
54368
|
appendFileSync(path, JSON.stringify(payload) + "\n", "utf-8");
|
|
54338
54369
|
return payload;
|
|
54339
54370
|
}
|
|
54340
|
-
var TELEMETRY_DIR, TELEMETRY_FILE2, MAX_TELEMETRY_BYTES, MAX_ROTATED_FILES;
|
|
54371
|
+
var TELEMETRY_DIR, TELEMETRY_FILE2, MAX_TELEMETRY_BYTES, MAX_ROTATED_FILES, TELEMETRY_INVENTORY_NAME_CAP;
|
|
54341
54372
|
var init_telemetry = __esm({
|
|
54342
54373
|
"src/engine/telemetry.ts"() {
|
|
54343
54374
|
"use strict";
|
|
@@ -54345,6 +54376,7 @@ var init_telemetry = __esm({
|
|
|
54345
54376
|
TELEMETRY_FILE2 = "scans.jsonl";
|
|
54346
54377
|
MAX_TELEMETRY_BYTES = 10 * 1024 * 1024;
|
|
54347
54378
|
MAX_ROTATED_FILES = 5;
|
|
54379
|
+
TELEMETRY_INVENTORY_NAME_CAP = 50;
|
|
54348
54380
|
}
|
|
54349
54381
|
});
|
|
54350
54382
|
|
|
@@ -54660,11 +54692,11 @@ async function persistRun(input) {
|
|
|
54660
54692
|
}
|
|
54661
54693
|
report.issues.push(...flywheelOutput.hotspotIssues);
|
|
54662
54694
|
}
|
|
54663
|
-
|
|
54695
|
+
let patternInventory;
|
|
54664
54696
|
if (config.projectMemory !== false) {
|
|
54665
54697
|
try {
|
|
54666
54698
|
const durationMs = Date.now() - startTime;
|
|
54667
|
-
|
|
54699
|
+
patternInventory = await buildPatternInventory(cwd, config);
|
|
54668
54700
|
const inventory = buildInventoryFromScan(
|
|
54669
54701
|
{ cwd, results },
|
|
54670
54702
|
patternInventory,
|
|
@@ -54698,6 +54730,7 @@ async function persistRun(input) {
|
|
|
54698
54730
|
}
|
|
54699
54731
|
}
|
|
54700
54732
|
}
|
|
54733
|
+
recordTelemetry(cwd, report, results, config, patternInventory);
|
|
54701
54734
|
}
|
|
54702
54735
|
var init_persistRun = __esm({
|
|
54703
54736
|
"src/cli/report/persistRun.ts"() {
|
|
@@ -55078,6 +55111,7 @@ function formatSarif(report, options) {
|
|
|
55078
55111
|
const results = report.issues.map(
|
|
55079
55112
|
(issue) => buildResultFromIssue(issue, options?.cwd, fileContentCache)
|
|
55080
55113
|
);
|
|
55114
|
+
const driverProperties = report.compositeScore ? { compositeScore: report.compositeScore } : void 0;
|
|
55081
55115
|
const log = {
|
|
55082
55116
|
$schema: "https://json.schemastore.org/sarif-2.1.0.json",
|
|
55083
55117
|
version: "2.1.0",
|
|
@@ -55088,7 +55122,8 @@ function formatSarif(report, options) {
|
|
|
55088
55122
|
name: "slopbrick",
|
|
55089
55123
|
version: report.version,
|
|
55090
55124
|
informationUri: REPO_INFORMATION_URI,
|
|
55091
|
-
rules
|
|
55125
|
+
rules,
|
|
55126
|
+
...driverProperties ? { properties: driverProperties } : {}
|
|
55092
55127
|
}
|
|
55093
55128
|
},
|
|
55094
55129
|
results
|
|
@@ -57075,402 +57110,6 @@ var init_scan2 = __esm({
|
|
|
57075
57110
|
}
|
|
57076
57111
|
});
|
|
57077
57112
|
|
|
57078
|
-
// src/mcp/slop-suggest-structure.ts
|
|
57079
|
-
async function runSuggestWithStructure(args, ctx) {
|
|
57080
|
-
const cached = await readStructureMarkdown(ctx.cwd);
|
|
57081
|
-
if (cached !== null) {
|
|
57082
|
-
return {
|
|
57083
|
-
content: [{ type: "text", text: cached }]
|
|
57084
|
-
};
|
|
57085
|
-
}
|
|
57086
|
-
const { handleToolCall: handleToolCall2 } = await Promise.resolve().then(() => (init_tools(), tools_exports));
|
|
57087
|
-
const result = await handleToolCall2("slop_suggest", args, ctx);
|
|
57088
|
-
if (result.isError) return result;
|
|
57089
|
-
try {
|
|
57090
|
-
const parsed = JSON.parse(result.content[0].text);
|
|
57091
|
-
if (parsed !== null && typeof parsed === "object") {
|
|
57092
|
-
parsed.structureHint = STRUCTURE_NOT_FOUND_HINT;
|
|
57093
|
-
return {
|
|
57094
|
-
content: [{ type: "text", text: JSON.stringify(parsed, null, 2) }]
|
|
57095
|
-
};
|
|
57096
|
-
}
|
|
57097
|
-
} catch {
|
|
57098
|
-
}
|
|
57099
|
-
return result;
|
|
57100
|
-
}
|
|
57101
|
-
var STRUCTURE_NOT_FOUND_HINT;
|
|
57102
|
-
var init_slop_suggest_structure = __esm({
|
|
57103
|
-
"src/mcp/slop-suggest-structure.ts"() {
|
|
57104
|
-
"use strict";
|
|
57105
|
-
init_structure_md();
|
|
57106
|
-
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.";
|
|
57107
|
-
}
|
|
57108
|
-
});
|
|
57109
|
-
|
|
57110
|
-
// src/mcp/tools.ts
|
|
57111
|
-
var tools_exports = {};
|
|
57112
|
-
__export(tools_exports, {
|
|
57113
|
-
TOOL_DEFINITIONS: () => TOOL_DEFINITIONS,
|
|
57114
|
-
canonicalToolNames: () => canonicalToolNames,
|
|
57115
|
-
getDeprecation: () => getDeprecation,
|
|
57116
|
-
handleToolCall: () => handleToolCall
|
|
57117
|
-
});
|
|
57118
|
-
import { readFileSync as readFileSync27 } from "fs";
|
|
57119
|
-
import { resolve as resolve17 } from "path";
|
|
57120
|
-
function toolError(message) {
|
|
57121
|
-
return {
|
|
57122
|
-
content: [{ type: "text", text: JSON.stringify({ error: message }) }],
|
|
57123
|
-
isError: true
|
|
57124
|
-
};
|
|
57125
|
-
}
|
|
57126
|
-
async function runScanFile(args, ctx) {
|
|
57127
|
-
const path = args.path;
|
|
57128
|
-
if (!path) return toolError("Missing required argument: path");
|
|
57129
|
-
const result = await scanFile(path, ctx.config);
|
|
57130
|
-
const simplified = {
|
|
57131
|
-
filePath: result.filePath,
|
|
57132
|
-
componentCount: result.componentCount,
|
|
57133
|
-
parseError: result.parseError,
|
|
57134
|
-
issues: result.issues.map((i) => ({
|
|
57135
|
-
ruleId: i.ruleId,
|
|
57136
|
-
category: i.category,
|
|
57137
|
-
severity: i.severity,
|
|
57138
|
-
line: i.line,
|
|
57139
|
-
column: i.column,
|
|
57140
|
-
message: i.message,
|
|
57141
|
-
advice: i.advice
|
|
57142
|
-
}))
|
|
57143
|
-
};
|
|
57144
|
-
return {
|
|
57145
|
-
content: [{ type: "text", text: JSON.stringify(simplified, null, 2) }]
|
|
57146
|
-
};
|
|
57147
|
-
}
|
|
57148
|
-
function explainRule2(args, ctx) {
|
|
57149
|
-
const ruleId = args.ruleId;
|
|
57150
|
-
if (!ruleId) return toolError("Missing required argument: ruleId");
|
|
57151
|
-
const rule = ctx.rules.find((r) => r.id === ruleId);
|
|
57152
|
-
if (!rule) return toolError("Unknown rule: " + ruleId);
|
|
57153
|
-
const explanation = {
|
|
57154
|
-
ruleId: rule.id,
|
|
57155
|
-
category: rule.category,
|
|
57156
|
-
severity: rule.severity,
|
|
57157
|
-
aiSpecific: rule.aiSpecific,
|
|
57158
|
-
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 + ".",
|
|
57159
|
-
whereToLook: "src/rules/" + rule.category + "/" + rule.id.replace(/^[^/]+\//, "") + ".ts"
|
|
57160
|
-
};
|
|
57161
|
-
return {
|
|
57162
|
-
content: [{ type: "text", text: JSON.stringify(explanation, null, 2) }]
|
|
57163
|
-
};
|
|
57164
|
-
}
|
|
57165
|
-
function listRules(args, ctx) {
|
|
57166
|
-
const category = args.category;
|
|
57167
|
-
const filtered = category ? ctx.rules.filter((r) => r.category === category) : ctx.rules;
|
|
57168
|
-
const rules = filtered.map((r) => ({
|
|
57169
|
-
id: r.id,
|
|
57170
|
-
category: r.category,
|
|
57171
|
-
severity: r.severity,
|
|
57172
|
-
aiSpecific: r.aiSpecific
|
|
57173
|
-
}));
|
|
57174
|
-
return {
|
|
57175
|
-
content: [
|
|
57176
|
-
{
|
|
57177
|
-
type: "text",
|
|
57178
|
-
text: JSON.stringify({ count: rules.length, rules }, null, 2)
|
|
57179
|
-
}
|
|
57180
|
-
]
|
|
57181
|
-
};
|
|
57182
|
-
}
|
|
57183
|
-
async function runSuggest(args, ctx) {
|
|
57184
|
-
const maxFilesRaw = args.maxFiles;
|
|
57185
|
-
const maxFiles = typeof maxFilesRaw === "number" && Number.isFinite(maxFilesRaw) && maxFilesRaw > 0 ? Math.min(2e3, Math.floor(maxFilesRaw)) : 200;
|
|
57186
|
-
try {
|
|
57187
|
-
const inventory = await buildPatternInventory(ctx.cwd, ctx.config, maxFiles);
|
|
57188
|
-
const doNotCreate = [
|
|
57189
|
-
...ctx.config.constitution?.forbidden ?? []
|
|
57190
|
-
];
|
|
57191
|
-
const declared = /* @__PURE__ */ new Set();
|
|
57192
|
-
for (const list of [
|
|
57193
|
-
ctx.config.constitution?.stateManagement ?? [],
|
|
57194
|
-
ctx.config.constitution?.dataFetching ?? [],
|
|
57195
|
-
ctx.config.constitution?.uiLibrary ?? [],
|
|
57196
|
-
ctx.config.constitution?.forms ?? [],
|
|
57197
|
-
ctx.config.constitution?.styling ?? [],
|
|
57198
|
-
ctx.config.constitution?.routing ?? []
|
|
57199
|
-
]) {
|
|
57200
|
-
for (const lib of list) declared.add(lib);
|
|
57201
|
-
}
|
|
57202
|
-
const doNotCreateCapped = doNotCreate.slice(0, 10);
|
|
57203
|
-
return {
|
|
57204
|
-
content: [
|
|
57205
|
-
{
|
|
57206
|
-
type: "text",
|
|
57207
|
-
text: JSON.stringify(
|
|
57208
|
-
{
|
|
57209
|
-
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.",
|
|
57210
|
-
doNotCreate: doNotCreateCapped,
|
|
57211
|
-
declaredStack: Array.from(declared),
|
|
57212
|
-
existingPatterns: inventory
|
|
57213
|
-
},
|
|
57214
|
-
null,
|
|
57215
|
-
2
|
|
57216
|
-
)
|
|
57217
|
-
}
|
|
57218
|
-
]
|
|
57219
|
-
};
|
|
57220
|
-
} catch (err) {
|
|
57221
|
-
return toolError(err instanceof Error ? err.message : String(err));
|
|
57222
|
-
}
|
|
57223
|
-
}
|
|
57224
|
-
function runCheckConstitution(args, ctx) {
|
|
57225
|
-
const path = args.path;
|
|
57226
|
-
if (!path) return toolError("Missing required argument: path");
|
|
57227
|
-
const absPath = resolve17(ctx.cwd, path);
|
|
57228
|
-
let source;
|
|
57229
|
-
try {
|
|
57230
|
-
source = readFileSync27(absPath, "utf-8");
|
|
57231
|
-
} catch (err) {
|
|
57232
|
-
return toolError(
|
|
57233
|
-
`Cannot read file ${absPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
57234
|
-
);
|
|
57235
|
-
}
|
|
57236
|
-
const result = checkFileConstitution(source, ctx.config.constitution);
|
|
57237
|
-
return {
|
|
57238
|
-
content: [
|
|
57239
|
-
{
|
|
57240
|
-
type: "text",
|
|
57241
|
-
text: JSON.stringify(
|
|
57242
|
-
{
|
|
57243
|
-
file: absPath,
|
|
57244
|
-
importCount: result.imports.length,
|
|
57245
|
-
violationCount: result.violations.length,
|
|
57246
|
-
imports: result.imports,
|
|
57247
|
-
violations: result.violations,
|
|
57248
|
-
// Field name kept stable for backward compatibility with
|
|
57249
|
-
// older consumers; the value reflects whether the merged
|
|
57250
|
-
// `config.constitution` was declared, detected, or absent.
|
|
57251
|
-
conventionSource: ctx.config.constitution ? "declared-or-detected" : "none"
|
|
57252
|
-
},
|
|
57253
|
-
null,
|
|
57254
|
-
2
|
|
57255
|
-
)
|
|
57256
|
-
}
|
|
57257
|
-
]
|
|
57258
|
-
};
|
|
57259
|
-
}
|
|
57260
|
-
async function runFindSimilar(args, ctx) {
|
|
57261
|
-
const { findSimilarFunctions: findSimilarFunctions2 } = await Promise.resolve().then(() => (init_dist2(), dist_exports2));
|
|
57262
|
-
const hooks = Array.isArray(args.hooks) ? args.hooks : [];
|
|
57263
|
-
const props = Array.isArray(args.props) ? args.props : [];
|
|
57264
|
-
const limitRaw = args.limit;
|
|
57265
|
-
const limit = typeof limitRaw === "number" && Number.isFinite(limitRaw) && limitRaw > 0 ? Math.floor(limitRaw) : 10;
|
|
57266
|
-
try {
|
|
57267
|
-
const matches = await findSimilarFunctions2(
|
|
57268
|
-
{
|
|
57269
|
-
name: typeof args.name === "string" ? args.name : void 0,
|
|
57270
|
-
hooks,
|
|
57271
|
-
props,
|
|
57272
|
-
limit,
|
|
57273
|
-
workspaceDir: ctx.cwd
|
|
57274
|
-
},
|
|
57275
|
-
{ cwd: ctx.cwd }
|
|
57276
|
-
);
|
|
57277
|
-
return {
|
|
57278
|
-
content: [
|
|
57279
|
-
{
|
|
57280
|
-
type: "text",
|
|
57281
|
-
text: JSON.stringify(
|
|
57282
|
-
{
|
|
57283
|
-
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.",
|
|
57284
|
-
count: matches.length,
|
|
57285
|
-
matches: matches.map((m) => ({
|
|
57286
|
-
name: m.signature.name,
|
|
57287
|
-
file: m.signature.fileRel,
|
|
57288
|
-
line: m.signature.line,
|
|
57289
|
-
similarity: Number(m.similarity.toFixed(3)),
|
|
57290
|
-
fingerprint: m.fingerprint,
|
|
57291
|
-
hooks: m.signature.hooks,
|
|
57292
|
-
props: m.signature.props,
|
|
57293
|
-
params: m.signature.params
|
|
57294
|
-
}))
|
|
57295
|
-
},
|
|
57296
|
-
null,
|
|
57297
|
-
2
|
|
57298
|
-
)
|
|
57299
|
-
}
|
|
57300
|
-
]
|
|
57301
|
-
};
|
|
57302
|
-
} catch (err) {
|
|
57303
|
-
return toolError(err instanceof Error ? err.message : String(err));
|
|
57304
|
-
}
|
|
57305
|
-
}
|
|
57306
|
-
async function handleToolCall(toolName, args, ctx) {
|
|
57307
|
-
const deprecation = getDeprecation(toolName);
|
|
57308
|
-
const deprecationNotice = deprecation ? {
|
|
57309
|
-
tool: toolName,
|
|
57310
|
-
replacedBy: deprecation.replacedBy,
|
|
57311
|
-
removedIn: deprecation.removedIn ?? "next major",
|
|
57312
|
-
reason: deprecation.reason
|
|
57313
|
-
} : void 0;
|
|
57314
|
-
switch (toolName) {
|
|
57315
|
-
case "slop_scan_file":
|
|
57316
|
-
return runScanFile(args, ctx);
|
|
57317
|
-
case "slop_explain_rule":
|
|
57318
|
-
return explainRule2(args, ctx);
|
|
57319
|
-
case "slop_list_rules":
|
|
57320
|
-
return listRules(args, ctx);
|
|
57321
|
-
case "slop_suggest":
|
|
57322
|
-
return runSuggest(args, ctx);
|
|
57323
|
-
case "slop_suggest_with_structure":
|
|
57324
|
-
return runSuggestWithStructure(args, ctx);
|
|
57325
|
-
// v0.39.0: removed 3 deprecated tools (slop_governance,
|
|
57326
|
-
// slop_architecture_score, slop_business_logic_score) that
|
|
57327
|
-
// were marked for removal in v0.13.0 but never removed.
|
|
57328
|
-
// Their runner functions (runGovernance, runArchitectureScore,
|
|
57329
|
-
// runBusinessLogicScore) are kept in the file for now
|
|
57330
|
-
// (marked @deprecated) to keep the diff small; they can be
|
|
57331
|
-
// deleted in a follow-up. New clients will never see these
|
|
57332
|
-
// tools listed in the MCP tools/list response.
|
|
57333
|
-
case "slop_check_constitution":
|
|
57334
|
-
return runCheckConstitution(args, ctx);
|
|
57335
|
-
case "slop_find_similar":
|
|
57336
|
-
return runFindSimilar(args, ctx);
|
|
57337
|
-
default:
|
|
57338
|
-
return toolError("Unknown tool: " + toolName);
|
|
57339
|
-
}
|
|
57340
|
-
}
|
|
57341
|
-
function canonicalToolNames() {
|
|
57342
|
-
return TOOL_DEFINITIONS.filter((t) => !t.deprecated).map((t) => t.name);
|
|
57343
|
-
}
|
|
57344
|
-
function getDeprecation(toolName) {
|
|
57345
|
-
return TOOL_DEFINITIONS.find((t) => t.name === toolName)?.deprecated;
|
|
57346
|
-
}
|
|
57347
|
-
var TOOL_DEFINITIONS;
|
|
57348
|
-
var init_tools = __esm({
|
|
57349
|
-
"src/mcp/tools.ts"() {
|
|
57350
|
-
"use strict";
|
|
57351
|
-
init_worker();
|
|
57352
|
-
init_patterns();
|
|
57353
|
-
init_architecture_score();
|
|
57354
|
-
init_business_logic();
|
|
57355
|
-
init_slop_suggest_structure();
|
|
57356
|
-
TOOL_DEFINITIONS = [
|
|
57357
|
-
{
|
|
57358
|
-
name: "slop_scan_file",
|
|
57359
|
-
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.",
|
|
57360
|
-
inputSchema: {
|
|
57361
|
-
type: "object",
|
|
57362
|
-
properties: {
|
|
57363
|
-
path: { type: "string", description: "Absolute or cwd-relative path to the source file." },
|
|
57364
|
-
framework: {
|
|
57365
|
-
type: "string",
|
|
57366
|
-
enum: ["react", "vue", "svelte", "astro", "html"],
|
|
57367
|
-
description: "Framework multiplier to apply. Defaults to the configured framework."
|
|
57368
|
-
}
|
|
57369
|
-
},
|
|
57370
|
-
required: ["path"]
|
|
57371
|
-
}
|
|
57372
|
-
},
|
|
57373
|
-
{
|
|
57374
|
-
name: "slop_explain_rule",
|
|
57375
|
-
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.",
|
|
57376
|
-
inputSchema: {
|
|
57377
|
-
type: "object",
|
|
57378
|
-
properties: {
|
|
57379
|
-
ruleId: { type: "string", description: 'e.g. "visual/ai-default-palette".' }
|
|
57380
|
-
},
|
|
57381
|
-
required: ["ruleId"]
|
|
57382
|
-
}
|
|
57383
|
-
},
|
|
57384
|
-
{
|
|
57385
|
-
name: "slop_list_rules",
|
|
57386
|
-
description: "List all registered rules with their category, severity, and aiSpecific flag. Optional category filter (visual | logic | wcag | security | perf | typo | layout | component | arch).",
|
|
57387
|
-
inputSchema: {
|
|
57388
|
-
type: "object",
|
|
57389
|
-
properties: {
|
|
57390
|
-
category: { type: "string", description: "Optional category filter." }
|
|
57391
|
-
}
|
|
57392
|
-
}
|
|
57393
|
-
},
|
|
57394
|
-
{
|
|
57395
|
-
name: "slop_suggest",
|
|
57396
|
-
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.",
|
|
57397
|
-
inputSchema: {
|
|
57398
|
-
type: "object",
|
|
57399
|
-
properties: {
|
|
57400
|
-
maxFiles: {
|
|
57401
|
-
type: "number",
|
|
57402
|
-
description: "Cap on files scanned to keep the inventory cheap. Defaults to 200."
|
|
57403
|
-
}
|
|
57404
|
-
}
|
|
57405
|
-
}
|
|
57406
|
-
},
|
|
57407
|
-
{
|
|
57408
|
-
name: "slop_suggest_with_structure",
|
|
57409
|
-
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.",
|
|
57410
|
-
inputSchema: {
|
|
57411
|
-
type: "object",
|
|
57412
|
-
properties: {
|
|
57413
|
-
maxFiles: {
|
|
57414
|
-
type: "number",
|
|
57415
|
-
description: "Cap on files scanned for the slow-path fallback. Defaults to 200."
|
|
57416
|
-
}
|
|
57417
|
-
}
|
|
57418
|
-
}
|
|
57419
|
-
},
|
|
57420
|
-
{
|
|
57421
|
-
// v0.39.0: removed 3 deprecated tools (slop_governance,
|
|
57422
|
-
// slop_architecture_score, slop_business_logic_score) that
|
|
57423
|
-
// were marked for removal in v0.13.0 but never removed.
|
|
57424
|
-
// They were strict subsets of slop_suggest; users should
|
|
57425
|
-
// call slop_suggest and read repositoryHealth /
|
|
57426
|
-
// architectureConsistency / businessLogicCoherence.
|
|
57427
|
-
name: "slop_check_constitution",
|
|
57428
|
-
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.",
|
|
57429
|
-
inputSchema: {
|
|
57430
|
-
type: "object",
|
|
57431
|
-
properties: {
|
|
57432
|
-
path: { type: "string", description: "Absolute or cwd-relative path to the source file." }
|
|
57433
|
-
},
|
|
57434
|
-
required: ["path"]
|
|
57435
|
-
}
|
|
57436
|
-
},
|
|
57437
|
-
{
|
|
57438
|
-
// v0.10.1: find_similar_function. The GIR (Give-Implementation-
|
|
57439
|
-
// Reference) primitive for slop_suggest. Given a function signature
|
|
57440
|
-
// (name + hooks + props), find the most similar existing
|
|
57441
|
-
// implementations across the codebase. Uses AST fingerprints
|
|
57442
|
-
// (sha256 over sorted hooks ∪ props ∪ params) + Jaccard similarity
|
|
57443
|
-
// — no LLM, no embeddings, deterministic. Foundation for StackPick.
|
|
57444
|
-
name: "slop_find_similar",
|
|
57445
|
-
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].",
|
|
57446
|
-
inputSchema: {
|
|
57447
|
-
type: "object",
|
|
57448
|
-
properties: {
|
|
57449
|
-
name: {
|
|
57450
|
-
type: "string",
|
|
57451
|
-
description: "Function/component name to match. Omit to match by hooks+props only."
|
|
57452
|
-
},
|
|
57453
|
-
hooks: {
|
|
57454
|
-
type: "array",
|
|
57455
|
-
items: { type: "string" },
|
|
57456
|
-
description: 'React hooks used by the target signature, e.g. ["useState", "useEffect"].'
|
|
57457
|
-
},
|
|
57458
|
-
props: {
|
|
57459
|
-
type: "array",
|
|
57460
|
-
items: { type: "string" },
|
|
57461
|
-
description: 'Component props for the target signature, e.g. ["variant", "size", "children"].'
|
|
57462
|
-
},
|
|
57463
|
-
limit: {
|
|
57464
|
-
type: "number",
|
|
57465
|
-
description: "Top-k results to return. Default 10. Capped at 50."
|
|
57466
|
-
}
|
|
57467
|
-
}
|
|
57468
|
-
}
|
|
57469
|
-
}
|
|
57470
|
-
];
|
|
57471
|
-
}
|
|
57472
|
-
});
|
|
57473
|
-
|
|
57474
57113
|
// src/index.ts
|
|
57475
57114
|
init_types();
|
|
57476
57115
|
init_config2();
|
|
@@ -62141,7 +61780,374 @@ init_logger();
|
|
|
62141
61780
|
init_builtins();
|
|
62142
61781
|
init_config2();
|
|
62143
61782
|
init_header();
|
|
62144
|
-
|
|
61783
|
+
|
|
61784
|
+
// src/mcp/tools.ts
|
|
61785
|
+
init_worker();
|
|
61786
|
+
init_patterns();
|
|
61787
|
+
init_architecture_score();
|
|
61788
|
+
init_business_logic();
|
|
61789
|
+
init_structure_md();
|
|
61790
|
+
import { readFileSync as readFileSync27 } from "fs";
|
|
61791
|
+
import { resolve as resolve17 } from "path";
|
|
61792
|
+
var TOOL_DEFINITIONS = [
|
|
61793
|
+
{
|
|
61794
|
+
name: "slop_scan_file",
|
|
61795
|
+
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.",
|
|
61796
|
+
inputSchema: {
|
|
61797
|
+
type: "object",
|
|
61798
|
+
properties: {
|
|
61799
|
+
path: { type: "string", description: "Absolute or cwd-relative path to the source file." },
|
|
61800
|
+
framework: {
|
|
61801
|
+
type: "string",
|
|
61802
|
+
enum: ["react", "vue", "svelte", "astro", "html"],
|
|
61803
|
+
description: "Framework multiplier to apply. Defaults to the configured framework."
|
|
61804
|
+
}
|
|
61805
|
+
},
|
|
61806
|
+
required: ["path"]
|
|
61807
|
+
}
|
|
61808
|
+
},
|
|
61809
|
+
{
|
|
61810
|
+
name: "slop_explain_rule",
|
|
61811
|
+
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.",
|
|
61812
|
+
inputSchema: {
|
|
61813
|
+
type: "object",
|
|
61814
|
+
properties: {
|
|
61815
|
+
ruleId: { type: "string", description: 'e.g. "visual/ai-default-palette".' }
|
|
61816
|
+
},
|
|
61817
|
+
required: ["ruleId"]
|
|
61818
|
+
}
|
|
61819
|
+
},
|
|
61820
|
+
{
|
|
61821
|
+
name: "slop_list_rules",
|
|
61822
|
+
description: "List all registered rules with their category, severity, and aiSpecific flag. Optional category filter (visual | logic | wcag | security | perf | typo | layout | component | arch).",
|
|
61823
|
+
inputSchema: {
|
|
61824
|
+
type: "object",
|
|
61825
|
+
properties: {
|
|
61826
|
+
category: { type: "string", description: "Optional category filter." }
|
|
61827
|
+
}
|
|
61828
|
+
}
|
|
61829
|
+
},
|
|
61830
|
+
{
|
|
61831
|
+
name: "slop_suggest",
|
|
61832
|
+
description: "**Primary entry point for AI agents.** Returns the project's existing patterns (modals, buttons, api clients, state libs, data-fetching libs), the do-not-create list (forbidden imports + canonical patterns not to duplicate), 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.",
|
|
61833
|
+
inputSchema: {
|
|
61834
|
+
type: "object",
|
|
61835
|
+
properties: {
|
|
61836
|
+
maxFiles: {
|
|
61837
|
+
type: "number",
|
|
61838
|
+
description: "Cap on files scanned to keep the inventory cheap. Defaults to 200."
|
|
61839
|
+
}
|
|
61840
|
+
}
|
|
61841
|
+
}
|
|
61842
|
+
},
|
|
61843
|
+
{
|
|
61844
|
+
name: "slop_suggest_with_structure",
|
|
61845
|
+
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.",
|
|
61846
|
+
inputSchema: {
|
|
61847
|
+
type: "object",
|
|
61848
|
+
properties: {
|
|
61849
|
+
maxFiles: {
|
|
61850
|
+
type: "number",
|
|
61851
|
+
description: "Cap on files scanned for the slow-path fallback. Defaults to 200."
|
|
61852
|
+
}
|
|
61853
|
+
}
|
|
61854
|
+
}
|
|
61855
|
+
},
|
|
61856
|
+
{
|
|
61857
|
+
// v0.39.0: removed 3 deprecated tools (slop_governance,
|
|
61858
|
+
// slop_architecture_score, slop_business_logic_score) that
|
|
61859
|
+
// were marked for removal in v0.13.0 but never removed.
|
|
61860
|
+
// They were strict subsets of slop_suggest; users should
|
|
61861
|
+
// call slop_suggest and read repositoryHealth /
|
|
61862
|
+
// architectureConsistency / businessLogicCoherence.
|
|
61863
|
+
name: "slop_check_constitution",
|
|
61864
|
+
description: "Check a single file against the project's declared constitution (stateManagement, dataFetching, uiLibrary, forms, styling, routing, plus a forbidden deny-list in slopbrick.config.mjs). Returns 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.",
|
|
61865
|
+
inputSchema: {
|
|
61866
|
+
type: "object",
|
|
61867
|
+
properties: {
|
|
61868
|
+
path: { type: "string", description: "Absolute or cwd-relative path to the source file." }
|
|
61869
|
+
},
|
|
61870
|
+
required: ["path"]
|
|
61871
|
+
}
|
|
61872
|
+
},
|
|
61873
|
+
{
|
|
61874
|
+
// v0.10.1: find_similar_function. The GIR (Give-Implementation-
|
|
61875
|
+
// Reference) primitive for slop_suggest. Given a function signature
|
|
61876
|
+
// (name + hooks + props), find the most similar existing
|
|
61877
|
+
// implementations across the codebase. Uses AST fingerprints
|
|
61878
|
+
// (sha256 over sorted hooks ∪ props ∪ params) + Jaccard similarity
|
|
61879
|
+
// — no LLM, no embeddings, deterministic. Foundation for StackPick.
|
|
61880
|
+
name: "slop_find_similar",
|
|
61881
|
+
description: "Find the most similar existing function/component implementations across the codebase, ranked by Jaccard similarity over the union of (hooks \u222A props \u222A params). Use this BEFORE writing new code so the agent reuses an existing pattern instead of inventing a new one. Returns top-k matches with name, file, line, fingerprint, and similarity score in [0, 1].",
|
|
61882
|
+
inputSchema: {
|
|
61883
|
+
type: "object",
|
|
61884
|
+
properties: {
|
|
61885
|
+
name: {
|
|
61886
|
+
type: "string",
|
|
61887
|
+
description: "Function/component name to match. Omit to match by hooks+props only."
|
|
61888
|
+
},
|
|
61889
|
+
hooks: {
|
|
61890
|
+
type: "array",
|
|
61891
|
+
items: { type: "string" },
|
|
61892
|
+
description: 'React hooks used by the target signature, e.g. ["useState", "useEffect"].'
|
|
61893
|
+
},
|
|
61894
|
+
props: {
|
|
61895
|
+
type: "array",
|
|
61896
|
+
items: { type: "string" },
|
|
61897
|
+
description: 'Component props for the target signature, e.g. ["variant", "size", "children"].'
|
|
61898
|
+
},
|
|
61899
|
+
limit: {
|
|
61900
|
+
type: "number",
|
|
61901
|
+
description: "Top-k results to return. Default 10. Capped at 50."
|
|
61902
|
+
}
|
|
61903
|
+
}
|
|
61904
|
+
}
|
|
61905
|
+
}
|
|
61906
|
+
];
|
|
61907
|
+
function toolError(message) {
|
|
61908
|
+
return {
|
|
61909
|
+
content: [{ type: "text", text: JSON.stringify({ error: message }) }],
|
|
61910
|
+
isError: true
|
|
61911
|
+
};
|
|
61912
|
+
}
|
|
61913
|
+
async function runScanFile(args, ctx) {
|
|
61914
|
+
const path = args.path;
|
|
61915
|
+
if (!path) return toolError("Missing required argument: path");
|
|
61916
|
+
const result = await scanFile(path, ctx.config);
|
|
61917
|
+
const simplified = {
|
|
61918
|
+
filePath: result.filePath,
|
|
61919
|
+
componentCount: result.componentCount,
|
|
61920
|
+
parseError: result.parseError,
|
|
61921
|
+
issues: result.issues.map((i) => ({
|
|
61922
|
+
ruleId: i.ruleId,
|
|
61923
|
+
category: i.category,
|
|
61924
|
+
severity: i.severity,
|
|
61925
|
+
line: i.line,
|
|
61926
|
+
column: i.column,
|
|
61927
|
+
message: i.message,
|
|
61928
|
+
advice: i.advice
|
|
61929
|
+
}))
|
|
61930
|
+
};
|
|
61931
|
+
return {
|
|
61932
|
+
content: [{ type: "text", text: JSON.stringify(simplified, null, 2) }]
|
|
61933
|
+
};
|
|
61934
|
+
}
|
|
61935
|
+
function explainRule2(args, ctx) {
|
|
61936
|
+
const ruleId = args.ruleId;
|
|
61937
|
+
if (!ruleId) return toolError("Missing required argument: ruleId");
|
|
61938
|
+
const rule = ctx.rules.find((r) => r.id === ruleId);
|
|
61939
|
+
if (!rule) return toolError("Unknown rule: " + ruleId);
|
|
61940
|
+
const explanation = {
|
|
61941
|
+
ruleId: rule.id,
|
|
61942
|
+
category: rule.category,
|
|
61943
|
+
severity: rule.severity,
|
|
61944
|
+
aiSpecific: rule.aiSpecific,
|
|
61945
|
+
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 + ".",
|
|
61946
|
+
whereToLook: "src/rules/" + rule.category + "/" + rule.id.replace(/^[^/]+\//, "") + ".ts"
|
|
61947
|
+
};
|
|
61948
|
+
return {
|
|
61949
|
+
content: [{ type: "text", text: JSON.stringify(explanation, null, 2) }]
|
|
61950
|
+
};
|
|
61951
|
+
}
|
|
61952
|
+
function listRules(args, ctx) {
|
|
61953
|
+
const category = args.category;
|
|
61954
|
+
const filtered = category ? ctx.rules.filter((r) => r.category === category) : ctx.rules;
|
|
61955
|
+
const rules = filtered.map((r) => ({
|
|
61956
|
+
id: r.id,
|
|
61957
|
+
category: r.category,
|
|
61958
|
+
severity: r.severity,
|
|
61959
|
+
aiSpecific: r.aiSpecific
|
|
61960
|
+
}));
|
|
61961
|
+
return {
|
|
61962
|
+
content: [
|
|
61963
|
+
{
|
|
61964
|
+
type: "text",
|
|
61965
|
+
text: JSON.stringify({ count: rules.length, rules }, null, 2)
|
|
61966
|
+
}
|
|
61967
|
+
]
|
|
61968
|
+
};
|
|
61969
|
+
}
|
|
61970
|
+
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.";
|
|
61971
|
+
async function runSuggest(args, ctx, options = {}) {
|
|
61972
|
+
const { includeStructure = false } = options;
|
|
61973
|
+
if (includeStructure) {
|
|
61974
|
+
const cached = await readStructureMarkdown(ctx.cwd);
|
|
61975
|
+
if (cached !== null) {
|
|
61976
|
+
return {
|
|
61977
|
+
content: [{ type: "text", text: cached }]
|
|
61978
|
+
};
|
|
61979
|
+
}
|
|
61980
|
+
}
|
|
61981
|
+
const maxFilesRaw = args.maxFiles;
|
|
61982
|
+
const maxFiles = typeof maxFilesRaw === "number" && Number.isFinite(maxFilesRaw) && maxFilesRaw > 0 ? Math.min(2e3, Math.floor(maxFilesRaw)) : 200;
|
|
61983
|
+
try {
|
|
61984
|
+
const inventory = await buildPatternInventory(ctx.cwd, ctx.config, maxFiles);
|
|
61985
|
+
const doNotCreate = [
|
|
61986
|
+
...ctx.config.constitution?.forbidden ?? []
|
|
61987
|
+
];
|
|
61988
|
+
const declared = /* @__PURE__ */ new Set();
|
|
61989
|
+
for (const list of [
|
|
61990
|
+
ctx.config.constitution?.stateManagement ?? [],
|
|
61991
|
+
ctx.config.constitution?.dataFetching ?? [],
|
|
61992
|
+
ctx.config.constitution?.uiLibrary ?? [],
|
|
61993
|
+
ctx.config.constitution?.forms ?? [],
|
|
61994
|
+
ctx.config.constitution?.styling ?? [],
|
|
61995
|
+
ctx.config.constitution?.routing ?? []
|
|
61996
|
+
]) {
|
|
61997
|
+
for (const lib of list) declared.add(lib);
|
|
61998
|
+
}
|
|
61999
|
+
const doNotCreateCapped = doNotCreate.slice(0, 10);
|
|
62000
|
+
const payload = {
|
|
62001
|
+
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.",
|
|
62002
|
+
doNotCreate: doNotCreateCapped,
|
|
62003
|
+
declaredStack: Array.from(declared),
|
|
62004
|
+
existingPatterns: inventory
|
|
62005
|
+
};
|
|
62006
|
+
if (includeStructure) {
|
|
62007
|
+
payload.structureHint = STRUCTURE_NOT_FOUND_HINT;
|
|
62008
|
+
}
|
|
62009
|
+
try {
|
|
62010
|
+
const { loadHealth: loadHealth2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
|
|
62011
|
+
const health = loadHealth2(ctx.cwd);
|
|
62012
|
+
if (health?.compositeScore) {
|
|
62013
|
+
payload.compositeScore = health.compositeScore;
|
|
62014
|
+
}
|
|
62015
|
+
} catch {
|
|
62016
|
+
}
|
|
62017
|
+
return {
|
|
62018
|
+
content: [
|
|
62019
|
+
{
|
|
62020
|
+
type: "text",
|
|
62021
|
+
text: JSON.stringify(payload, null, 2)
|
|
62022
|
+
}
|
|
62023
|
+
]
|
|
62024
|
+
};
|
|
62025
|
+
} catch (err) {
|
|
62026
|
+
return toolError(err instanceof Error ? err.message : String(err));
|
|
62027
|
+
}
|
|
62028
|
+
}
|
|
62029
|
+
function runCheckConstitution(args, ctx) {
|
|
62030
|
+
const path = args.path;
|
|
62031
|
+
if (!path) return toolError("Missing required argument: path");
|
|
62032
|
+
const absPath = resolve17(ctx.cwd, path);
|
|
62033
|
+
let source;
|
|
62034
|
+
try {
|
|
62035
|
+
source = readFileSync27(absPath, "utf-8");
|
|
62036
|
+
} catch (err) {
|
|
62037
|
+
return toolError(
|
|
62038
|
+
`Cannot read file ${absPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
62039
|
+
);
|
|
62040
|
+
}
|
|
62041
|
+
const result = checkFileConstitution(source, ctx.config.constitution);
|
|
62042
|
+
return {
|
|
62043
|
+
content: [
|
|
62044
|
+
{
|
|
62045
|
+
type: "text",
|
|
62046
|
+
text: JSON.stringify(
|
|
62047
|
+
{
|
|
62048
|
+
file: absPath,
|
|
62049
|
+
importCount: result.imports.length,
|
|
62050
|
+
violationCount: result.violations.length,
|
|
62051
|
+
imports: result.imports,
|
|
62052
|
+
violations: result.violations,
|
|
62053
|
+
// Field name kept stable for backward compatibility with
|
|
62054
|
+
// older consumers; the value reflects whether the merged
|
|
62055
|
+
// `config.constitution` was declared, detected, or absent.
|
|
62056
|
+
conventionSource: ctx.config.constitution ? "declared-or-detected" : "none"
|
|
62057
|
+
},
|
|
62058
|
+
null,
|
|
62059
|
+
2
|
|
62060
|
+
)
|
|
62061
|
+
}
|
|
62062
|
+
]
|
|
62063
|
+
};
|
|
62064
|
+
}
|
|
62065
|
+
async function runFindSimilar(args, ctx) {
|
|
62066
|
+
const { findSimilarFunctions: findSimilarFunctions2 } = await Promise.resolve().then(() => (init_dist2(), dist_exports2));
|
|
62067
|
+
const hooks = Array.isArray(args.hooks) ? args.hooks : [];
|
|
62068
|
+
const props = Array.isArray(args.props) ? args.props : [];
|
|
62069
|
+
const limitRaw = args.limit;
|
|
62070
|
+
const limit = typeof limitRaw === "number" && Number.isFinite(limitRaw) && limitRaw > 0 ? Math.floor(limitRaw) : 10;
|
|
62071
|
+
try {
|
|
62072
|
+
const matches = await findSimilarFunctions2(
|
|
62073
|
+
{
|
|
62074
|
+
name: typeof args.name === "string" ? args.name : void 0,
|
|
62075
|
+
hooks,
|
|
62076
|
+
props,
|
|
62077
|
+
limit,
|
|
62078
|
+
workspaceDir: ctx.cwd
|
|
62079
|
+
},
|
|
62080
|
+
{ cwd: ctx.cwd }
|
|
62081
|
+
);
|
|
62082
|
+
return {
|
|
62083
|
+
content: [
|
|
62084
|
+
{
|
|
62085
|
+
type: "text",
|
|
62086
|
+
text: JSON.stringify(
|
|
62087
|
+
{
|
|
62088
|
+
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.",
|
|
62089
|
+
count: matches.length,
|
|
62090
|
+
matches: matches.map((m) => ({
|
|
62091
|
+
name: m.signature.name,
|
|
62092
|
+
file: m.signature.fileRel,
|
|
62093
|
+
line: m.signature.line,
|
|
62094
|
+
similarity: Number(m.similarity.toFixed(3)),
|
|
62095
|
+
fingerprint: m.fingerprint,
|
|
62096
|
+
hooks: m.signature.hooks,
|
|
62097
|
+
props: m.signature.props,
|
|
62098
|
+
params: m.signature.params
|
|
62099
|
+
}))
|
|
62100
|
+
},
|
|
62101
|
+
null,
|
|
62102
|
+
2
|
|
62103
|
+
)
|
|
62104
|
+
}
|
|
62105
|
+
]
|
|
62106
|
+
};
|
|
62107
|
+
} catch (err) {
|
|
62108
|
+
return toolError(err instanceof Error ? err.message : String(err));
|
|
62109
|
+
}
|
|
62110
|
+
}
|
|
62111
|
+
async function handleToolCall(toolName, args, ctx) {
|
|
62112
|
+
const deprecation = getDeprecation(toolName);
|
|
62113
|
+
const deprecationNotice = deprecation ? {
|
|
62114
|
+
tool: toolName,
|
|
62115
|
+
replacedBy: deprecation.replacedBy,
|
|
62116
|
+
removedIn: deprecation.removedIn ?? "next major",
|
|
62117
|
+
reason: deprecation.reason
|
|
62118
|
+
} : void 0;
|
|
62119
|
+
switch (toolName) {
|
|
62120
|
+
case "slop_scan_file":
|
|
62121
|
+
return runScanFile(args, ctx);
|
|
62122
|
+
case "slop_explain_rule":
|
|
62123
|
+
return explainRule2(args, ctx);
|
|
62124
|
+
case "slop_list_rules":
|
|
62125
|
+
return listRules(args, ctx);
|
|
62126
|
+
case "slop_suggest":
|
|
62127
|
+
return runSuggest(args, ctx);
|
|
62128
|
+
case "slop_suggest_with_structure":
|
|
62129
|
+
return runSuggest(args, ctx, { includeStructure: true });
|
|
62130
|
+
// v0.39.0: removed 3 deprecated tools (slop_governance,
|
|
62131
|
+
// slop_architecture_score, slop_business_logic_score) that
|
|
62132
|
+
// were marked for removal in v0.13.0 but never removed.
|
|
62133
|
+
// Their runner functions (runGovernance, runArchitectureScore,
|
|
62134
|
+
// runBusinessLogicScore) are kept in the file for now
|
|
62135
|
+
// (marked @deprecated) to keep the diff small; they can be
|
|
62136
|
+
// deleted in a follow-up. New clients will never see these
|
|
62137
|
+
// tools listed in the MCP tools/list response.
|
|
62138
|
+
case "slop_check_constitution":
|
|
62139
|
+
return runCheckConstitution(args, ctx);
|
|
62140
|
+
case "slop_find_similar":
|
|
62141
|
+
return runFindSimilar(args, ctx);
|
|
62142
|
+
default:
|
|
62143
|
+
return toolError("Unknown tool: " + toolName);
|
|
62144
|
+
}
|
|
62145
|
+
}
|
|
62146
|
+
function getDeprecation(toolName) {
|
|
62147
|
+
return TOOL_DEFINITIONS.find((t) => t.name === toolName)?.deprecated;
|
|
62148
|
+
}
|
|
62149
|
+
|
|
62150
|
+
// src/mcp/server.ts
|
|
62145
62151
|
var SERVER_INFO = {
|
|
62146
62152
|
name: "slopbrick",
|
|
62147
62153
|
// v0.39.0: use the VERSION constant (same source as the CLI)
|
|
@@ -63579,6 +63585,7 @@ import { resolve as resolve26 } from "path";
|
|
|
63579
63585
|
// src/cli/drift.ts
|
|
63580
63586
|
init_discover();
|
|
63581
63587
|
init_patterns();
|
|
63588
|
+
init_telemetry();
|
|
63582
63589
|
import { readFileSync as readFileSync30 } from "fs";
|
|
63583
63590
|
import { basename as basename4, relative as relative16 } from "path";
|
|
63584
63591
|
async function runDrift(cwd, config, options = {}) {
|
|
@@ -63688,26 +63695,217 @@ function formatDrift(result, opts = {}) {
|
|
|
63688
63695
|
function driftExitCode(result) {
|
|
63689
63696
|
return result.totalViolations > 0 ? 1 : 0;
|
|
63690
63697
|
}
|
|
63698
|
+
async function runDriftOverTime(cwd, config, options) {
|
|
63699
|
+
const payloads = readTelemetry(cwd);
|
|
63700
|
+
const sortedAsc = [...payloads].sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
|
63701
|
+
const withInventory = sortedAsc.filter((p) => p.inventory !== void 0);
|
|
63702
|
+
const baselineSource = options.since === "baseline" ? "baseline" : "since";
|
|
63703
|
+
let baseline;
|
|
63704
|
+
if (baselineSource === "baseline") {
|
|
63705
|
+
baseline = withInventory[0];
|
|
63706
|
+
} else {
|
|
63707
|
+
baseline = withInventory.find((p) => p.timestamp >= options.since);
|
|
63708
|
+
}
|
|
63709
|
+
const current = withInventory.at(-1);
|
|
63710
|
+
if (!baseline || !current) {
|
|
63711
|
+
return {
|
|
63712
|
+
scannedFiles: 0,
|
|
63713
|
+
filesWithViolations: 0,
|
|
63714
|
+
totalViolations: 0,
|
|
63715
|
+
byCategory: {},
|
|
63716
|
+
byFile: [],
|
|
63717
|
+
conventionSource: deriveSource(config.constitution),
|
|
63718
|
+
constitution: config.constitution,
|
|
63719
|
+
introduced: [],
|
|
63720
|
+
removed: [],
|
|
63721
|
+
introducedUndeclared: [],
|
|
63722
|
+
snapshotsConsidered: withInventory.length,
|
|
63723
|
+
driftScore: 0,
|
|
63724
|
+
baselineAt: baseline?.timestamp ?? "",
|
|
63725
|
+
currentAt: current?.timestamp ?? "",
|
|
63726
|
+
baselineSource
|
|
63727
|
+
};
|
|
63728
|
+
}
|
|
63729
|
+
const baselineNames = flattenPatternNames(baseline.inventory.patternNames);
|
|
63730
|
+
const currentNames = flattenPatternNames(current.inventory.patternNames);
|
|
63731
|
+
const baselineSet = new Set(baselineNames.map((p) => `${p.category}\0${p.name}`));
|
|
63732
|
+
const currentSet = new Set(currentNames.map((p) => `${p.category}\0${p.name}`));
|
|
63733
|
+
const introduced = [];
|
|
63734
|
+
const removed = [];
|
|
63735
|
+
for (const p of currentNames) {
|
|
63736
|
+
const key = `${p.category}\0${p.name}`;
|
|
63737
|
+
if (!baselineSet.has(key)) introduced.push(p);
|
|
63738
|
+
}
|
|
63739
|
+
for (const p of baselineNames) {
|
|
63740
|
+
const key = `${p.category}\0${p.name}`;
|
|
63741
|
+
if (!currentSet.has(key)) removed.push(p);
|
|
63742
|
+
}
|
|
63743
|
+
introduced.sort((a, b) => a.category.localeCompare(b.category) || a.name.localeCompare(b.name));
|
|
63744
|
+
removed.sort((a, b) => a.category.localeCompare(b.category) || a.name.localeCompare(b.name));
|
|
63745
|
+
const declared = collectDeclaredNames(config.constitution);
|
|
63746
|
+
const introducedUndeclared = introduced.filter((p) => !declared.has(p.name.toLowerCase()));
|
|
63747
|
+
const baselineTotal = Math.max(baselineSet.size, 1);
|
|
63748
|
+
const driftScore = Math.min(
|
|
63749
|
+
100,
|
|
63750
|
+
Math.round((introduced.length + removed.length) / baselineTotal * 100)
|
|
63751
|
+
);
|
|
63752
|
+
return {
|
|
63753
|
+
scannedFiles: current.inventory.scannedFiles,
|
|
63754
|
+
filesWithViolations: 0,
|
|
63755
|
+
totalViolations: 0,
|
|
63756
|
+
byCategory: {},
|
|
63757
|
+
byFile: [],
|
|
63758
|
+
conventionSource: deriveSource(config.constitution),
|
|
63759
|
+
constitution: config.constitution,
|
|
63760
|
+
introduced,
|
|
63761
|
+
removed,
|
|
63762
|
+
introducedUndeclared,
|
|
63763
|
+
snapshotsConsidered: withInventory.length,
|
|
63764
|
+
driftScore,
|
|
63765
|
+
baselineAt: baseline.timestamp,
|
|
63766
|
+
currentAt: current.timestamp,
|
|
63767
|
+
baselineSource
|
|
63768
|
+
};
|
|
63769
|
+
}
|
|
63770
|
+
function flattenPatternNames(patternNames) {
|
|
63771
|
+
const out = [];
|
|
63772
|
+
for (const [category, names] of Object.entries(patternNames)) {
|
|
63773
|
+
for (const name of names) {
|
|
63774
|
+
out.push({ category, name });
|
|
63775
|
+
}
|
|
63776
|
+
}
|
|
63777
|
+
out.sort((a, b) => a.category.localeCompare(b.category) || a.name.localeCompare(b.name));
|
|
63778
|
+
return out;
|
|
63779
|
+
}
|
|
63780
|
+
function collectDeclaredNames(constitution) {
|
|
63781
|
+
const out = /* @__PURE__ */ new Set();
|
|
63782
|
+
if (!constitution) return out;
|
|
63783
|
+
const fields = ["stateManagement", "dataFetching", "uiLibrary", "forms", "styling", "routing"];
|
|
63784
|
+
for (const f of fields) {
|
|
63785
|
+
for (const v of constitution[f] ?? []) {
|
|
63786
|
+
out.add(v.toLowerCase());
|
|
63787
|
+
}
|
|
63788
|
+
}
|
|
63789
|
+
for (const list of Object.values(constitution.custom ?? {})) {
|
|
63790
|
+
for (const v of list) {
|
|
63791
|
+
out.add(v.toLowerCase());
|
|
63792
|
+
}
|
|
63793
|
+
}
|
|
63794
|
+
for (const v of constitution.forbidden ?? []) {
|
|
63795
|
+
out.add(v.toLowerCase());
|
|
63796
|
+
}
|
|
63797
|
+
return out;
|
|
63798
|
+
}
|
|
63799
|
+
function formatDriftOverTime(result) {
|
|
63800
|
+
const lines = [];
|
|
63801
|
+
lines.push("Temporal drift report");
|
|
63802
|
+
lines.push("");
|
|
63803
|
+
if (result.snapshotsConsidered === 0 || !result.baselineAt || !result.currentAt) {
|
|
63804
|
+
lines.push(" No historical telemetry found at .slopbrick/flywheel/scans.jsonl.");
|
|
63805
|
+
lines.push(" Run a few scans with `slopbrick scan` first; the temporal drift");
|
|
63806
|
+
lines.push(" detector needs \u2265 2 scan payloads to compute a baseline window.");
|
|
63807
|
+
return lines.join("\n");
|
|
63808
|
+
}
|
|
63809
|
+
const sinceLabel = result.baselineSource === "baseline" ? `baseline (oldest scan)` : `since ${result.baselineAt}`;
|
|
63810
|
+
lines.push(` Window: ${sinceLabel} \u2192 ${result.currentAt}`);
|
|
63811
|
+
lines.push(` Snapshots considered: ${result.snapshotsConsidered}`);
|
|
63812
|
+
lines.push(` Drift score: ${result.driftScore} / 100 (informational)`);
|
|
63813
|
+
lines.push("");
|
|
63814
|
+
lines.push(` Patterns introduced: ${result.introduced.length}`);
|
|
63815
|
+
for (const p of result.introduced) {
|
|
63816
|
+
lines.push(` + ${p.category}/${p.name}`);
|
|
63817
|
+
}
|
|
63818
|
+
if (result.introducedUndeclared.length > 0) {
|
|
63819
|
+
lines.push("");
|
|
63820
|
+
lines.push(
|
|
63821
|
+
` Patterns introduced but not in declared constitution: ${result.introducedUndeclared.length}`
|
|
63822
|
+
);
|
|
63823
|
+
for (const p of result.introducedUndeclared) {
|
|
63824
|
+
lines.push(` ! ${p.category}/${p.name}`);
|
|
63825
|
+
}
|
|
63826
|
+
}
|
|
63827
|
+
lines.push("");
|
|
63828
|
+
lines.push(` Patterns removed: ${result.removed.length}`);
|
|
63829
|
+
for (const p of result.removed) {
|
|
63830
|
+
lines.push(` - ${p.category}/${p.name}`);
|
|
63831
|
+
}
|
|
63832
|
+
lines.push("");
|
|
63833
|
+
if (result.introducedUndeclared.length > 0) {
|
|
63834
|
+
lines.push(
|
|
63835
|
+
" Tip: add undeclared patterns to slopbrick.config.mjs#constitution to"
|
|
63836
|
+
);
|
|
63837
|
+
lines.push(" promote them into the declared set on the next scan.");
|
|
63838
|
+
} else if (result.introduced.length === 0 && result.removed.length === 0) {
|
|
63839
|
+
lines.push(" \u2713 No pattern churn since the baseline window.");
|
|
63840
|
+
}
|
|
63841
|
+
return lines.join("\n");
|
|
63842
|
+
}
|
|
63843
|
+
function driftOverTimeExitCode(result) {
|
|
63844
|
+
return result.introducedUndeclared.length > 0 ? 1 : 0;
|
|
63845
|
+
}
|
|
63846
|
+
|
|
63847
|
+
// src/cli/commands/drift.ts
|
|
63848
|
+
init_load();
|
|
63849
|
+
|
|
63850
|
+
// src/cli/commands/_shared.ts
|
|
63851
|
+
init_logger();
|
|
63852
|
+
import { CommanderError } from "commander";
|
|
63853
|
+
function setExitOverride(program) {
|
|
63854
|
+
program.exitOverride();
|
|
63855
|
+
}
|
|
63856
|
+
async function dispatch(program, runFn) {
|
|
63857
|
+
try {
|
|
63858
|
+
await runFn();
|
|
63859
|
+
} catch (err) {
|
|
63860
|
+
if (err instanceof CommanderError) {
|
|
63861
|
+
if (err.code !== "commander.helpDisplayed" && err.code !== "commander.help") {
|
|
63862
|
+
logger.error(err.message);
|
|
63863
|
+
}
|
|
63864
|
+
process.exit(err.exitCode);
|
|
63865
|
+
return;
|
|
63866
|
+
}
|
|
63867
|
+
throw err;
|
|
63868
|
+
}
|
|
63869
|
+
}
|
|
63870
|
+
function withExitCode(result, compute, message) {
|
|
63871
|
+
const code = compute(result);
|
|
63872
|
+
if (code === 0) return;
|
|
63873
|
+
throw new CommanderError(code, "slopbrick.exit", message);
|
|
63874
|
+
}
|
|
63691
63875
|
|
|
63692
63876
|
// src/cli/commands/drift.ts
|
|
63693
63877
|
function registerDrift(program) {
|
|
63694
63878
|
program.command("drift").description(
|
|
63695
63879
|
"detect imports that violate declared constitution (state, data-fetching, UI, forms, styling, routing) or import forbidden packages"
|
|
63696
|
-
).option("--format <pretty|json>", "output format", "pretty").option("--max-files <n>", "cap on files scanned", parseCount, 1e3).
|
|
63880
|
+
).option("--format <pretty|json>", "output format", "pretty").option("--max-files <n>", "cap on files scanned", parseCount, 1e3).option(
|
|
63881
|
+
"--temporal-since <date>",
|
|
63882
|
+
`temporal drift since the given date (ISO-8601, or the literal string \`baseline\` to use the oldest scan in scans.jsonl)`
|
|
63883
|
+
).action(
|
|
63697
63884
|
async (cmdOptions, command) => {
|
|
63698
|
-
|
|
63699
|
-
|
|
63700
|
-
|
|
63701
|
-
|
|
63702
|
-
|
|
63703
|
-
const
|
|
63704
|
-
|
|
63705
|
-
|
|
63706
|
-
|
|
63707
|
-
|
|
63708
|
-
|
|
63709
|
-
|
|
63885
|
+
const options = command.optsWithGlobals();
|
|
63886
|
+
const rawFormat = options.format ?? cmdOptions.format ?? "pretty";
|
|
63887
|
+
const format = rawFormat === "json" || rawFormat === "pretty" ? rawFormat : "pretty";
|
|
63888
|
+
const cwd = resolve26(options.workspace ?? process.cwd());
|
|
63889
|
+
if (cmdOptions.temporalSince !== void 0) {
|
|
63890
|
+
const temporalSince = cmdOptions.temporalSince.trim();
|
|
63891
|
+
if (temporalSince.length === 0) {
|
|
63892
|
+
throw new Error("--temporal-since expects a non-empty ISO date or `baseline`");
|
|
63893
|
+
}
|
|
63894
|
+
const sinceArg = temporalSince === "baseline" ? "baseline" : temporalSince;
|
|
63895
|
+
const config2 = await loadConfig(cwd);
|
|
63896
|
+
const result2 = await runDriftOverTime(cwd, config2, { since: sinceArg });
|
|
63897
|
+
logger.info(formatDriftOverTime(result2));
|
|
63898
|
+
withExitCode(
|
|
63899
|
+
result2,
|
|
63900
|
+
driftOverTimeExitCode,
|
|
63901
|
+
`drift --temporal-since: ${result2.introducedUndeclared.length} undeclared patterns introduced`
|
|
63902
|
+
);
|
|
63903
|
+
return;
|
|
63710
63904
|
}
|
|
63905
|
+
const { config } = await runScan({ ...options, workspace: cwd });
|
|
63906
|
+
const result = await runDrift(cwd, config, { maxFiles: cmdOptions.maxFiles });
|
|
63907
|
+
logger.info(formatDrift(result, { json: format === "json" }));
|
|
63908
|
+
withExitCode(result, driftExitCode, `drift: ${result.totalViolations} violations`);
|
|
63711
63909
|
}
|
|
63712
63910
|
);
|
|
63713
63911
|
}
|
|
@@ -66368,6 +66566,7 @@ async function runCli({ start }) {
|
|
|
66368
66566
|
try {
|
|
66369
66567
|
const program = new Command().name("slopbrick").description("Repository Coherence Scanner \u2014 surface AI-induced pattern drift, secret leaks, and design-token violations").version(VERSION).option("--framework <name>", "framework multiplier to apply").option("--include <glob>", "include pattern (repeatable)", collectGlob, []).option("--exclude <glob>", "exclude pattern (repeatable)", collectGlob, []).option("--ai-only", "only report AI-specific issues").option("--human-only", "only report human-facing issues").option("--ignore-wcag22", "ignore WCAG 2.2 related issues").option("--format <pretty|json|sarif|html>", "output format", "pretty").option("--threads <n>", "number of worker threads", parseThreads).option("--since <ref>", "only scan files changed since git ref").option("--diff <ref>", "alias for --since <ref>; also adds PR Slop Score to the report").option("--workspace <path>", "workspace/project path", process.cwd()).option("--tighten", "tighten baseline allowances").option("--fix", "apply auto-fixes").option("--dry-run", "with --fix: print what would change without writing").option("--show-fixes-diff", "print unified diff of proposed auto-fixes").option("--doctor", "run diagnostics").option("--watch", "watch files and re-run").option("--suggest", "print remediation advice").option("--why-failing", "print the top 5 rules dragging the score down").option("--brief", "terse output (verdict + headline + threshold + delta only)").option("--heatmap", "print migration ROI heatmap").option("--quiet", "suppress non-error output").option("--verbose", "enable debug logging (file paths, timings, rule-fire counts)").option("--strict", "exit 2 if any high-severity issue remains").option("--no-increase", "exit 2 if 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)");
|
|
66370
66568
|
program.helpInformation = () => formatGroupedHelp(program);
|
|
66569
|
+
setExitOverride(program);
|
|
66371
66570
|
registerInit(program);
|
|
66372
66571
|
registerInstall(program);
|
|
66373
66572
|
registerUninstall(program);
|
|
@@ -66552,7 +66751,9 @@ async function runCli({ start }) {
|
|
|
66552
66751
|
program.outputHelp();
|
|
66553
66752
|
process.exit(0);
|
|
66554
66753
|
}
|
|
66555
|
-
await program
|
|
66754
|
+
await dispatch(program, async () => {
|
|
66755
|
+
await program.parseAsync(process.argv);
|
|
66756
|
+
});
|
|
66556
66757
|
} catch (err) {
|
|
66557
66758
|
if (err instanceof ConfigValidationError) {
|
|
66558
66759
|
logger.error(err.message);
|