@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,308 @@
1
+ import { execFileSync } from 'child_process';
2
+ import { existsSync } from 'fs';
3
+
4
+ // src/dependency-vulnerability-gate/index.ts
5
+ var API_VERSION = "^0.1.10";
6
+ var state = {
7
+ invocations: 0,
8
+ installsSeen: 0,
9
+ auditsRun: 0,
10
+ blocks: 0,
11
+ warns: 0,
12
+ errors: 0,
13
+ lastResult: null,
14
+ hookUnregister: null
15
+ };
16
+ var DEFAULTS = {
17
+ enabled: true,
18
+ severityThreshold: "high",
19
+ block: true,
20
+ timeoutMs: 12e4
21
+ };
22
+ var SEVERITY_RANK = {
23
+ info: 0,
24
+ low: 1,
25
+ moderate: 2,
26
+ high: 3,
27
+ critical: 4
28
+ };
29
+ function readConfig(raw) {
30
+ if (!raw || typeof raw !== "object") return { ...DEFAULTS };
31
+ const r = raw;
32
+ const threshold = typeof r["severityThreshold"] === "string" && ["low", "moderate", "high", "critical"].includes(r["severityThreshold"]) ? r["severityThreshold"] : DEFAULTS.severityThreshold;
33
+ return {
34
+ enabled: r["enabled"] !== false,
35
+ severityThreshold: threshold,
36
+ block: r["block"] !== false,
37
+ timeoutMs: typeof r["timeoutMs"] === "number" && r["timeoutMs"] > 0 ? r["timeoutMs"] : DEFAULTS.timeoutMs
38
+ };
39
+ }
40
+ function collectSeverities(out, severity) {
41
+ const key = severity.toLowerCase();
42
+ out.counts[key] = (out.counts[key] ?? 0) + 1;
43
+ out.total += 1;
44
+ if (SEVERITY_RANK[key] != null) {
45
+ const currentMax = out.maxSeverity;
46
+ if (currentMax === null || SEVERITY_RANK[key] > (SEVERITY_RANK[currentMax] ?? 0)) {
47
+ out.maxSeverity = key;
48
+ }
49
+ }
50
+ }
51
+ function parseAuditJson(jsonString) {
52
+ let parsed;
53
+ try {
54
+ parsed = JSON.parse(jsonString);
55
+ } catch {
56
+ return null;
57
+ }
58
+ const out = { maxSeverity: null, counts: {}, total: 0 };
59
+ if (parsed && typeof parsed === "object") {
60
+ const vulnerabilities = parsed["vulnerabilities"];
61
+ if (vulnerabilities && typeof vulnerabilities === "object" && !Array.isArray(vulnerabilities)) {
62
+ for (const entry of Object.values(vulnerabilities)) {
63
+ if (entry && typeof entry === "object") {
64
+ const severity = entry["severity"];
65
+ if (typeof severity === "string") {
66
+ collectSeverities(out, severity);
67
+ }
68
+ }
69
+ }
70
+ }
71
+ if (out.total === 0) {
72
+ const metadata = parsed["metadata"];
73
+ const metaVulns = metadata && typeof metadata === "object" ? metadata["vulnerabilities"] : void 0;
74
+ if (metaVulns && typeof metaVulns === "object") {
75
+ for (const [key, value] of Object.entries(metaVulns)) {
76
+ const count = typeof value === "number" ? value : 0;
77
+ if (count > 0 && SEVERITY_RANK[key] != null) {
78
+ out.counts[key] = (out.counts[key] ?? 0) + count;
79
+ out.total += count;
80
+ const currentMax = out.maxSeverity;
81
+ if (currentMax === null || SEVERITY_RANK[key] > (SEVERITY_RANK[currentMax] ?? 0)) {
82
+ out.maxSeverity = key;
83
+ }
84
+ }
85
+ }
86
+ }
87
+ }
88
+ }
89
+ if (Array.isArray(parsed)) {
90
+ for (const entry of parsed) {
91
+ if (entry && typeof entry === "object") {
92
+ const severity = entry["severity"];
93
+ if (typeof severity === "string") {
94
+ collectSeverities(out, severity);
95
+ }
96
+ }
97
+ }
98
+ }
99
+ return out;
100
+ }
101
+ function exceedsThreshold(report, threshold) {
102
+ if (report.maxSeverity === null) return false;
103
+ const thresholdRank = SEVERITY_RANK[threshold];
104
+ const maxRank = SEVERITY_RANK[report.maxSeverity];
105
+ if (thresholdRank == null || maxRank == null) return false;
106
+ return maxRank >= thresholdRank;
107
+ }
108
+ var INSTALL_RE = /\b(?:npm\s+(?:install|i)|pnpm\s+add|yarn\s+add)\b/i;
109
+ function isInstallCommand(input) {
110
+ if (input.toolName === "install") return true;
111
+ if (input.toolName !== "bash" && input.toolName !== "exec") return false;
112
+ const ti = input.toolInput ?? {};
113
+ const command = typeof ti["command"] === "string" ? ti["command"] : "";
114
+ return INSTALL_RE.test(command);
115
+ }
116
+ function detectManager() {
117
+ return existsSync("pnpm-lock.yaml") ? "pnpm" : "npm";
118
+ }
119
+ function runAudit(cfg) {
120
+ const manager = detectManager();
121
+ const start = Date.now();
122
+ let stdout = "";
123
+ try {
124
+ stdout = execFileSync(manager, ["audit", "--json"], {
125
+ encoding: "utf-8",
126
+ timeout: cfg.timeoutMs,
127
+ cwd: process.cwd(),
128
+ stdio: ["pipe", "pipe", "pipe"],
129
+ shell: false
130
+ });
131
+ } catch (err) {
132
+ const e = err;
133
+ if (e.killed) return null;
134
+ stdout = e.stdout ?? "";
135
+ }
136
+ const report = parseAuditJson(stdout);
137
+ if (!report) return null;
138
+ return { manager, report, durationMs: Date.now() - start };
139
+ }
140
+ var plugin = {
141
+ name: "dependency-vulnerability-gate",
142
+ version: "0.1.0",
143
+ description: "PostToolUse hook that runs npm/pnpm audit after dependency installs and blocks or warns on vulnerabilities above a severity threshold",
144
+ apiVersion: API_VERSION,
145
+ capabilities: { tools: true, hooks: true },
146
+ defaultConfig: { ...DEFAULTS },
147
+ configSchema: {
148
+ type: "object",
149
+ properties: {
150
+ enabled: {
151
+ type: "boolean",
152
+ default: true,
153
+ description: "Master switch."
154
+ },
155
+ severityThreshold: {
156
+ type: "string",
157
+ enum: ["low", "moderate", "high", "critical"],
158
+ default: "high",
159
+ description: "Minimum severity that triggers a block or warning."
160
+ },
161
+ block: {
162
+ type: "boolean",
163
+ default: true,
164
+ description: "true = block the install when threshold is exceeded; false = inject warning context."
165
+ },
166
+ timeoutMs: {
167
+ type: "number",
168
+ minimum: 5e3,
169
+ default: 12e4,
170
+ description: "Audit process timeout in milliseconds."
171
+ }
172
+ }
173
+ },
174
+ setup(api) {
175
+ state.invocations = 0;
176
+ state.installsSeen = 0;
177
+ state.auditsRun = 0;
178
+ state.blocks = 0;
179
+ state.warns = 0;
180
+ state.errors = 0;
181
+ state.lastResult = null;
182
+ if (state.hookUnregister) {
183
+ try {
184
+ state.hookUnregister();
185
+ } catch {
186
+ }
187
+ state.hookUnregister = null;
188
+ }
189
+ const cfg = readConfig(api.config.extensions?.["dependency-vulnerability-gate"]);
190
+ const hook = (input) => {
191
+ if (!cfg.enabled) return;
192
+ state.invocations += 1;
193
+ if (input.toolResult?.isError) return;
194
+ if (!isInstallCommand(input)) return;
195
+ state.installsSeen += 1;
196
+ api.metrics.counter("installs_seen");
197
+ const result = runAudit(cfg);
198
+ if (!result) {
199
+ state.errors += 1;
200
+ return;
201
+ }
202
+ state.auditsRun += 1;
203
+ state.lastResult = {
204
+ manager: result.manager,
205
+ exceeded: exceedsThreshold(result.report, cfg.severityThreshold),
206
+ maxSeverity: result.report.maxSeverity,
207
+ counts: result.report.counts,
208
+ durationMs: result.durationMs,
209
+ when: (/* @__PURE__ */ new Date()).toISOString()
210
+ };
211
+ if (!exceedsThreshold(result.report, cfg.severityThreshold)) {
212
+ return;
213
+ }
214
+ const countsText = Object.entries(result.report.counts).filter(([, count]) => count > 0).map(([sev, count]) => `${sev}: ${count}`).join(", ");
215
+ const message = `
216
+ \u26A0\uFE0F dependency-vulnerability-gate: ${result.manager} audit found vulnerabilities at or above the configured threshold (${cfg.severityThreshold}).
217
+ Max severity: ${result.report.maxSeverity}
218
+ Counts: ${countsText || "unknown"}
219
+ Review the audit output or adjust the dependency choice.`;
220
+ if (cfg.block) {
221
+ state.blocks += 1;
222
+ api.metrics.counter("blocks");
223
+ return {
224
+ decision: "block",
225
+ reason: message
226
+ };
227
+ }
228
+ state.warns += 1;
229
+ api.metrics.counter("warns");
230
+ return { additionalContext: message };
231
+ };
232
+ state.hookUnregister = api.registerHook("PostToolUse", "install|bash|exec", hook);
233
+ api.tools.register({
234
+ name: "dependency_audit_status",
235
+ description: "Reports dependency-vulnerability-gate state: threshold, block mode, and per-session counters.",
236
+ inputSchema: { type: "object", properties: {} },
237
+ permission: "auto",
238
+ category: "Diagnostics",
239
+ mutating: false,
240
+ async execute() {
241
+ return {
242
+ ok: true,
243
+ enabled: cfg.enabled,
244
+ severityThreshold: cfg.severityThreshold,
245
+ block: cfg.block,
246
+ timeoutMs: cfg.timeoutMs,
247
+ counters: {
248
+ invocations: state.invocations,
249
+ installsSeen: state.installsSeen,
250
+ auditsRun: state.auditsRun,
251
+ blocks: state.blocks,
252
+ warns: state.warns,
253
+ errors: state.errors
254
+ },
255
+ lastResult: state.lastResult
256
+ };
257
+ }
258
+ });
259
+ api.log.info("dependency-vulnerability-gate plugin loaded", {
260
+ version: "0.1.0",
261
+ severityThreshold: cfg.severityThreshold,
262
+ block: cfg.block
263
+ });
264
+ },
265
+ teardown(api) {
266
+ if (state.hookUnregister) {
267
+ try {
268
+ state.hookUnregister();
269
+ } catch {
270
+ }
271
+ state.hookUnregister = null;
272
+ }
273
+ const final = {
274
+ invocations: state.invocations,
275
+ installsSeen: state.installsSeen,
276
+ auditsRun: state.auditsRun,
277
+ blocks: state.blocks,
278
+ warns: state.warns,
279
+ errors: state.errors
280
+ };
281
+ state.invocations = 0;
282
+ state.installsSeen = 0;
283
+ state.auditsRun = 0;
284
+ state.blocks = 0;
285
+ state.warns = 0;
286
+ state.errors = 0;
287
+ state.lastResult = null;
288
+ api.log.info("dependency-vulnerability-gate: teardown complete", { final });
289
+ },
290
+ async health() {
291
+ return {
292
+ ok: true,
293
+ message: state.lastResult ? `dependency-vulnerability-gate: ${state.auditsRun} audit(s), last ${state.lastResult.exceeded ? "EXCEEDED" : "clean"} (${state.lastResult.maxSeverity})` : `dependency-vulnerability-gate: ${state.installsSeen} install(s) seen, ${state.auditsRun} audit(s)`,
294
+ counters: {
295
+ invocations: state.invocations,
296
+ installsSeen: state.installsSeen,
297
+ auditsRun: state.auditsRun,
298
+ blocks: state.blocks,
299
+ warns: state.warns,
300
+ errors: state.errors
301
+ },
302
+ lastResult: state.lastResult
303
+ };
304
+ }
305
+ };
306
+ var dependency_vulnerability_gate_default = plugin;
307
+
308
+ export { dependency_vulnerability_gate_default as default };
@@ -1,13 +1,28 @@
1
- import { execSync } from 'child_process';
1
+ import { execFileSync } from 'child_process';
2
+ import { isAbsolute, resolve, relative } from 'path';
2
3
 
