@wrongstack/plugins 0.281.3 → 0.282.1

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 (84) hide show
  1. package/README.md +30 -4
  2. package/dist/accessibility-auditor.d.ts +40 -0
  3. package/dist/accessibility-auditor.js +411 -0
  4. package/dist/agent-handoff.d.ts +37 -0
  5. package/dist/agent-handoff.js +298 -0
  6. package/dist/api-compatibility-gate.d.ts +38 -0
  7. package/dist/api-compatibility-gate.js +357 -0
  8. package/dist/auto-escalate.d.ts +1 -1
  9. package/dist/auto-i18n-extractor.d.ts +36 -0
  10. package/dist/auto-i18n-extractor.js +335 -0
  11. package/dist/branch-guard.d.ts +6 -5
  12. package/dist/branch-guard.js +54 -4
  13. package/dist/checkpoint.js +18 -0
  14. package/dist/code-metrics.d.ts +31 -0
  15. package/dist/code-metrics.js +338 -0
  16. package/dist/commit-validator.js +67 -12
  17. package/dist/context-pins.js +4 -4
  18. package/dist/cost-tracker.d.ts +1 -1
  19. package/dist/cost-tracker.js +58 -19
  20. package/dist/cron.js +18 -8
  21. package/dist/dead-code-detector.d.ts +34 -0
  22. package/dist/dead-code-detector.js +354 -0
  23. package/dist/dep-guard.js +47 -6
  24. package/dist/dependency-vulnerability-gate.d.ts +35 -0
  25. package/dist/dependency-vulnerability-gate.js +308 -0
  26. package/dist/diff-summary.js +97 -10
  27. package/dist/doc-sync-guard.d.ts +33 -0
  28. package/dist/doc-sync-guard.js +223 -0
  29. package/dist/duplicate-code-detector.d.ts +33 -0
  30. package/dist/duplicate-code-detector.js +384 -0
  31. package/dist/feature-flag-tracker.d.ts +38 -0
  32. package/dist/feature-flag-tracker.js +316 -0
  33. package/dist/file-watcher.js +85 -36
  34. package/dist/format-on-save.js +99 -18
  35. package/dist/import-organizer.js +73 -14
  36. package/dist/index.d.ts +27 -0
  37. package/dist/index.js +11205 -2106
  38. package/dist/interface-contract-guard.d.ts +37 -0
  39. package/dist/interface-contract-guard.js +302 -0
  40. package/dist/knowledge-graph.d.ts +45 -0
  41. package/dist/knowledge-graph.js +325 -0
  42. package/dist/license-audit-gate.d.ts +34 -0
  43. package/dist/license-audit-gate.js +260 -0
  44. package/dist/llm-cache.js +5 -0
  45. package/dist/loop-breaker.d.ts +0 -38
  46. package/dist/loop-breaker.js +209 -8
  47. package/dist/migration-planner.d.ts +30 -0
  48. package/dist/migration-planner.js +349 -0
  49. package/dist/model-router.js +5 -0
  50. package/dist/performance-regression-gate.d.ts +33 -0
  51. package/dist/performance-regression-gate.js +315 -0
  52. package/dist/plugin-stack-observer.d.ts +35 -0
  53. package/dist/plugin-stack-observer.js +138 -0
  54. package/dist/pr-drafter.d.ts +35 -0
  55. package/dist/pr-drafter.js +334 -0
  56. package/dist/prompt-firewall.js +5 -0
  57. package/dist/refactor-suggester.d.ts +38 -0
  58. package/dist/refactor-suggester.js +382 -0
  59. package/dist/release-notes-generator.d.ts +27 -0
  60. package/dist/release-notes-generator.js +209 -0
  61. package/dist/schema-evolution-guard.d.ts +42 -0
  62. package/dist/schema-evolution-guard.js +319 -0
  63. package/dist/security-hotspot-scanner.d.ts +30 -0
  64. package/dist/security-hotspot-scanner.js +402 -0
  65. package/dist/semantic-search-indexer.d.ts +38 -0
  66. package/dist/semantic-search-indexer.js +436 -0
  67. package/dist/shell-check.js +38 -3
  68. package/dist/smart-rename.d.ts +26 -0
  69. package/dist/smart-rename.js +170 -0
  70. package/dist/spec-linker.js +273 -133
  71. package/dist/test-coverage-gate.d.ts +37 -0
  72. package/dist/test-coverage-gate.js +263 -0
  73. package/dist/test-flake-detector.d.ts +27 -0
  74. package/dist/test-flake-detector.js +274 -0
  75. package/dist/test-generator.d.ts +32 -0
  76. package/dist/test-generator.js +243 -0
  77. package/dist/test-runner-gate.js +154 -22
  78. package/dist/todo-listener.d.ts +2 -2
  79. package/dist/todo-listener.js +5 -5
  80. package/dist/token-throttle.js +5 -0
  81. package/dist/type-gate.d.ts +37 -0
  82. package/dist/type-gate.js +311 -0
  83. package/package.json +112 -4
  84. package/LICENSE +0 -21
