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.
Files changed (3) hide show
  1. package/dist/index.cjs +621 -420
  2. package/dist/index.js +619 -418
  3. 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.40.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
- recordTelemetry(cwd, report, results, config);
54708
+ let patternInventory;
54677
54709
  if (config.projectMemory !== false) {
54678
54710
  try {
54679
54711
  const durationMs = Date.now() - startTime;
54680
- const patternInventory = await buildPatternInventory(cwd, config);
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 import_commander2 = require("commander");
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
- init_tools();
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).action(
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
- try {
63794
- const options = command.optsWithGlobals();
63795
- const rawFormat = options.format ?? cmdOptions.format ?? "pretty";
63796
- const format = rawFormat === "json" || rawFormat === "pretty" ? rawFormat : "pretty";
63797
- const cwd = (0, import_node_path53.resolve)(options.workspace ?? process.cwd());
63798
- const { config } = await runScan({ ...options, workspace: cwd });
63799
- const result = await runDrift(cwd, config, { maxFiles: cmdOptions.maxFiles });
63800
- logger.info(formatDrift(result, { json: format === "json" }));
63801
- process.exit(driftExitCode(result));
63802
- } catch (err) {
63803
- logger.error(err instanceof Error ? err.message : String(err));
63804
- process.exit(2);
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 import_commander2.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)");
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.parseAsync(process.argv);
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);