3
4
  // src/diff-summary/index.ts
4
5
  var API_VERSION = "^0.1.10";
6
+ function withinProject(p) {
7
+ if (typeof p !== "string" || p.length === 0 || p.length > 4096) return false;
8
+ const root = process.cwd();
9
+ const resolved = isAbsolute(p) ? resolve(p) : resolve(root, p);
10
+ const rel = relative(root, resolved);
11
+ if (rel === "" || rel === ".") return true;
12
+ if (rel.startsWith("..")) return false;
13
+ if (isAbsolute(rel)) return false;
14
+ return true;
15
+ }
5
16
  var state = {
6
17
  invocationCount: 0,
7
18
  /** Times a diff was successfully generated and injected. */
8
19
  injectedCount: 0,
9
20
  /** Times git diff failed (not a repo, untracked, etc.). */
10
21
  fallbackCount: 0,
22
+ /** Times the per-path throttle fired (no git diff was spawned). */
23
+ throttledCount: 0,
24
+ /** Times the content hash matched the last injection (no git diff). */
25
+ duplicateContentCount: 0,
11
26
  /** Hook handle for teardown. */
12
27
  hookUnregister: null,
13
28
  /** Last diff summary — surfaced by health() + status tool. */
@@ -17,7 +32,9 @@ var DEFAULTS = {
17
32
  maxLines: 50,
18
33
  showStat: true,
19
34
  mode: "diff",
20
- includeContext: 3
35
+ includeContext: 3,
36
+ minIntervalMs: 1e3,
37
+ enableContentHashCache: true
21
38
  };
22
39
  function readConfig(raw) {
23
40
  if (!raw || typeof raw !== "object") return { ...DEFAULTS };
@@ -26,9 +43,20 @@ function readConfig(raw) {
26
43
  maxLines: typeof r["maxLines"] === "number" && r["maxLines"] > 0 ? r["maxLines"] : DEFAULTS.maxLines,
27
44
  showStat: r["showStat"] !== false,
28
45
  mode: r["mode"] === "stat" ? "stat" : r["mode"] === "off" ? "off" : "diff",
29
- includeContext: typeof r["includeContext"] === "number" && r["includeContext"] >= 0 ? r["includeContext"] : DEFAULTS.includeContext
46
+ includeContext: typeof r["includeContext"] === "number" && r["includeContext"] >= 0 ? r["includeContext"] : DEFAULTS.includeContext,
47
+ minIntervalMs: typeof r["minIntervalMs"] === "number" && r["minIntervalMs"] >= 0 ? Math.floor(r["minIntervalMs"]) : DEFAULTS.minIntervalMs,
48
+ enableContentHashCache: r["enableContentHashCache"] !== false
30
49
  };
31
50
  }
51
+ function contentHash(s) {
52
+ const cap = Math.min(s.length, 65536);
53
+ let h = 5381;
54
+ for (let i = 0; i < cap; i++) {
55
+ h = (h << 5) + h + s.charCodeAt(i) | 0;
56
+ }
57
+ return h >>> 0;
58
+ }
59
+ var pathMemo = /* @__PURE__ */ new Map();
32
60
  function getGitDiff(filePath, contextLines, cwd) {
33
61
  const opts = {
34
62
  encoding: "utf-8",
@@ -38,7 +66,7 @@ function getGitDiff(filePath, contextLines, cwd) {
38
66
  };
39
67
  let isTracked = false;
40
68
  try {
41
- execSync(`git ls-files --error-unmatch "${filePath}"`, opts);
69
+ execFileSync("git", ["ls-files", "--error-unmatch", "--", filePath], opts);
42
70
  isTracked = true;
43
71
  } catch {
44
72
  isTracked = false;
@@ -47,10 +75,14 @@ function getGitDiff(filePath, contextLines, cwd) {
47
75
  let rawDiff;
48
76
  const contextFlag = `-U${contextLines}`;
49
77
  if (isTracked) {
50
- rawDiff = execSync(`git diff ${contextFlag} -- "${filePath}"`, opts);
78
+ rawDiff = execFileSync("git", ["diff", contextFlag, "--", filePath], opts);
51
79
  } else {
52
80
  try {
53
- rawDiff = execSync(`git diff --no-index ${contextFlag} /dev/null "${filePath}"`, opts);
81
+ rawDiff = execFileSync(
82
+ "git",
83
+ ["diff", "--no-index", contextFlag, "/dev/null", filePath],
84
+ opts
85
+ );
54
86
  } catch (err) {
55
87
  const e = err;
56
88
  rawDiff = e.stdout ?? "";
@@ -121,6 +153,17 @@ var plugin = {
121
153
  minimum: 0,
122
154
  default: 3,
123
155
  description: "Context lines around each change (git -U<N>). 0 = compact (no surrounding lines), 3 = git default, higher = more orientation."
156
+ },
157
+ minIntervalMs: {
158
+ type: "number",
159
+ minimum: 0,
160
+ default: 1e3,
161
+ description: "Per-path throttle: minimum interval (ms) between two diff-summary injections for the same file. Skips `git diff` when the model re-touches a file within the window. Set to 0 to disable."
162
+ },
163
+ enableContentHashCache: {
164
+ type: "boolean",
165
+ default: true,
166
+ description: 'When true, fingerprints the PostToolUse payload content and skips `git diff` when the hash matches the previously injected hash for the same path. Catches "edit retry" loops.'
124
167
  }
125
168
  }
126
169
  },
@@ -128,8 +171,11 @@ var plugin = {
128
171
  state.invocationCount = 0;
129
172
  state.injectedCount = 0;
130
173
  state.fallbackCount = 0;
174
+ state.throttledCount = 0;
175
+ state.duplicateContentCount = 0;
131
176
  state.hookUnregister = null;
132
177
  state.lastSummary = null;
178
+ pathMemo.clear();
133
179
  const cfg = readConfig(api.config.extensions?.["diff-summary"]);
134
180
  const cwd = typeof process.cwd === "function" ? process.cwd() : void 0;
135
181
  const hook = (input) => {
@@ -140,6 +186,25 @@ var plugin = {
140
186
  const filePath = inp["path"];
141
187
  if (!filePath || typeof filePath !== "string") return;
142
188
  state.invocationCount += 1;
189
+ if (!withinProject(filePath)) {
190
+ state.fallbackCount += 1;
191
+ return;
192
+ }
193
+ const toolInputForHash = toolName === "edit" ? inp["new_string"] ?? "" : toolName === "write" ? inp["content"] ?? "" : "";
194
+ const now = Date.now();
195
+ const memo = pathMemo.get(filePath);
196
+ if (memo) {
197
+ if (cfg.minIntervalMs > 0 && now - memo.lastInjectedAt < cfg.minIntervalMs) {
198
+ state.throttledCount = (state.throttledCount ?? 0) + 1;
199
+ api.metrics.counter("throttled");
200
+ return;
201
+ }
202
+ if (cfg.enableContentHashCache && typeof toolInputForHash === "string" && contentHash(toolInputForHash) === memo.hash) {
203
+ state.duplicateContentCount = (state.duplicateContentCount ?? 0) + 1;
204
+ api.metrics.counter("duplicate_content");
205
+ return;
206
+ }
207
+ }
143
208
  const result = getGitDiff(filePath, cfg.includeContext, cwd);
144
209
  if (!result) {
145
210
  state.fallbackCount += 1;
@@ -156,6 +221,17 @@ var plugin = {
156
221
  removed: result.removed,
157
222
  when: (/* @__PURE__ */ new Date()).toISOString()
158
223
  };
224
+ if (cfg.enableContentHashCache && typeof toolInputForHash === "string") {
225
+ pathMemo.set(filePath, {
226
+ hash: contentHash(toolInputForHash),
227
+ lastInjectedAt: now
228
+ });
229
+ } else if (cfg.minIntervalMs > 0) {
230
+ pathMemo.set(filePath, {
231
+ hash: 0,
232
+ lastInjectedAt: now
233
+ });
234
+ }
159
235
  let summary;
160
236
  if (cfg.mode === "stat") {
161
237
  summary = buildStatSummary(filePath, result);
@@ -183,10 +259,14 @@ var plugin = {
183
259
  maxLines: cfg.maxLines,
184
260
  showStat: cfg.showStat,
185
261
  includeContext: cfg.includeContext,
262
+ minIntervalMs: cfg.minIntervalMs,
263
+ enableContentHashCache: cfg.enableContentHashCache,
186
264
  counters: {
187
265
  invocations: state.invocationCount,
188
266
  injected: state.injectedCount,
189
- fallbacks: state.fallbackCount
267
+ fallbacks: state.fallbackCount,
268
+ throttled: state.throttledCount,
269
+ duplicateContent: state.duplicateContentCount
190
270
  },
191
271
  lastSummary: state.lastSummary
192
272
  };
@@ -209,22 +289,29 @@ var plugin = {
209
289
  const final = {
210
290
  invocations: state.invocationCount,
211
291
  injected: state.injectedCount,
212
- fallbacks: state.fallbackCount
292
+ fallbacks: state.fallbackCount,
293
+ throttled: state.throttledCount,
294
+ duplicateContent: state.duplicateContentCount
213
295
  };
214
296
  state.invocationCount = 0;
215
297
  state.injectedCount = 0;
216
298
  state.fallbackCount = 0;
299
+ state.throttledCount = 0;
300
+ state.duplicateContentCount = 0;
217
301
  state.lastSummary = null;
302
+ pathMemo.clear();
218
303
  api.log.info("diff-summary: teardown complete", { final });
219
304
  },
220
305
  async health() {
221
306
  return {
222
307
  ok: true,
223
- message: state.lastSummary === null ? `diff-summary: ${state.invocationCount} invocation(s), ${state.injectedCount} injected` : `diff-summary: last ${state.lastSummary.tool} on ${state.lastSummary.path} (+${state.lastSummary.added} -${state.lastSummary.removed}) at ${state.lastSummary.when}`,
308
+ message: state.lastSummary === null ? `diff-summary: ${state.invocationCount} invocation(s), ${state.injectedCount} injected, ${state.throttledCount} throttled, ${state.duplicateContentCount} dup-content` : `diff-summary: last ${state.lastSummary.tool} on ${state.lastSummary.path} (+${state.lastSummary.added} -${state.lastSummary.removed}) at ${state.lastSummary.when}`,
224
309
  counters: {
225
310
  invocations: state.invocationCount,
226
311
  injected: state.injectedCount,
227
- fallbacks: state.fallbackCount
312
+ fallbacks: state.fallbackCount,
313
+ throttled: state.throttledCount,
314
+ duplicateContent: state.duplicateContentCount
228
315
  },
229
316
  lastSummary: state.lastSummary
230
317
  };
@@ -0,0 +1,33 @@
1
+ import { Plugin } from '@wrongstack/core';
2
+
3
+ /**
4
+ * doc-sync-guard plugin — warns when documentation is edited without
5
+ * referencing recently changed public source files.
6
+ *
7
+ * Tracks public source files changed by `write` or `edit`. When a README or
8
+ * docs file is written, checks whether the recently changed source files are
9
+ * mentioned and injects an `additionalContext` warning if any are missing.
10
+ *
11
+ * Tools registered:
12
+ * - doc_sync_status : Show tracked changed files and counters.
13
+ *
14
+ * Hooks registered:
15
+ * - PostToolUse with matcher `write|edit`.
16
+ *
17
+ * Config (`config.extensions['doc-sync-guard']`):
18
+ *
19
+ * ```jsonc
20
+ * {
21
+ * "enabled": true,
22
+ * "sourceExtensions": [".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"],
23
+ * "docNames": ["README.md", "README", "CONTRIBUTING.md", "CHANGELOG.md"],
24
+ * "maxTrackedFiles": 20
25
+ * }
26
+ * ```
27
+ *
28
+ * @public
29
+ */
30
+
31
+ declare const plugin: Plugin;
32
+
33
+ export { plugin as default };