@wrongstack/plugins 0.281.3 → 0.282.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/README.md +29 -3
- package/dist/accessibility-auditor.d.ts +40 -0
- package/dist/accessibility-auditor.js +411 -0
- package/dist/agent-handoff.d.ts +37 -0
- package/dist/agent-handoff.js +298 -0
- package/dist/api-compatibility-gate.d.ts +38 -0
- package/dist/api-compatibility-gate.js +357 -0
- package/dist/auto-i18n-extractor.d.ts +36 -0
- package/dist/auto-i18n-extractor.js +335 -0
- package/dist/checkpoint.js +18 -0
- package/dist/code-metrics.d.ts +31 -0
- package/dist/code-metrics.js +338 -0
- package/dist/commit-validator.js +67 -12
- package/dist/cost-tracker.js +58 -16
- package/dist/dead-code-detector.d.ts +34 -0
- package/dist/dead-code-detector.js +354 -0
- package/dist/dep-guard.js +47 -6
- package/dist/dependency-vulnerability-gate.d.ts +35 -0
- package/dist/dependency-vulnerability-gate.js +308 -0
- package/dist/diff-summary.js +97 -10
- package/dist/doc-sync-guard.d.ts +33 -0
- package/dist/doc-sync-guard.js +223 -0
- package/dist/duplicate-code-detector.d.ts +33 -0
- package/dist/duplicate-code-detector.js +384 -0
- package/dist/feature-flag-tracker.d.ts +38 -0
- package/dist/feature-flag-tracker.js +316 -0
- package/dist/file-watcher.js +85 -36
- package/dist/format-on-save.js +76 -10
- package/dist/import-organizer.js +73 -14
- package/dist/index.d.ts +27 -0
- package/dist/index.js +14067 -5054
- package/dist/interface-contract-guard.d.ts +37 -0
- package/dist/interface-contract-guard.js +302 -0
- package/dist/knowledge-graph.d.ts +45 -0
- package/dist/knowledge-graph.js +325 -0
- package/dist/license-audit-gate.d.ts +34 -0
- package/dist/license-audit-gate.js +260 -0
- package/dist/llm-cache.js +5 -0
- package/dist/loop-breaker.d.ts +0 -38
- package/dist/loop-breaker.js +209 -8
- package/dist/migration-planner.d.ts +30 -0
- package/dist/migration-planner.js +349 -0
- package/dist/model-router.js +5 -0
- package/dist/performance-regression-gate.d.ts +33 -0
- package/dist/performance-regression-gate.js +315 -0
- package/dist/plugin-stack-observer.d.ts +35 -0
- package/dist/plugin-stack-observer.js +125 -0
- package/dist/pr-drafter.d.ts +35 -0
- package/dist/pr-drafter.js +334 -0
- package/dist/prompt-firewall.js +5 -0
- package/dist/refactor-suggester.d.ts +38 -0
- package/dist/refactor-suggester.js +382 -0
- package/dist/release-notes-generator.d.ts +27 -0
- package/dist/release-notes-generator.js +209 -0
- package/dist/schema-evolution-guard.d.ts +42 -0
- package/dist/schema-evolution-guard.js +319 -0
- package/dist/security-hotspot-scanner.d.ts +30 -0
- package/dist/security-hotspot-scanner.js +402 -0
- package/dist/semantic-search-indexer.d.ts +38 -0
- package/dist/semantic-search-indexer.js +436 -0
- package/dist/shell-check.js +38 -3
- package/dist/smart-rename.d.ts +26 -0
- package/dist/smart-rename.js +170 -0
- package/dist/spec-linker.js +273 -133
- package/dist/test-coverage-gate.d.ts +37 -0
- package/dist/test-coverage-gate.js +263 -0
- package/dist/test-flake-detector.d.ts +27 -0
- package/dist/test-flake-detector.js +274 -0
- package/dist/test-generator.d.ts +32 -0
- package/dist/test-generator.js +243 -0
- package/dist/test-runner-gate.js +154 -22
- package/dist/todo-listener.d.ts +2 -2
- package/dist/todo-listener.js +5 -5
- package/dist/token-throttle.js +5 -0
- package/dist/type-gate.d.ts +37 -0
- package/dist/type-gate.js +311 -0
- package/package.json +112 -4
- 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 };
|
package/dist/diff-summary.js
CHANGED
|
@@ -1,13 +1,28 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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 =
|
|
78
|
+
rawDiff = execFileSync("git", ["diff", contextFlag, "--", filePath], opts);
|
|
51
79
|
} else {
|
|
52
80
|
try {
|
|
53
|
-
rawDiff =
|
|
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 };
|