@@ -0,0 +1,382 @@
1
+ import { readFileSync, existsSync, statSync, readdirSync } from 'fs';
2
+ import { resolve, isAbsolute, relative, extname } from 'path';
3
+
4
+ // src/refactor-suggester/index.ts
5
+ var API_VERSION = "^0.1.10";
6
+ var state = {
7
+ scanCount: 0,
8
+ suggestionCount: 0,
9
+ hookInvocationCount: 0,
10
+ warningCount: 0,
11
+ errorCount: 0,
12
+ hookUnregister: null,
13
+ lastHookWarning: /* @__PURE__ */ new Map()
14
+ };
15
+ var DEFAULTS = {
16
+ enabled: false,
17
+ extensions: [".ts", ".tsx", ".js", ".jsx"],
18
+ maxSuggestions: 5,
19
+ rules: { longFunctionLines: 50, maxParams: 5, maxNesting: 3 }
20
+ };
21
+ function readRules(raw) {
22
+ if (!raw || typeof raw !== "object") return { ...DEFAULTS.rules };
23
+ const r = raw;
24
+ return {
25
+ longFunctionLines: typeof r["longFunctionLines"] === "number" && r["longFunctionLines"] >= 1 ? r["longFunctionLines"] : DEFAULTS.rules.longFunctionLines,
26
+ maxParams: typeof r["maxParams"] === "number" && r["maxParams"] >= 1 ? r["maxParams"] : DEFAULTS.rules.maxParams,
27
+ maxNesting: typeof r["maxNesting"] === "number" && r["maxNesting"] >= 1 ? r["maxNesting"] : DEFAULTS.rules.maxNesting
28
+ };
29
+ }
30
+ function readConfig(raw) {
31
+ if (!raw || typeof raw !== "object") return { ...DEFAULTS };
32
+ const r = raw;
33
+ return {
34
+ enabled: r["enabled"] === true,
35
+ extensions: Array.isArray(r["extensions"]) ? r["extensions"].filter((x) => typeof x === "string") : DEFAULTS.extensions,
36
+ maxSuggestions: typeof r["maxSuggestions"] === "number" && r["maxSuggestions"] >= 1 && r["maxSuggestions"] <= 500 ? r["maxSuggestions"] : DEFAULTS.maxSuggestions,
37
+ rules: readRules(r["rules"])
38
+ };
39
+ }
40
+ function withinProject(p) {
41
+ if (typeof p !== "string" || p.length === 0 || p.length > 4096) return false;
42
+ const root = process.cwd();
43
+ const resolved = isAbsolute(p) ? resolve(p) : resolve(root, p);
44
+ const rel = relative(root, resolved);
45
+ if (rel === "" || rel === ".") return true;
46
+ if (rel.startsWith("..")) return false;
47
+ if (isAbsolute(rel)) return false;
48
+ return true;
49
+ }
50
+ function normalizeExtensions(exts) {
51
+ return exts.map((e) => e.startsWith(".") ? e.toLowerCase() : `.${e.toLowerCase()}`);
52
+ }
53
+ function matchesExtension(p, exts) {
54
+ return exts.includes(extname(p).toLowerCase());
55
+ }
56
+ function collectSourceFiles(root, exts) {
57
+ const files = [];
58
+ if (!existsSync(root)) return files;
59
+ const s = statSync(root);
60
+ if (s.isFile()) {
61
+ if (matchesExtension(root, exts)) files.push(root);
62
+ return files;
63
+ }
64
+ if (!s.isDirectory()) return files;
65
+ function walk(dir) {
66
+ let entries;
67
+ try {
68
+ entries = readdirSync(dir);
69
+ } catch {
70
+ return;
71
+ }
72
+ for (const entry of entries) {
73
+ if (entry === "node_modules" || entry === "dist" || entry === ".git" || entry === "coverage") continue;
74
+ const full = resolve(dir, entry);
75
+ let st;
76
+ try {
77
+ st = statSync(full);
78
+ } catch {
79
+ continue;
80
+ }
81
+ if (st.isDirectory()) {
82
+ walk(full);
83
+ } else if (st.isFile() && matchesExtension(full, exts)) {
84
+ files.push(full);
85
+ }
86
+ }
87
+ }
88
+ walk(root);
89
+ return files;
90
+ }
91
+ function toPosix(p) {
92
+ return p.replace(/\\/g, "/");
93
+ }
94
+ function relativePath(p) {
95
+ return toPosix(relative(process.cwd(), p));
96
+ }
97
+ function leadingIndentLevel(line) {
98
+ const leading = line.match(/^(\s*)/)?.[1] ?? "";
99
+ const tabs = leading.split(" ").length - 1;
100
+ const spaces = leading.replace(/\t/g, "").length;
101
+ return tabs + Math.floor(spaces / 2);
102
+ }
103
+ function detectSmells(filePath, content, rules) {
104
+ const suggestions = [];
105
+ const lines = content.split(/\r?\n/);
106
+ const stripped = content.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, " ");
107
+ const functionLikeRe = /(?:export\s+)?(?:async\s+)?(?:function\s+)?([A-Za-z_$][A-Za-z0-9_$]*)\s*\(([^)]*)\)\s*\{/g;
108
+ let m;
109
+ functionLikeRe.lastIndex = 0;
110
+ while ((m = functionLikeRe.exec(stripped)) !== null) {
111
+ const name = m[1];
112
+ const paramsRaw = m[2];
113
+ const params = paramsRaw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
114
+ if (params.length > rules.maxParams) {
115
+ suggestions.push({
116
+ file: relativePath(filePath),
117
+ line: content.slice(0, m.index).split(/\r?\n/).length,
118
+ type: "many-parameters",
119
+ message: `${name} has ${params.length} parameters (limit ${rules.maxParams})`
120
+ });
121
+ }
122
+ const bodyStart = m.index + m[0].length;
123
+ let depth = 1;
124
+ let lineEnd = bodyStart;
125
+ for (let i = bodyStart; i < stripped.length && depth > 0; i++) {
126
+ const ch = stripped[i];
127
+ if (ch === "{") depth++;
128
+ if (ch === "}") depth--;
129
+ lineEnd = i;
130
+ }
131
+ const bodyLines = stripped.slice(bodyStart, lineEnd + 1).split(/\r?\n/).length;
132
+ if (bodyLines > rules.longFunctionLines) {
133
+ suggestions.push({
134
+ file: relativePath(filePath),
135
+ line: content.slice(0, m.index).split(/\r?\n/).length,
136
+ type: "long-function",
137
+ message: `${name} spans ~${bodyLines} lines (limit ${rules.longFunctionLines})`
138
+ });
139
+ }
140
+ }
141
+ for (let i = 0; i < lines.length; i++) {
142
+ const line = lines[i];
143
+ const lineNo = i + 1;
144
+ const strippedLine = line.replace(/\/\/.*$/, "");
145
+ const level = leadingIndentLevel(line);
146
+ if (level > rules.maxNesting) {
147
+ suggestions.push({
148
+ file: relativePath(filePath),
149
+ line: lineNo,
150
+ type: "deep-nesting",
151
+ message: `indentation level ${level} exceeds ${rules.maxNesting}`
152
+ });
153
+ }
154
+ if (/\bconsole\.(log|warn|error)\b/.test(strippedLine)) {
155
+ suggestions.push({
156
+ file: relativePath(filePath),
157
+ line: lineNo,
158
+ type: "console-log",
159
+ message: "console.log/warn/error usage detected"
160
+ });
161
+ }
162
+ }
163
+ const magicRe = /\b-?\d+(?:\.\d+)?\b/g;
164
+ const allowed = /* @__PURE__ */ new Set(["0", "1", "-1", "2"]);
165
+ magicRe.lastIndex = 0;
166
+ while ((m = magicRe.exec(content)) !== null) {
167
+ const match = m[0];
168
+ if (allowed.has(match)) continue;
169
+ const before = content[m.index - 1];
170
+ const after = content[m.index + match.length];
171
+ if (before === "[" && after === "]") continue;
172
+ suggestions.push({
173
+ file: relativePath(filePath),
174
+ line: content.slice(0, m.index).split(/\r?\n/).length,
175
+ type: "magic-number",
176
+ message: `magic number ${match} should be a named constant`
177
+ });
178
+ }
179
+ return suggestions;
180
+ }
181
+ function scanPath(rawPath, cfg) {
182
+ const root = process.cwd();
183
+ const resolved = isAbsolute(rawPath) ? resolve(rawPath) : resolve(root, rawPath);
184
+ const exts = normalizeExtensions(cfg.extensions);
185
+ const files = collectSourceFiles(resolved, exts);
186
+ const suggestions = [];
187
+ for (const p of files) {
188
+ try {
189
+ const content = readFileSync(p, "utf-8");
190
+ suggestions.push(...detectSmells(p, content, cfg.rules));
191
+ if (suggestions.length >= cfg.maxSuggestions) break;
192
+ } catch {
193
+ }
194
+ }
195
+ return { suggestions: suggestions.slice(0, cfg.maxSuggestions), scannedFiles: files.length };
196
+ }
197
+ var plugin = {
198
+ name: "refactor-suggester",
199
+ version: "0.1.0",
200
+ description: "Suggests refactoring opportunities using regex-based smell detection",
201
+ apiVersion: API_VERSION,
202
+ capabilities: { tools: true, hooks: true },
203
+ defaultConfig: { ...DEFAULTS },
204
+ configSchema: {
205
+ type: "object",
206
+ properties: {
207
+ enabled: { type: "boolean", default: false, description: "Master switch." },
208
+ extensions: {
209
+ type: "array",
210
+ items: { type: "string" },
211
+ default: [".ts", ".tsx", ".js", ".jsx"],
212
+ description: "File extensions to scan."
213
+ },
214
+ maxSuggestions: {
215
+ type: "number",
216
+ minimum: 1,
217
+ maximum: 500,
218
+ default: 20,
219
+ description: "Maximum suggestions returned per scan."
220
+ },
221
+ rules: {
222
+ type: "object",
223
+ properties: {
224
+ longFunctionLines: { type: "number", minimum: 1, default: 50 },
225
+ maxParams: { type: "number", minimum: 1, default: 5 },
226
+ maxNesting: { type: "number", minimum: 1, default: 3 }
227
+ }
228
+ }
229
+ }
230
+ },
231
+ setup(api) {
232
+ state.scanCount = 0;
233
+ state.suggestionCount = 0;
234
+ state.hookInvocationCount = 0;
235
+ state.warningCount = 0;
236
+ state.errorCount = 0;
237
+ state.lastHookWarning.clear();
238
+ if (state.hookUnregister) {
239
+ try {
240
+ state.hookUnregister();
241
+ } catch {
242
+ }
243
+ state.hookUnregister = null;
244
+ }
245
+ const cfg = readConfig(api.config.extensions?.["refactor-suggester"]);
246
+ const hook = (input) => {
247
+ if (!cfg.enabled) return;
248
+ if (input.toolResult?.isError) return;
249
+ const inp = input.toolInput ?? {};
250
+ const sourcePath = inp["path"];
251
+ if (!sourcePath || typeof sourcePath !== "string") return;
252
+ if (!withinProject(sourcePath)) return;
253
+ const exts = normalizeExtensions(cfg.extensions);
254
+ if (!matchesExtension(sourcePath, exts)) return;
255
+ state.hookInvocationCount += 1;
256
+ const now = Date.now();
257
+ const lastWarning = state.lastHookWarning.get(sourcePath);
258
+ if (lastWarning !== void 0 && now - lastWarning < 6e4) return;
259
+ const resolved = resolve(process.cwd(), sourcePath);
260
+ let content;
261
+ try {
262
+ content = readFileSync(resolved, "utf-8");
263
+ } catch {
264
+ state.errorCount += 1;
265
+ return;
266
+ }
267
+ const suggestions = detectSmells(resolved, content, cfg.rules);
268
+ if (suggestions.length === 0) return;
269
+ state.warningCount += suggestions.length;
270
+ state.lastHookWarning.set(sourcePath, now);
271
+ return {
272
+ additionalContext: `\u{1F527} refactor-suggester: ${suggestions.length} suggestion(s) for ${sourcePath}. Run suggest_refactors for the full list.`,
273
+ contextAs: "separate"
274
+ };
275
+ };
276
+ state.hookUnregister = api.registerHook("PostToolUse", "write|edit", hook);
277
+ api.tools.register({
278
+ name: "suggest_refactors",
279
+ description: "Scan source files for refactoring smells: long functions, deep nesting, many parameters, magic numbers, and console logging.",
280
+ inputSchema: {
281
+ type: "object",
282
+ properties: {
283
+ path: { type: "string", default: ".", description: "File or directory path to scan." }
284
+ }
285
+ },
286
+ permission: "auto",
287
+ category: "Diagnostics",
288
+ mutating: false,
289
+ async execute(input) {
290
+ if (!cfg.enabled) return { ok: false, error: "refactor-suggester is disabled" };
291
+ const rawPath = typeof input.path === "string" ? input.path : ".";
292
+ if (!withinProject(rawPath)) {
293
+ return { ok: false, error: "path is outside the project root" };
294
+ }
295
+ state.scanCount += 1;
296
+ let result;
297
+ try {
298
+ result = scanPath(rawPath, cfg);
299
+ } catch (err) {
300
+ state.errorCount += 1;
301
+ return { ok: false, error: String(err) };
302
+ }
303
+ state.suggestionCount += result.suggestions.length;
304
+ return {
305
+ ok: true,
306
+ path: relativePath(resolve(process.cwd(), rawPath)),
307
+ scannedFiles: result.scannedFiles,
308
+ suggestions: result.suggestions,
309
+ rules: cfg.rules
310
+ };
311
+ }
312
+ });
313
+ api.tools.register({
314
+ name: "refactor_status",
315
+ description: "Reports refactor-suggester state: config + counters.",
316
+ inputSchema: { type: "object", properties: {} },
317
+ permission: "auto",
318
+ category: "Diagnostics",
319
+ mutating: false,
320
+ async execute() {
321
+ return {
322
+ ok: true,
323
+ enabled: cfg.enabled,
324
+ extensions: cfg.extensions,
325
+ maxSuggestions: cfg.maxSuggestions,
326
+ rules: cfg.rules,
327
+ counters: {
328
+ scans: state.scanCount,
329
+ suggestions: state.suggestionCount,
330
+ hookInvocations: state.hookInvocationCount,
331
+ warnings: state.warningCount,
332
+ errors: state.errorCount
333
+ }
334
+ };
335
+ }
336
+ });
337
+ api.log.info("refactor-suggester plugin loaded", {
338
+ version: "0.1.0",
339
+ rules: cfg.rules,
340
+ extensions: cfg.extensions
341
+ });
342
+ },
343
+ teardown(api) {
344
+ if (state.hookUnregister) {
345
+ try {
346
+ state.hookUnregister();
347
+ } catch {
348
+ }
349
+ state.hookUnregister = null;
350
+ }
351
+ const final = {
352
+ scans: state.scanCount,
353
+ suggestions: state.suggestionCount,
354
+ hookInvocations: state.hookInvocationCount,
355
+ warnings: state.warningCount,
356
+ errors: state.errorCount
357
+ };
358
+ state.scanCount = 0;
359
+ state.suggestionCount = 0;
360
+ state.hookInvocationCount = 0;
361
+ state.warningCount = 0;
362
+ state.errorCount = 0;
363
+ state.lastHookWarning.clear();
364
+ api.log.info("refactor-suggester: teardown complete", { final });
365
+ },
366
+ async health() {
367
+ return {
368
+ ok: state.errorCount === 0,
369
+ message: state.errorCount ? `refactor-suggester: ${state.errorCount} error(s)` : `refactor-suggester: ${state.scanCount} scan(s), ${state.suggestionCount} suggestion(s)`,
370
+ counters: {
371
+ scans: state.scanCount,
372
+ suggestions: state.suggestionCount,
373
+ hookInvocations: state.hookInvocationCount,
374
+ warnings: state.warningCount,
375
+ errors: state.errorCount
376
+ }
377
+ };
378
+ }
379
+ };
380
+ var refactor_suggester_default = plugin;
381
+
382
+ export { refactor_suggester_default as default };
@@ -0,0 +1,27 @@
1
+ import { Plugin } from '@wrongstack/core';
2
+
3
+ /**
4
+ * release-notes-generator plugin — generates grouped release notes from git
5
+ * history using conventional-commit parsing.
6
+ *
7
+ * Tool registered:
8
+ * - generate_release_notes : Produce grouped notes between two refs.
9
+ *
10
+ * No hooks are registered.
11
+ *
12
+ * Config (`config.extensions['release-notes-generator']`):
13
+ *
14
+ * ```jsonc
15
+ * {
16
+ * "enabled": true,
17
+ * "includeScope": true,
18
+ * "defaultFrom": "latest-tag"
19
+ * }
20
+ * ```
21
+ *
22
+ * @public
23
+ */
24
+
25
+ declare const plugin: Plugin;
26
+
27
+ export { plugin as default };
@@ -0,0 +1,209 @@
1
+ import { execSync } from 'child_process';
2
+
3
+ // src/release-notes-generator/index.ts
4
+ var API_VERSION = "^0.1.10";
5
+ var state = {
6
+ generateCount: 0,
7
+ commitCount: 0,
8
+ errorCount: 0
9
+ };
10
+ var DEFAULTS = {
11
+ enabled: true,
12
+ includeScope: true,
13
+ defaultFrom: "latest-tag"
14
+ };
15
+ function readConfig(raw) {
16
+ if (!raw || typeof raw !== "object") return { ...DEFAULTS };
17
+ const r = raw;
18
+ return {
19
+ enabled: r["enabled"] !== false,
20
+ includeScope: r["includeScope"] !== false,
21
+ defaultFrom: typeof r["defaultFrom"] === "string" ? r["defaultFrom"] : DEFAULTS.defaultFrom
22
+ };
23
+ }
24
+ var CONVENTIONAL_TYPES = ["feat", "fix", "docs", "refactor", "perf", "test", "chore"];
25
+ function parseConventionalCommit(subject) {
26
+ const match = subject.match(/^([a-z]+)(?:\(([^)]+)\))?!?:\s*(.+)$/);
27
+ if (!match) {
28
+ return { type: "uncategorized", scope: null, description: subject };
29
+ }
30
+ const rawType = match[1];
31
+ const scope = match[2] ?? null;
32
+ const description = match[3];
33
+ const type = CONVENTIONAL_TYPES.includes(rawType) ? rawType : "uncategorized";
34
+ return { type, scope, description };
35
+ }
36
+ function formatCommit(c, includeScope) {
37
+ let line = `- ${c.hash.slice(0, 7)}`;
38
+ if (includeScope && c.scope) {
39
+ line += ` [${c.scope}]`;
40
+ }
41
+ line += ` ${c.description}`;
42
+ return line;
43
+ }
44
+ function resolveFromRef(defaultFrom, inputFrom) {
45
+ if (inputFrom) return inputFrom;
46
+ if (defaultFrom === "latest-tag") {
47
+ try {
48
+ return execSync("git describe --tags --abbrev=0", { encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] }).trim();
49
+ } catch {
50
+ return "";
51
+ }
52
+ }
53
+ return defaultFrom;
54
+ }
55
+ function getCommits(from, to) {
56
+ const range = from ? `${from}..${to}` : to;
57
+ const output = execSync(`git log --pretty=format:'%H%x09%s' ${range}`, {
58
+ encoding: "utf-8",
59
+ stdio: ["pipe", "pipe", "ignore"]
60
+ });
61
+ if (!output.trim()) return [];
62
+ const commits = [];
63
+ for (const line of output.split(/\r?\n/)) {
64
+ const tab = line.indexOf(" ");
65
+ if (tab === -1) continue;
66
+ const hash = line.slice(0, tab);
67
+ const subject = line.slice(tab + 1);
68
+ const parsed = parseConventionalCommit(subject);
69
+ commits.push({ hash, subject, ...parsed });
70
+ }
71
+ return commits;
72
+ }
73
+ function groupCommits(commits) {
74
+ const groups = {};
75
+ for (const c of commits) {
76
+ const key = c.type;
77
+ if (!groups[key]) groups[key] = [];
78
+ groups[key].push(c);
79
+ }
80
+ return groups;
81
+ }
82
+ function generateNotes(commits, includeScope) {
83
+ if (commits.length === 0) return "No commits found.";
84
+ const groups = groupCommits(commits);
85
+ const lines = [];
86
+ lines.push(`## Release Notes (${commits.length} commit${commits.length === 1 ? "" : "s"})`);
87
+ lines.push("");
88
+ const order = ["feat", "fix", "perf", "refactor", "docs", "test", "chore"];
89
+ for (const type of order) {
90
+ const list = groups[type];
91
+ if (!list || list.length === 0) continue;
92
+ lines.push(`### ${type}`);
93
+ for (const c of list) {
94
+ lines.push(formatCommit(c, includeScope));
95
+ }
96
+ lines.push("");
97
+ }
98
+ const uncategorized = groups["uncategorized"];
99
+ if (uncategorized && uncategorized.length > 0) {
100
+ lines.push("### Uncategorized");
101
+ for (const c of uncategorized) {
102
+ lines.push(formatCommit(c, includeScope));
103
+ }
104
+ lines.push("");
105
+ }
106
+ return lines.join("\n").trim();
107
+ }
108
+ var plugin = {
109
+ name: "release-notes-generator",
110
+ version: "0.1.0",
111
+ description: "Generates grouped release notes from conventional commits between two git refs",
112
+ apiVersion: API_VERSION,
113
+ capabilities: { tools: true },
114
+ defaultConfig: { ...DEFAULTS },
115
+ configSchema: {
116
+ type: "object",
117
+ properties: {
118
+ enabled: { type: "boolean", default: true, description: "Master switch." },
119
+ includeScope: {
120
+ type: "boolean",
121
+ default: true,
122
+ description: "Include commit scopes in the formatted notes."
123
+ },
124
+ defaultFrom: {
125
+ type: "string",
126
+ default: "latest-tag",
127
+ description: 'Default starting ref when `from` is omitted. Use "latest-tag" to discover the most recent tag.'
128
+ }
129
+ }
130
+ },
131
+ setup(api) {
132
+ state.generateCount = 0;
133
+ state.commitCount = 0;
134
+ state.errorCount = 0;
135
+ const cfg = readConfig(api.config.extensions?.["release-notes-generator"]);
136
+ api.tools.register({
137
+ name: "generate_release_notes",
138
+ description: "Generate release notes by grouping conventional commits between two git refs. Defaults to the latest tag..HEAD.",
139
+ inputSchema: {
140
+ type: "object",
141
+ properties: {
142
+ from: {
143
+ type: "string",
144
+ description: "Starting git ref (tag, commit, branch). Defaults to the configured defaultFrom."
145
+ },
146
+ to: {
147
+ type: "string",
148
+ default: "HEAD",
149
+ description: "Ending git ref."
150
+ }
151
+ }
152
+ },
153
+ permission: "auto",
154
+ category: "Development",
155
+ mutating: false,
156
+ async execute(input) {
157
+ if (!cfg.enabled) return { ok: false, error: "release-notes-generator is disabled" };
158
+ const toRef = typeof input.to === "string" ? input.to : "HEAD";
159
+ const fromRef = resolveFromRef(cfg.defaultFrom, input.from);
160
+ state.generateCount += 1;
161
+ let commits;
162
+ try {
163
+ commits = getCommits(fromRef, toRef);
164
+ } catch (err) {
165
+ state.errorCount += 1;
166
+ return { ok: false, error: String(err) };
167
+ }
168
+ state.commitCount += commits.length;
169
+ return {
170
+ ok: true,
171
+ from: fromRef || null,
172
+ to: toRef,
173
+ commitCount: commits.length,
174
+ notes: generateNotes(commits, cfg.includeScope)
175
+ };
176
+ }
177
+ });
178
+ api.log.info("release-notes-generator plugin loaded", {
179
+ version: "0.1.0",
180
+ defaultFrom: cfg.defaultFrom,
181
+ includeScope: cfg.includeScope
182
+ });
183
+ },
184
+ teardown(api) {
185
+ const final = {
186
+ generated: state.generateCount,
187
+ commits: state.commitCount,
188
+ errors: state.errorCount
189
+ };
190
+ state.generateCount = 0;
191
+ state.commitCount = 0;
192
+ state.errorCount = 0;
193
+ api.log.info("release-notes-generator: teardown complete", { final });
194
+ },
195
+ async health() {
196
+ return {
197
+ ok: state.errorCount === 0,
198
+ message: state.errorCount ? `release-notes-generator: ${state.errorCount} error(s)` : `release-notes-generator: ${state.generateCount} generation(s), ${state.commitCount} commit(s)`,
199
+ counters: {
200
+ generated: state.generateCount,
201
+ commits: state.commitCount,
202
+ errors: state.errorCount
203
+ }
204
+ };
205
+ }
206
+ };
207
+ var release_notes_generator_default = plugin;
208
+
209
+ export { release_notes_generator_default as default };
@@ -0,0 +1,42 @@
1
+ import { Plugin } from '@wrongstack/core';
2
+
3
+ /**
4
+ * schema-evolution-guard plugin — guards database/API schema changes.
5
+ *
6
+ * Registers a `PostToolUse` hook on `write|edit` that scans files matching
7
+ * schema-related patterns for destructive changes:
8
+ *
9
+ * - DROP TABLE / DROP COLUMN (Prisma migrations, raw SQL)
10
+ * - NOT NULL added without a default (SQL)
11
+ * - Required field without a default (Prisma, TypeScript schema files)
12
+ * - Required field added to an OpenAPI schema (YAML/JSON)
13
+ *
14
+ * When a destructive pattern is found the plugin either injects an
15
+ * `additionalContext` warning or blocks the tool, depending on `failSeverity`.
16
+ *
17
+ * Tools registered:
18
+ * - schema_evolution_status : counters + config snapshot.
19
+ *
20
+ * Config (`config.extensions['schema-evolution-guard']`):
21
+ *
22
+ * ```jsonc
23
+ * {
24
+ * "enabled": true,
25
+ * "failSeverity": "warn", // "warn" | "block"
26
+ * "maxFindings": 5,
27
+ * "filePatterns": [
28
+ * "*.prisma",
29
+ * "*migration*.sql",
30
+ * "*openapi*.yaml",
31
+ * "*openapi*.json",
32
+ * "*schema*.ts"
33
+ * ]
34
+ * }
35
+ * ```
36
+ *
37
+ * @public
38
+ */
39
+
40
+ declare const plugin: Plugin;
41
+
42
+ export { plugin as default };