slopbrick 0.39.0 → 0.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -36,7 +36,7 @@ var VERSION;
36
36
  var init_header = __esm({
37
37
  "src/types/_header.ts"() {
38
38
  "use strict";
39
- VERSION = "0.39.0";
39
+ VERSION = "0.41.0";
40
40
  }
41
41
  });
42
42
 
@@ -45823,6 +45823,15 @@ function formatCompositeScore(report) {
45823
45823
  )
45824
45824
  );
45825
45825
  }
45826
+ const composite = report.compositeScore;
45827
+ if (composite !== void 0) {
45828
+ lines.push("");
45829
+ lines.push(
45830
+ import_chalk.default.dim(
45831
+ `composite=${composite.tier}@${composite.mean.toFixed(2)} \u2014 project-level Bayesian aggregate across ${composite.fileCount} file${composite.fileCount === 1 ? "" : "s"} (max ${composite.max.toFixed(2)}); informational, does not gate CI.`
45832
+ )
45833
+ );
45834
+ }
45826
45835
  return lines.join("\n");
45827
45836
  }
45828
45837
  function formatCoherenceScores(report) {
@@ -50764,6 +50773,12 @@ function severityBump(severity) {
50764
50773
  const idx = order.indexOf(severity);
50765
50774
  return order[Math.min(idx + 1, order.length - 1)];
50766
50775
  }
50776
+ function severityRelax(severity) {
50777
+ const order = ["off", "low", "medium", "high"];
50778
+ const idx = order.indexOf(severity);
50779
+ if (idx <= 0) return "off";
50780
+ return order[idx - 1];
50781
+ }
50767
50782
  function resolveEffectiveSeverity(override, defaultSeverity) {
50768
50783
  if (override === void 0 || override === "off" || override === "auto") {
50769
50784
  return defaultSeverity;
@@ -50794,6 +50809,7 @@ function countConsecutiveTopFileAppearances(currentHash, recentTopHashes) {
50794
50809
  }
50795
50810
  function computeFlywheelOutput(runs, currentTopFiles, recentTopHashes, unmatchedStringLiterals, config, rules) {
50796
50811
  const autoTuned = [];
50812
+ const autoRelaxed = [];
50797
50813
  const hotspotIssues = [];
50798
50814
  const suggestions = [];
50799
50815
  const ruleById = new Map(rules.map((r) => [r.id, r]));
@@ -50814,6 +50830,29 @@ function computeFlywheelOutput(runs, currentTopFiles, recentTopHashes, unmatched
50814
50830
  }
50815
50831
  }
50816
50832
  }
50833
+ if (runs.length >= IGNORE_THRESHOLD) {
50834
+ const window = runs.slice(-IGNORE_THRESHOLD);
50835
+ const candidateRuleIds = new Set(window.flatMap((r) => r.topOffenseIds));
50836
+ for (const ruleId of candidateRuleIds) {
50837
+ const rule = ruleById.get(ruleId);
50838
+ const defaultSeverity = rule?.severity ?? "medium";
50839
+ const currentSeverity = resolveEffectiveSeverity(
50840
+ config.rules[ruleId] ?? "auto",
50841
+ defaultSeverity
50842
+ );
50843
+ const streak = countConsecutiveTopOffenses(runs, ruleId);
50844
+ if (streak >= IGNORE_THRESHOLD) {
50845
+ autoRelaxed.push({
50846
+ ruleId,
50847
+ severity: severityRelax(currentSeverity),
50848
+ previousSeverity: currentSeverity,
50849
+ reason: `In top-3 offenses for ${streak} consecutive scans; corpus prior ${DEFAULT_RELAXATION_PRIOR.toFixed(2)} suggests the rule is high-FP for this corpus slice.`,
50850
+ defaultPrior: DEFAULT_RELAXATION_PRIOR,
50851
+ relaxedAt: (/* @__PURE__ */ new Date()).toISOString()
50852
+ });
50853
+ }
50854
+ }
50855
+ }
50817
50856
  if (currentTopFiles.length > 0 && runs.length >= CONSECUTIVE_THRESHOLD) {
50818
50857
  for (const file of currentTopFiles) {
50819
50858
  const consecutive = countConsecutiveTopFileAppearances(file.hash, recentTopHashes);
@@ -50848,13 +50887,14 @@ function computeFlywheelOutput(runs, currentTopFiles, recentTopHashes, unmatched
50848
50887
  });
50849
50888
  }
50850
50889
  }
50851
- return { autoTuned, hotspotIssues, suggestions };
50890
+ return { autoTuned, autoRelaxed, hotspotIssues, suggestions };
50852
50891
  }
50853
50892
  function migrateFlywheelState(state) {
50854
50893
  return {
50855
50894
  version: FLYWHEEL_VERSION,
50856
50895
  updatedAt: state.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
50857
50896
  autoTuned: state.autoTuned ?? [],
50897
+ autoRelaxed: state.autoRelaxed ?? [],
50858
50898
  research: state.research
50859
50899
  };
50860
50900
  }
@@ -50910,7 +50950,7 @@ function saveFlywheelState(cwd, state) {
50910
50950
  function hashFile(filePath) {
50911
50951
  return (0, import_node_crypto6.createHash)("sha256").update(filePath).digest("hex").slice(0, 16);
50912
50952
  }
50913
- var import_node_crypto6, import_node_fs14, import_node_path15, FLYWHEEL_DIR, STATE_FILE, CONSECUTIVE_THRESHOLD, FLYWHEEL_VERSION;
50953
+ var import_node_crypto6, import_node_fs14, import_node_path15, FLYWHEEL_DIR, STATE_FILE, CONSECUTIVE_THRESHOLD, IGNORE_THRESHOLD, DEFAULT_RELAXATION_PRIOR, FLYWHEEL_VERSION;
50914
50954
  var init_flywheel = __esm({
50915
50955
  "src/engine/flywheel.ts"() {
50916
50956
  "use strict";
@@ -50920,7 +50960,9 @@ var init_flywheel = __esm({
50920
50960
  FLYWHEEL_DIR = ".slopbrick/flywheel";
50921
50961
  STATE_FILE = "auto-tuned.json";
50922
50962
  CONSECUTIVE_THRESHOLD = 3;
50923
- FLYWHEEL_VERSION = "2";
50963
+ IGNORE_THRESHOLD = 5;
50964
+ DEFAULT_RELAXATION_PRIOR = 0.3;
50965
+ FLYWHEEL_VERSION = "3";
50924
50966
  }
50925
50967
  });
50926
50968
 
@@ -54205,6 +54247,21 @@ function safeRelative(cwd, filePath) {
54205
54247
  return filePath;
54206
54248
  }
54207
54249
  }
54250
+ function buildInventorySummary(inventory) {
54251
+ const patternCounts = {};
54252
+ const patternNames = {};
54253
+ for (const [category, matches] of Object.entries(inventory.patterns)) {
54254
+ if (matches.length === 0) continue;
54255
+ const names = Array.from(new Set(matches.map((m) => m.name))).sort();
54256
+ patternCounts[category] = names.length;
54257
+ patternNames[category] = names.slice(0, TELEMETRY_INVENTORY_NAME_CAP);
54258
+ }
54259
+ return {
54260
+ scannedFiles: inventory.scannedFiles,
54261
+ patternCounts,
54262
+ patternNames
54263
+ };
54264
+ }
54208
54265
  function aggregateViolations(report) {
54209
54266
  const counts = /* @__PURE__ */ new Map();
54210
54267
  for (const issue of report.issues) {
@@ -54284,7 +54341,7 @@ function readTelemetry(cwd) {
54284
54341
  }
54285
54342
  return payloads;
54286
54343
  }
54287
- function recordTelemetry(cwd, report, results, config) {
54344
+ function recordTelemetry(cwd, report, results, config, inventory) {
54288
54345
  if (config.telemetry === false) {
54289
54346
  return void 0;
54290
54347
  }
@@ -54305,7 +54362,14 @@ function recordTelemetry(cwd, report, results, config) {
54305
54362
  framework: config.framework
54306
54363
  },
54307
54364
  violations: aggregateViolations(report),
54308
- files: buildFileRecords(cwd, report, results)
54365
+ files: buildFileRecords(cwd, report, results),
54366
+ // v0.41.0 (Sprint 2, task 2a.1): the inventory summary is
54367
+ // additive — omitting it (legacy callers) keeps the JSONL line
54368
+ // shape identical to v0.40.x payloads, so old readers stay
54369
+ // green. New readers (`slopbrick drift --since <date>` in
54370
+ // Sprint 2a.2) treat the field as optional and fall back to
54371
+ // a re-scan when it's missing.
54372
+ ...inventory ? { inventory: buildInventorySummary(inventory) } : {}
54309
54373
  };
54310
54374
  const path = telemetryPath2(cwd);
54311
54375
  const dir = (0, import_node_path22.dirname)(path);
@@ -54316,7 +54380,7 @@ function recordTelemetry(cwd, report, results, config) {
54316
54380
  (0, import_node_fs20.appendFileSync)(path, JSON.stringify(payload) + "\n", "utf-8");
54317
54381
  return payload;
54318
54382
  }
54319
- var import_node_fs20, import_node_crypto8, import_node_path22, TELEMETRY_DIR, TELEMETRY_FILE2, MAX_TELEMETRY_BYTES, MAX_ROTATED_FILES;
54383
+ var import_node_fs20, import_node_crypto8, import_node_path22, TELEMETRY_DIR, TELEMETRY_FILE2, MAX_TELEMETRY_BYTES, MAX_ROTATED_FILES, TELEMETRY_INVENTORY_NAME_CAP;
54320
54384
  var init_telemetry = __esm({
54321
54385
  "src/engine/telemetry.ts"() {
54322
54386
  "use strict";
@@ -54327,6 +54391,7 @@ var init_telemetry = __esm({
54327
54391
  TELEMETRY_FILE2 = "scans.jsonl";
54328
54392
  MAX_TELEMETRY_BYTES = 10 * 1024 * 1024;
54329
54393
  MAX_ROTATED_FILES = 5;
54394
+ TELEMETRY_INVENTORY_NAME_CAP = 50;
54330
54395
  }
54331
54396
  });
54332
54397
 
@@ -54623,6 +54688,7 @@ async function persistRun(input) {
54623
54688
  );
54624
54689
  const state = loadFlywheelState(cwd);
54625
54690
  state.autoTuned = flywheelOutput.autoTuned;
54691
+ state.autoRelaxed = flywheelOutput.autoRelaxed;
54626
54692
  state.research = loadResearchMetricsFromDisk(cwd);
54627
54693
  state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
54628
54694
  saveFlywheelState(cwd, state);
@@ -54639,11 +54705,11 @@ async function persistRun(input) {
54639
54705
  }
54640
54706
  report.issues.push(...flywheelOutput.hotspotIssues);
54641
54707
  }
54642
- recordTelemetry(cwd, report, results, config);
54708
+ let patternInventory;
54643
54709
  if (config.projectMemory !== false) {
54644
54710
  try {
54645
54711
  const durationMs = Date.now() - startTime;
54646
- const patternInventory = await buildPatternInventory(cwd, config);
54712
+ patternInventory = await buildPatternInventory(cwd, config);
54647
54713
  const inventory = buildInventoryFromScan(
54648
54714
  { cwd, results },
54649
54715
  patternInventory,
@@ -54677,6 +54743,7 @@ async function persistRun(input) {
54677
54743
  }
54678
54744
  }
54679
54745
  }
54746
+ recordTelemetry(cwd, report, results, config, patternInventory);
54680
54747
  }
54681
54748
  var import_node_fs22, import_node_path24;
54682
54749
  var init_persistRun = __esm({
@@ -55058,6 +55125,7 @@ function formatSarif(report, options) {
55058
55125
  const results = report.issues.map(
55059
55126
  (issue) => buildResultFromIssue(issue, options?.cwd, fileContentCache)
55060
55127
  );
55128
+ const driverProperties = report.compositeScore ? { compositeScore: report.compositeScore } : void 0;
55061
55129
  const log = {
55062
55130
  $schema: "https://json.schemastore.org/sarif-2.1.0.json",
55063
55131
  version: "2.1.0",
@@ -55068,7 +55136,8 @@ function formatSarif(report, options) {
55068
55136
  name: "slopbrick",
55069
55137
  version: report.version,
55070
55138
  informationUri: REPO_INFORMATION_URI,
55071
- rules
55139
+ rules,
55140
+ ...driverProperties ? { properties: driverProperties } : {}
55072
55141
  }
55073
55142
  },
55074
55143
  results
@@ -56903,6 +56972,13 @@ async function runScan(options, explicitPaths) {
56903
56972
  if (defaultOffRules.has(tuned.ruleId)) continue;
56904
56973
  config.rules[tuned.ruleId] = tuned.severity;
56905
56974
  }
56975
+ for (const relaxed of flywheelState.autoRelaxed) {
56976
+ if (config.rules[relaxed.ruleId] === "off") {
56977
+ continue;
56978
+ }
56979
+ if (defaultOffRules.has(relaxed.ruleId)) continue;
56980
+ config.rules[relaxed.ruleId] = relaxed.severity;
56981
+ }
56906
56982
  }
56907
56983
  const pool = new WorkerPool({
56908
56984
  config,
@@ -57055,402 +57131,6 @@ var init_scan2 = __esm({
57055
57131
  }
57056
57132
  });
57057
57133
 
57058
- // src/mcp/slop-suggest-structure.ts
57059
- async function runSuggestWithStructure(args, ctx) {
57060
- const cached = await readStructureMarkdown(ctx.cwd);
57061
- if (cached !== null) {
57062
- return {
57063
- content: [{ type: "text", text: cached }]
57064
- };
57065
- }
57066
- const { handleToolCall: handleToolCall2 } = await Promise.resolve().then(() => (init_tools(), tools_exports));
57067
- const result = await handleToolCall2("slop_suggest", args, ctx);
57068
- if (result.isError) return result;
57069
- try {
57070
- const parsed = JSON.parse(result.content[0].text);
57071
- if (parsed !== null && typeof parsed === "object") {
57072
- parsed.structureHint = STRUCTURE_NOT_FOUND_HINT;
57073
- return {
57074
- content: [{ type: "text", text: JSON.stringify(parsed, null, 2) }]
57075
- };
57076
- }
57077
- } catch {
57078
- }
57079
- return result;
57080
- }
57081
- var STRUCTURE_NOT_FOUND_HINT;
57082
- var init_slop_suggest_structure = __esm({
57083
- "src/mcp/slop-suggest-structure.ts"() {
57084
- "use strict";
57085
- init_structure_md();
57086
- STRUCTURE_NOT_FOUND_HINT = "No .slopbrick/structure.md found. Run `slopbrick scan` to persist the pattern inventory, then call this tool again for the O(read file) fast path.";
57087
- }
57088
- });
57089
-
57090
- // src/mcp/tools.ts
57091
- var tools_exports = {};
57092
- __export(tools_exports, {
57093
- TOOL_DEFINITIONS: () => TOOL_DEFINITIONS,
57094
- canonicalToolNames: () => canonicalToolNames,
57095
- getDeprecation: () => getDeprecation,
57096
- handleToolCall: () => handleToolCall
57097
- });
57098
- function toolError(message) {
57099
- return {
57100
- content: [{ type: "text", text: JSON.stringify({ error: message }) }],
57101
- isError: true
57102
- };
57103
- }
57104
- async function runScanFile(args, ctx) {
57105
- const path = args.path;
57106
- if (!path) return toolError("Missing required argument: path");
57107
- const result = await scanFile(path, ctx.config);
57108
- const simplified = {
57109
- filePath: result.filePath,
57110
- componentCount: result.componentCount,
57111
- parseError: result.parseError,
57112
- issues: result.issues.map((i) => ({
57113
- ruleId: i.ruleId,
57114
- category: i.category,
57115
- severity: i.severity,
57116
- line: i.line,
57117
- column: i.column,
57118
- message: i.message,
57119
- advice: i.advice
57120
- }))
57121
- };
57122
- return {
57123
- content: [{ type: "text", text: JSON.stringify(simplified, null, 2) }]
57124
- };
57125
- }
57126
- function explainRule2(args, ctx) {
57127
- const ruleId = args.ruleId;
57128
- if (!ruleId) return toolError("Missing required argument: ruleId");
57129
- const rule = ctx.rules.find((r) => r.id === ruleId);
57130
- if (!rule) return toolError("Unknown rule: " + ruleId);
57131
- const explanation = {
57132
- ruleId: rule.id,
57133
- category: rule.category,
57134
- severity: rule.severity,
57135
- aiSpecific: rule.aiSpecific,
57136
- rationale: "This rule flags " + rule.category + " patterns associated with AI-generated code. It is marked as " + (rule.aiSpecific ? "AI-specific" : "cross-cutting") + ". Severity: " + rule.severity + ".",
57137
- whereToLook: "src/rules/" + rule.category + "/" + rule.id.replace(/^[^/]+\//, "") + ".ts"
57138
- };
57139
- return {
57140
- content: [{ type: "text", text: JSON.stringify(explanation, null, 2) }]
57141
- };
57142
- }
57143
- function listRules(args, ctx) {
57144
- const category = args.category;
57145
- const filtered = category ? ctx.rules.filter((r) => r.category === category) : ctx.rules;
57146
- const rules = filtered.map((r) => ({
57147
- id: r.id,
57148
- category: r.category,
57149
- severity: r.severity,
57150
- aiSpecific: r.aiSpecific
57151
- }));
57152
- return {
57153
- content: [
57154
- {
57155
- type: "text",
57156
- text: JSON.stringify({ count: rules.length, rules }, null, 2)
57157
- }
57158
- ]
57159
- };
57160
- }
57161
- async function runSuggest(args, ctx) {
57162
- const maxFilesRaw = args.maxFiles;
57163
- const maxFiles = typeof maxFilesRaw === "number" && Number.isFinite(maxFilesRaw) && maxFilesRaw > 0 ? Math.min(2e3, Math.floor(maxFilesRaw)) : 200;
57164
- try {
57165
- const inventory = await buildPatternInventory(ctx.cwd, ctx.config, maxFiles);
57166
- const doNotCreate = [
57167
- ...ctx.config.constitution?.forbidden ?? []
57168
- ];
57169
- const declared = /* @__PURE__ */ new Set();
57170
- for (const list of [
57171
- ctx.config.constitution?.stateManagement ?? [],
57172
- ctx.config.constitution?.dataFetching ?? [],
57173
- ctx.config.constitution?.uiLibrary ?? [],
57174
- ctx.config.constitution?.forms ?? [],
57175
- ctx.config.constitution?.styling ?? [],
57176
- ctx.config.constitution?.routing ?? []
57177
- ]) {
57178
- for (const lib of list) declared.add(lib);
57179
- }
57180
- const doNotCreateCapped = doNotCreate.slice(0, 10);
57181
- return {
57182
- content: [
57183
- {
57184
- type: "text",
57185
- text: JSON.stringify(
57186
- {
57187
- hint: "Use these patterns instead of creating new ones. Pick the closest existing entry and import it. The `doNotCreate` list is the deny-list \u2014 never import any of these.",
57188
- doNotCreate: doNotCreateCapped,
57189
- declaredStack: Array.from(declared),
57190
- existingPatterns: inventory
57191
- },
57192
- null,
57193
- 2
57194
- )
57195
- }
57196
- ]
57197
- };
57198
- } catch (err) {
57199
- return toolError(err instanceof Error ? err.message : String(err));
57200
- }
57201
- }
57202
- function runCheckConstitution(args, ctx) {
57203
- const path = args.path;
57204
- if (!path) return toolError("Missing required argument: path");
57205
- const absPath = (0, import_node_path40.resolve)(ctx.cwd, path);
57206
- let source;
57207
- try {
57208
- source = (0, import_node_fs32.readFileSync)(absPath, "utf-8");
57209
- } catch (err) {
57210
- return toolError(
57211
- `Cannot read file ${absPath}: ${err instanceof Error ? err.message : String(err)}`
57212
- );
57213
- }
57214
- const result = checkFileConstitution(source, ctx.config.constitution);
57215
- return {
57216
- content: [
57217
- {
57218
- type: "text",
57219
- text: JSON.stringify(
57220
- {
57221
- file: absPath,
57222
- importCount: result.imports.length,
57223
- violationCount: result.violations.length,
57224
- imports: result.imports,
57225
- violations: result.violations,
57226
- // Field name kept stable for backward compatibility with
57227
- // older consumers; the value reflects whether the merged
57228
- // `config.constitution` was declared, detected, or absent.
57229
- conventionSource: ctx.config.constitution ? "declared-or-detected" : "none"
57230
- },
57231
- null,
57232
- 2
57233
- )
57234
- }
57235
- ]
57236
- };
57237
- }
57238
- async function runFindSimilar(args, ctx) {
57239
- const { findSimilarFunctions: findSimilarFunctions2 } = await Promise.resolve().then(() => (init_dist2(), dist_exports2));
57240
- const hooks = Array.isArray(args.hooks) ? args.hooks : [];
57241
- const props = Array.isArray(args.props) ? args.props : [];
57242
- const limitRaw = args.limit;
57243
- const limit = typeof limitRaw === "number" && Number.isFinite(limitRaw) && limitRaw > 0 ? Math.floor(limitRaw) : 10;
57244
- try {
57245
- const matches = await findSimilarFunctions2(
57246
- {
57247
- name: typeof args.name === "string" ? args.name : void 0,
57248
- hooks,
57249
- props,
57250
- limit,
57251
- workspaceDir: ctx.cwd
57252
- },
57253
- { cwd: ctx.cwd }
57254
- );
57255
- return {
57256
- content: [
57257
- {
57258
- type: "text",
57259
- text: JSON.stringify(
57260
- {
57261
- hint: "Each match is ranked by Jaccard similarity over (hooks \u222A props \u222A params). similarity=1 means the matched signature has an identical feature set. Agents should prefer the top match instead of writing a new implementation.",
57262
- count: matches.length,
57263
- matches: matches.map((m) => ({
57264
- name: m.signature.name,
57265
- file: m.signature.fileRel,
57266
- line: m.signature.line,
57267
- similarity: Number(m.similarity.toFixed(3)),
57268
- fingerprint: m.fingerprint,
57269
- hooks: m.signature.hooks,
57270
- props: m.signature.props,
57271
- params: m.signature.params
57272
- }))
57273
- },
57274
- null,
57275
- 2
57276
- )
57277
- }
57278
- ]
57279
- };
57280
- } catch (err) {
57281
- return toolError(err instanceof Error ? err.message : String(err));
57282
- }
57283
- }
57284
- async function handleToolCall(toolName, args, ctx) {
57285
- const deprecation = getDeprecation(toolName);
57286
- const deprecationNotice = deprecation ? {
57287
- tool: toolName,
57288
- replacedBy: deprecation.replacedBy,
57289
- removedIn: deprecation.removedIn ?? "next major",
57290
- reason: deprecation.reason
57291
- } : void 0;
57292
- switch (toolName) {
57293
- case "slop_scan_file":
57294
- return runScanFile(args, ctx);
57295
- case "slop_explain_rule":
57296
- return explainRule2(args, ctx);
57297
- case "slop_list_rules":
57298
- return listRules(args, ctx);
57299
- case "slop_suggest":
57300
- return runSuggest(args, ctx);
57301
- case "slop_suggest_with_structure":
57302
- return runSuggestWithStructure(args, ctx);
57303
- // v0.39.0: removed 3 deprecated tools (slop_governance,
57304
- // slop_architecture_score, slop_business_logic_score) that
57305
- // were marked for removal in v0.13.0 but never removed.
57306
- // Their runner functions (runGovernance, runArchitectureScore,
57307
- // runBusinessLogicScore) are kept in the file for now
57308
- // (marked @deprecated) to keep the diff small; they can be
57309
- // deleted in a follow-up. New clients will never see these
57310
- // tools listed in the MCP tools/list response.
57311
- case "slop_check_constitution":
57312
- return runCheckConstitution(args, ctx);
57313
- case "slop_find_similar":
57314
- return runFindSimilar(args, ctx);
57315
- default:
57316
- return toolError("Unknown tool: " + toolName);
57317
- }
57318
- }
57319
- function canonicalToolNames() {
57320
- return TOOL_DEFINITIONS.filter((t) => !t.deprecated).map((t) => t.name);
57321
- }
57322
- function getDeprecation(toolName) {
57323
- return TOOL_DEFINITIONS.find((t) => t.name === toolName)?.deprecated;
57324
- }
57325
- var import_node_fs32, import_node_path40, TOOL_DEFINITIONS;
57326
- var init_tools = __esm({
57327
- "src/mcp/tools.ts"() {
57328
- "use strict";
57329
- import_node_fs32 = require("fs");
57330
- import_node_path40 = require("path");
57331
- init_worker();
57332
- init_patterns();
57333
- init_architecture_score();
57334
- init_business_logic();
57335
- init_slop_suggest_structure();
57336
- TOOL_DEFINITIONS = [
57337
- {
57338
- name: "slop_scan_file",
57339
- description: "Scan a single TypeScript/JavaScript file for AI-generated frontend slop. Returns issues (ruleId, severity, line, column, message, advice) and the file-level Slop Index.",
57340
- inputSchema: {
57341
- type: "object",
57342
- properties: {
57343
- path: { type: "string", description: "Absolute or cwd-relative path to the source file." },
57344
- framework: {
57345
- type: "string",
57346
- enum: ["react", "vue", "svelte", "astro", "html"],
57347
- description: "Framework multiplier to apply. Defaults to the configured framework."
57348
- }
57349
- },
57350
- required: ["path"]
57351
- }
57352
- },
57353
- {
57354
- name: "slop_explain_rule",
57355
- description: "Return metadata for a single rule (id, category, severity, aiSpecific) plus a rationale and the recommended fix. Use this before auto-applying --fix to understand what the rule catches.",
57356
- inputSchema: {
57357
- type: "object",
57358
- properties: {
57359
- ruleId: { type: "string", description: 'e.g. "visual/ai-default-palette".' }
57360
- },
57361
- required: ["ruleId"]
57362
- }
57363
- },
57364
- {
57365
- name: "slop_list_rules",
57366
- description: "List all registered rules with their category, severity, and aiSpecific flag. Optional category filter (visual | logic | wcag | security | perf | typo | layout | component | arch).",
57367
- inputSchema: {
57368
- type: "object",
57369
- properties: {
57370
- category: { type: "string", description: "Optional category filter." }
57371
- }
57372
- }
57373
- },
57374
- {
57375
- name: "slop_suggest",
57376
- description: "**Primary entry point for AI agents.** Returns the project's existing patterns (modals, buttons, api clients, state libs, data-fetching libs), the do-not-create list (forbidden imports + canonical patterns not to duplicate), top issues by rule, hot files by issue count, and the composite Repository Health score. Call this BEFORE writing new code so the agent reuses existing patterns instead of duplicating them.",
57377
- inputSchema: {
57378
- type: "object",
57379
- properties: {
57380
- maxFiles: {
57381
- type: "number",
57382
- description: "Cap on files scanned to keep the inventory cheap. Defaults to 200."
57383
- }
57384
- }
57385
- }
57386
- },
57387
- {
57388
- name: "slop_suggest_with_structure",
57389
- description: "Fast-path variant of `slop_suggest` that reads `.slopbrick/structure.md` from disk instead of re-scanning the codebase. Requires a prior `slopbrick scan` to have persisted the inventory (100\u20131000\xD7 latency win on the agent integration). If `structure.md` is missing, falls back to `slop_suggest` and annotates the response with `structureHint` so the caller knows to run `slopbrick scan` first.",
57390
- inputSchema: {
57391
- type: "object",
57392
- properties: {
57393
- maxFiles: {
57394
- type: "number",
57395
- description: "Cap on files scanned for the slow-path fallback. Defaults to 200."
57396
- }
57397
- }
57398
- }
57399
- },
57400
- {
57401
- // v0.39.0: removed 3 deprecated tools (slop_governance,
57402
- // slop_architecture_score, slop_business_logic_score) that
57403
- // were marked for removal in v0.13.0 but never removed.
57404
- // They were strict subsets of slop_suggest; users should
57405
- // call slop_suggest and read repositoryHealth /
57406
- // architectureConsistency / businessLogicCoherence.
57407
- name: "slop_check_constitution",
57408
- description: "Check a single file against the project's declared constitution (stateManagement, dataFetching, uiLibrary, forms, styling, routing, plus a forbidden deny-list in slopbrick.config.mjs). Returns a list of imports that violate declared values or hit the deny-list. Use this on a newly-written or modified file before suggesting a PR.",
57409
- inputSchema: {
57410
- type: "object",
57411
- properties: {
57412
- path: { type: "string", description: "Absolute or cwd-relative path to the source file." }
57413
- },
57414
- required: ["path"]
57415
- }
57416
- },
57417
- {
57418
- // v0.10.1: find_similar_function. The GIR (Give-Implementation-
57419
- // Reference) primitive for slop_suggest. Given a function signature
57420
- // (name + hooks + props), find the most similar existing
57421
- // implementations across the codebase. Uses AST fingerprints
57422
- // (sha256 over sorted hooks ∪ props ∪ params) + Jaccard similarity
57423
- // — no LLM, no embeddings, deterministic. Foundation for StackPick.
57424
- name: "slop_find_similar",
57425
- description: "Find the most similar existing function/component implementations across the codebase, ranked by Jaccard similarity over the union of (hooks \u222A props \u222A params). Use this BEFORE writing new code so the agent reuses an existing pattern instead of inventing a new one. Returns top-k matches with name, file, line, fingerprint, and similarity score in [0, 1].",
57426
- inputSchema: {
57427
- type: "object",
57428
- properties: {
57429
- name: {
57430
- type: "string",
57431
- description: "Function/component name to match. Omit to match by hooks+props only."
57432
- },
57433
- hooks: {
57434
- type: "array",
57435
- items: { type: "string" },
57436
- description: 'React hooks used by the target signature, e.g. ["useState", "useEffect"].'
57437
- },
57438
- props: {
57439
- type: "array",
57440
- items: { type: "string" },
57441
- description: 'Component props for the target signature, e.g. ["variant", "size", "children"].'
57442
- },
57443
- limit: {
57444
- type: "number",
57445
- description: "Top-k results to return. Default 10. Capped at 50."
57446
- }
57447
- }
57448
- }
57449
- }
57450
- ];
57451
- }
57452
- });
57453
-
57454
57134
  // src/index.ts
57455
57135
  var src_exports = {};
57456
57136
  __export(src_exports, {
@@ -57538,7 +57218,7 @@ init_dist2();
57538
57218
 
57539
57219
  // src/cli/program.ts
57540
57220
  var import_node_path72 = require("path");
57541
- var import_commander2 = require("commander");
57221
+ var import_commander3 = require("commander");
57542
57222
 
57543
57223
  // src/cli/options.ts
57544
57224
  var import_commander = require("commander");
@@ -62195,7 +61875,374 @@ init_logger();
62195
61875
  init_builtins();
62196
61876
  init_config2();
62197
61877
  init_header();
62198
- 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
62199
62246
  var SERVER_INFO = {
62200
62247
  name: "slopbrick",
62201
62248
  // v0.39.0: use the VERSION constant (same source as the CLI)
@@ -63635,6 +63682,7 @@ var import_node_fs38 = require("fs");
63635
63682
  var import_node_path52 = require("path");
63636
63683
  init_discover();
63637
63684
  init_patterns();
63685
+ init_telemetry();
63638
63686
  async function runDrift(cwd, config, options = {}) {
63639
63687
  const maxFiles = options.maxFiles ?? 1e3;
63640
63688
  const allFiles = await discoverFiles(cwd, config);
@@ -63742,26 +63790,217 @@ function formatDrift(result, opts = {}) {
63742
63790
  function driftExitCode(result) {
63743
63791
  return result.totalViolations > 0 ? 1 : 0;
63744
63792
  }
63793
+ async function runDriftOverTime(cwd, config, options) {
63794
+ const payloads = readTelemetry(cwd);
63795
+ const sortedAsc = [...payloads].sort((a, b) => a.timestamp.localeCompare(b.timestamp));
63796
+ const withInventory = sortedAsc.filter((p) => p.inventory !== void 0);
63797
+ const baselineSource = options.since === "baseline" ? "baseline" : "since";
63798
+ let baseline;
63799
+ if (baselineSource === "baseline") {
63800
+ baseline = withInventory[0];
63801
+ } else {
63802
+ baseline = withInventory.find((p) => p.timestamp >= options.since);
63803
+ }
63804
+ const current = withInventory.at(-1);
63805
+ if (!baseline || !current) {
63806
+ return {
63807
+ scannedFiles: 0,
63808
+ filesWithViolations: 0,
63809
+ totalViolations: 0,
63810
+ byCategory: {},
63811
+ byFile: [],
63812
+ conventionSource: deriveSource(config.constitution),
63813
+ constitution: config.constitution,
63814
+ introduced: [],
63815
+ removed: [],
63816
+ introducedUndeclared: [],
63817
+ snapshotsConsidered: withInventory.length,
63818
+ driftScore: 0,
63819
+ baselineAt: baseline?.timestamp ?? "",
63820
+ currentAt: current?.timestamp ?? "",
63821
+ baselineSource
63822
+ };
63823
+ }
63824
+ const baselineNames = flattenPatternNames(baseline.inventory.patternNames);
63825
+ const currentNames = flattenPatternNames(current.inventory.patternNames);
63826
+ const baselineSet = new Set(baselineNames.map((p) => `${p.category}\0${p.name}`));
63827
+ const currentSet = new Set(currentNames.map((p) => `${p.category}\0${p.name}`));
63828
+ const introduced = [];
63829
+ const removed = [];
63830
+ for (const p of currentNames) {
63831
+ const key = `${p.category}\0${p.name}`;
63832
+ if (!baselineSet.has(key)) introduced.push(p);
63833
+ }
63834
+ for (const p of baselineNames) {
63835
+ const key = `${p.category}\0${p.name}`;
63836
+ if (!currentSet.has(key)) removed.push(p);
63837
+ }
63838
+ introduced.sort((a, b) => a.category.localeCompare(b.category) || a.name.localeCompare(b.name));
63839
+ removed.sort((a, b) => a.category.localeCompare(b.category) || a.name.localeCompare(b.name));
63840
+ const declared = collectDeclaredNames(config.constitution);
63841
+ const introducedUndeclared = introduced.filter((p) => !declared.has(p.name.toLowerCase()));
63842
+ const baselineTotal = Math.max(baselineSet.size, 1);
63843
+ const driftScore = Math.min(
63844
+ 100,
63845
+ Math.round((introduced.length + removed.length) / baselineTotal * 100)
63846
+ );
63847
+ return {
63848
+ scannedFiles: current.inventory.scannedFiles,
63849
+ filesWithViolations: 0,
63850
+ totalViolations: 0,
63851
+ byCategory: {},
63852
+ byFile: [],
63853
+ conventionSource: deriveSource(config.constitution),
63854
+ constitution: config.constitution,
63855
+ introduced,
63856
+ removed,
63857
+ introducedUndeclared,
63858
+ snapshotsConsidered: withInventory.length,
63859
+ driftScore,
63860
+ baselineAt: baseline.timestamp,
63861
+ currentAt: current.timestamp,
63862
+ baselineSource
63863
+ };
63864
+ }
63865
+ function flattenPatternNames(patternNames) {
63866
+ const out = [];
63867
+ for (const [category, names] of Object.entries(patternNames)) {
63868
+ for (const name of names) {
63869
+ out.push({ category, name });
63870
+ }
63871
+ }
63872
+ out.sort((a, b) => a.category.localeCompare(b.category) || a.name.localeCompare(b.name));
63873
+ return out;
63874
+ }
63875
+ function collectDeclaredNames(constitution) {
63876
+ const out = /* @__PURE__ */ new Set();
63877
+ if (!constitution) return out;
63878
+ const fields = ["stateManagement", "dataFetching", "uiLibrary", "forms", "styling", "routing"];
63879
+ for (const f of fields) {
63880
+ for (const v of constitution[f] ?? []) {
63881
+ out.add(v.toLowerCase());
63882
+ }
63883
+ }
63884
+ for (const list of Object.values(constitution.custom ?? {})) {
63885
+ for (const v of list) {
63886
+ out.add(v.toLowerCase());
63887
+ }
63888
+ }
63889
+ for (const v of constitution.forbidden ?? []) {
63890
+ out.add(v.toLowerCase());
63891
+ }
63892
+ return out;
63893
+ }
63894
+ function formatDriftOverTime(result) {
63895
+ const lines = [];
63896
+ lines.push("Temporal drift report");
63897
+ lines.push("");
63898
+ if (result.snapshotsConsidered === 0 || !result.baselineAt || !result.currentAt) {
63899
+ lines.push(" No historical telemetry found at .slopbrick/flywheel/scans.jsonl.");
63900
+ lines.push(" Run a few scans with `slopbrick scan` first; the temporal drift");
63901
+ lines.push(" detector needs \u2265 2 scan payloads to compute a baseline window.");
63902
+ return lines.join("\n");
63903
+ }
63904
+ const sinceLabel = result.baselineSource === "baseline" ? `baseline (oldest scan)` : `since ${result.baselineAt}`;
63905
+ lines.push(` Window: ${sinceLabel} \u2192 ${result.currentAt}`);
63906
+ lines.push(` Snapshots considered: ${result.snapshotsConsidered}`);
63907
+ lines.push(` Drift score: ${result.driftScore} / 100 (informational)`);
63908
+ lines.push("");
63909
+ lines.push(` Patterns introduced: ${result.introduced.length}`);
63910
+ for (const p of result.introduced) {
63911
+ lines.push(` + ${p.category}/${p.name}`);
63912
+ }
63913
+ if (result.introducedUndeclared.length > 0) {
63914
+ lines.push("");
63915
+ lines.push(
63916
+ ` Patterns introduced but not in declared constitution: ${result.introducedUndeclared.length}`
63917
+ );
63918
+ for (const p of result.introducedUndeclared) {
63919
+ lines.push(` ! ${p.category}/${p.name}`);
63920
+ }
63921
+ }
63922
+ lines.push("");
63923
+ lines.push(` Patterns removed: ${result.removed.length}`);
63924
+ for (const p of result.removed) {
63925
+ lines.push(` - ${p.category}/${p.name}`);
63926
+ }
63927
+ lines.push("");
63928
+ if (result.introducedUndeclared.length > 0) {
63929
+ lines.push(
63930
+ " Tip: add undeclared patterns to slopbrick.config.mjs#constitution to"
63931
+ );
63932
+ lines.push(" promote them into the declared set on the next scan.");
63933
+ } else if (result.introduced.length === 0 && result.removed.length === 0) {
63934
+ lines.push(" \u2713 No pattern churn since the baseline window.");
63935
+ }
63936
+ return lines.join("\n");
63937
+ }
63938
+ function driftOverTimeExitCode(result) {
63939
+ return result.introducedUndeclared.length > 0 ? 1 : 0;
63940
+ }
63941
+
63942
+ // src/cli/commands/drift.ts
63943
+ init_load();
63944
+
63945
+ // src/cli/commands/_shared.ts
63946
+ var import_commander2 = require("commander");
63947
+ init_logger();
63948
+ function setExitOverride(program) {
63949
+ program.exitOverride();
63950
+ }
63951
+ async function dispatch(program, runFn) {
63952
+ try {
63953
+ await runFn();
63954
+ } catch (err) {
63955
+ if (err instanceof import_commander2.CommanderError) {
63956
+ if (err.code !== "commander.helpDisplayed" && err.code !== "commander.help") {
63957
+ logger.error(err.message);
63958
+ }
63959
+ process.exit(err.exitCode);
63960
+ return;
63961
+ }
63962
+ throw err;
63963
+ }
63964
+ }
63965
+ function withExitCode(result, compute, message) {
63966
+ const code = compute(result);
63967
+ if (code === 0) return;
63968
+ throw new import_commander2.CommanderError(code, "slopbrick.exit", message);
63969
+ }
63745
63970
 
63746
63971
  // src/cli/commands/drift.ts
63747
63972
  function registerDrift(program) {
63748
63973
  program.command("drift").description(
63749
63974
  "detect imports that violate declared constitution (state, data-fetching, UI, forms, styling, routing) or import forbidden packages"
63750
- ).option("--format <pretty|json>", "output format", "pretty").option("--max-files <n>", "cap on files scanned", parseCount, 1e3).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(
63751
63979
  async (cmdOptions, command) => {
63752
- try {
63753
- const options = command.optsWithGlobals();
63754
- const rawFormat = options.format ?? cmdOptions.format ?? "pretty";
63755
- const format = rawFormat === "json" || rawFormat === "pretty" ? rawFormat : "pretty";
63756
- const cwd = (0, import_node_path53.resolve)(options.workspace ?? process.cwd());
63757
- const { config } = await runScan({ ...options, workspace: cwd });
63758
- const result = await runDrift(cwd, config, { maxFiles: cmdOptions.maxFiles });
63759
- logger.info(formatDrift(result, { json: format === "json" }));
63760
- process.exit(driftExitCode(result));
63761
- } catch (err) {
63762
- logger.error(err instanceof Error ? err.message : String(err));
63763
- 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;
63764
63999
  }
64000
+ const { config } = await runScan({ ...options, workspace: cwd });
64001
+ const result = await runDrift(cwd, config, { maxFiles: cmdOptions.maxFiles });
64002
+ logger.info(formatDrift(result, { json: format === "json" }));
64003
+ withExitCode(result, driftExitCode, `drift: ${result.totalViolations} violations`);
63765
64004
  }
63766
64005
  );
63767
64006
  }
@@ -66420,8 +66659,9 @@ process.on("uncaughtException", (err) => {
66420
66659
  });
66421
66660
  async function runCli({ start }) {
66422
66661
  try {
66423
- const program = new 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)");
66424
66663
  program.helpInformation = () => formatGroupedHelp(program);
66664
+ setExitOverride(program);
66425
66665
  registerInit(program);
66426
66666
  registerInstall(program);
66427
66667
  registerUninstall(program);
@@ -66606,7 +66846,9 @@ async function runCli({ start }) {
66606
66846
  program.outputHelp();
66607
66847
  process.exit(0);
66608
66848
  }
66609
- await program.parseAsync(process.argv);
66849
+ await dispatch(program, async () => {
66850
+ await program.parseAsync(process.argv);
66851
+ });
66610
66852
  } catch (err) {
66611
66853
  if (err instanceof ConfigValidationError) {
66612
66854
  logger.error(err.message);