@algosuite/vo-mcp 0.1.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 +153 -0
- package/bin/vo-mcp +36 -0
- package/dist/autostart-cli.js +167 -0
- package/dist/autostart-cli.js.map +7 -0
- package/dist/cli.js +5730 -0
- package/dist/cli.js.map +7 -0
- package/dist/index.js +4968 -0
- package/dist/index.js.map +7 -0
- package/dist/install-cli.js +603 -0
- package/dist/install-cli.js.map +7 -0
- package/dist/login-cli.js +382 -0
- package/dist/login-cli.js.map +7 -0
- package/package.json +50 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,4968 @@
|
|
|
1
|
+
import { createRequire as __cr } from 'module'; const require = __cr(import.meta.url);
|
|
2
|
+
|
|
3
|
+
// src/server.ts
|
|
4
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
5
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
6
|
+
import {
|
|
7
|
+
CallToolRequestSchema,
|
|
8
|
+
ListToolsRequestSchema
|
|
9
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
10
|
+
|
|
11
|
+
// src/tools/common.ts
|
|
12
|
+
import { createHash as createHash2, randomUUID } from "node:crypto";
|
|
13
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
14
|
+
import { dirname as dirname3, join as join4 } from "node:path";
|
|
15
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
16
|
+
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
|
|
17
|
+
|
|
18
|
+
// src/logging/events-writer.ts
|
|
19
|
+
import {
|
|
20
|
+
appendFileSync,
|
|
21
|
+
chmodSync,
|
|
22
|
+
mkdirSync,
|
|
23
|
+
readdirSync as readdirSync2,
|
|
24
|
+
readFileSync as readFileSync3,
|
|
25
|
+
statSync as statSync2,
|
|
26
|
+
unlinkSync,
|
|
27
|
+
writeFileSync
|
|
28
|
+
} from "node:fs";
|
|
29
|
+
import { homedir as homedir2 } from "node:os";
|
|
30
|
+
import { basename, dirname as dirname2, join as join3 } from "node:path";
|
|
31
|
+
import { gzipSync } from "node:zlib";
|
|
32
|
+
|
|
33
|
+
// ../vo-arch-defaults/src/schema/rule-v1.ts
|
|
34
|
+
import { z } from "zod";
|
|
35
|
+
var EvidenceMatcherKind = z.enum([
|
|
36
|
+
"regex",
|
|
37
|
+
"import-detector",
|
|
38
|
+
"package-json-field",
|
|
39
|
+
"file-size",
|
|
40
|
+
"ast-pattern",
|
|
41
|
+
"custom"
|
|
42
|
+
]);
|
|
43
|
+
var EvidenceMatcher = z.object({
|
|
44
|
+
kind: EvidenceMatcherKind,
|
|
45
|
+
pattern: z.string().optional(),
|
|
46
|
+
config: z.record(z.string(), z.unknown()).optional(),
|
|
47
|
+
description: z.string().min(1)
|
|
48
|
+
}).strict().refine(
|
|
49
|
+
(m) => m.kind !== "regex" || typeof m.pattern === "string" && m.pattern.length > 0,
|
|
50
|
+
{ message: "regex matcher requires non-empty pattern" }
|
|
51
|
+
);
|
|
52
|
+
var ChangeType = z.enum(["new-file", "edit", "delete", "rename", "any"]);
|
|
53
|
+
var AppliesWhen = z.object({
|
|
54
|
+
stack: z.array(z.string().min(1)).optional(),
|
|
55
|
+
change_types: z.array(ChangeType).optional(),
|
|
56
|
+
file_globs: z.array(z.string().min(1)).optional(),
|
|
57
|
+
not_file_globs: z.array(z.string().min(1)).optional()
|
|
58
|
+
}).strict();
|
|
59
|
+
var Reference = z.object({
|
|
60
|
+
type: z.enum(["framework-doc", "internal-doc", "pr", "incident", "external"]),
|
|
61
|
+
url: z.string().min(1),
|
|
62
|
+
description: z.string().min(1)
|
|
63
|
+
}).strict();
|
|
64
|
+
var IsoDate = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "last_verified must be ISO date YYYY-MM-DD").refine((s) => {
|
|
65
|
+
const d = /* @__PURE__ */ new Date(s + "T00:00:00Z");
|
|
66
|
+
return !Number.isNaN(d.getTime()) && d.toISOString().startsWith(s);
|
|
67
|
+
}, "last_verified must be a real calendar date");
|
|
68
|
+
var RuleId = z.string().regex(/^[a-z][a-z0-9-]*\/[a-z][a-z0-9-]*$/, {
|
|
69
|
+
message: "rule_id must be `<category>/<kebab-name>`"
|
|
70
|
+
});
|
|
71
|
+
var ArchitecturalDefaultRuleSchema = z.object({
|
|
72
|
+
schema_version: z.literal(1),
|
|
73
|
+
rule_id: RuleId,
|
|
74
|
+
rule_version: z.number().int().positive(),
|
|
75
|
+
category: z.string().min(1),
|
|
76
|
+
severity: z.enum(["blocker", "warning", "info"]),
|
|
77
|
+
title: z.string().min(1),
|
|
78
|
+
rationale: z.string().min(1),
|
|
79
|
+
applies_when: AppliesWhen,
|
|
80
|
+
evidence_of_violation: z.array(EvidenceMatcher).min(1, {
|
|
81
|
+
message: "rule must declare at least one evidence matcher"
|
|
82
|
+
}),
|
|
83
|
+
remediation: z.string().min(1),
|
|
84
|
+
references: z.array(Reference),
|
|
85
|
+
last_verified: IsoDate,
|
|
86
|
+
tags: z.array(z.string().min(1))
|
|
87
|
+
}).strict();
|
|
88
|
+
function parseRule(input) {
|
|
89
|
+
return ArchitecturalDefaultRuleSchema.parse(input);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ../vo-arch-defaults/src/schema/override-v1.ts
|
|
93
|
+
import { z as z2 } from "zod";
|
|
94
|
+
var PartialRuleSchema = ArchitecturalDefaultRuleSchema.partial();
|
|
95
|
+
var TenantOverrideSchema = z2.object({
|
|
96
|
+
schema_version: z2.literal(1),
|
|
97
|
+
suppressed_rule_ids: z2.array(z2.string().min(1)),
|
|
98
|
+
modified_rules: z2.record(z2.string().min(1), PartialRuleSchema),
|
|
99
|
+
added_rules: z2.array(ArchitecturalDefaultRuleSchema),
|
|
100
|
+
/**
|
|
101
|
+
* Per-tenant stack override (added 2026-05-24 — audit HIGH
|
|
102
|
+
* "DEFAULT_STACK hardcoded for Nexus repo"). When set, downstream
|
|
103
|
+
* consumers (vo-mcp KB pre-filter, vo-arch-check CLI) use this list
|
|
104
|
+
* instead of their built-in default. Lets external tenants whose
|
|
105
|
+
* repo isn't `firebase+react+pnpm` get useful KB rule matches by
|
|
106
|
+
* dropping a `~/.claude/vo-arch-defaults.local.json` with their own
|
|
107
|
+
* stack identifiers — no code change required.
|
|
108
|
+
*
|
|
109
|
+
* Open string union — values are not constrained beyond non-empty
|
|
110
|
+
* strings so out-of-tree tenants can declare their own
|
|
111
|
+
* (e.g. `vercel-edge`, `next-15`, `drizzle-postgres`).
|
|
112
|
+
*
|
|
113
|
+
* Optional. Undefined / omitted preserves pre-2026-05-24 behavior
|
|
114
|
+
* (consumer falls back to its hardcoded default stack).
|
|
115
|
+
*/
|
|
116
|
+
tenant_stack: z2.array(z2.string().min(1)).optional()
|
|
117
|
+
}).strict();
|
|
118
|
+
function parseOverride(input) {
|
|
119
|
+
return TenantOverrideSchema.parse(input);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ../vo-arch-defaults/src/storage/load-bundled.ts
|
|
123
|
+
import { readdirSync, readFileSync, statSync, existsSync } from "node:fs";
|
|
124
|
+
import { dirname, join } from "node:path";
|
|
125
|
+
import { fileURLToPath } from "node:url";
|
|
126
|
+
function resolveBundledCorpusDir() {
|
|
127
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
128
|
+
const candidates = [
|
|
129
|
+
join(here, "..", "corpus"),
|
|
130
|
+
// dist/corpus next to dist/storage
|
|
131
|
+
join(here, "..", "..", "corpus")
|
|
132
|
+
// src/corpus next to src/storage (under vitest)
|
|
133
|
+
];
|
|
134
|
+
for (const c of candidates) {
|
|
135
|
+
if (existsSync(c) && statSync(c).isDirectory()) return c;
|
|
136
|
+
}
|
|
137
|
+
throw new Error(
|
|
138
|
+
`vo-arch-defaults: could not locate bundled corpus directory. Tried: ${candidates.join(", ")}`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
function walkJson(dir, out) {
|
|
142
|
+
for (const entry of readdirSync(dir)) {
|
|
143
|
+
const full = join(dir, entry);
|
|
144
|
+
const st = statSync(full);
|
|
145
|
+
if (st.isDirectory()) {
|
|
146
|
+
walkJson(full, out);
|
|
147
|
+
} else if (entry.endsWith(".json")) {
|
|
148
|
+
out.push(full);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function loadBundledCorpus(opts = {}) {
|
|
153
|
+
const dir = opts.corpusDir ?? resolveBundledCorpusDir();
|
|
154
|
+
const files = [];
|
|
155
|
+
walkJson(dir, files);
|
|
156
|
+
files.sort();
|
|
157
|
+
const rules = [];
|
|
158
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
159
|
+
for (const file of files) {
|
|
160
|
+
const raw = readFileSync(file, "utf8");
|
|
161
|
+
let parsed;
|
|
162
|
+
try {
|
|
163
|
+
parsed = JSON.parse(raw);
|
|
164
|
+
} catch (err) {
|
|
165
|
+
const m = err instanceof Error ? err.message : String(err);
|
|
166
|
+
throw new Error(`vo-arch-defaults: invalid JSON in ${file}: ${m}`, { cause: err });
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
const rule = parseRule(parsed);
|
|
170
|
+
if (seenIds.has(rule.rule_id)) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
`vo-arch-defaults: duplicate rule_id '${rule.rule_id}' (second occurrence in ${file})`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
seenIds.add(rule.rule_id);
|
|
176
|
+
rules.push(rule);
|
|
177
|
+
} catch (err) {
|
|
178
|
+
const m = err instanceof Error ? err.message : String(err);
|
|
179
|
+
throw new Error(`vo-arch-defaults: schema validation failed for ${file}: ${m}`, { cause: err });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return { rules, source_paths: files };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ../vo-arch-defaults/src/storage/load-override.ts
|
|
186
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
187
|
+
import { homedir } from "node:os";
|
|
188
|
+
import { join as join2 } from "node:path";
|
|
189
|
+
function defaultOverridePath() {
|
|
190
|
+
return join2(homedir(), ".claude", "vo-arch-defaults.local.json");
|
|
191
|
+
}
|
|
192
|
+
function loadTenantOverride(opts = {}) {
|
|
193
|
+
const path3 = opts.path ?? defaultOverridePath();
|
|
194
|
+
if (!existsSync2(path3)) {
|
|
195
|
+
return { override: null, source_path: null };
|
|
196
|
+
}
|
|
197
|
+
const raw = readFileSync2(path3, "utf8");
|
|
198
|
+
let parsed;
|
|
199
|
+
try {
|
|
200
|
+
parsed = JSON.parse(raw);
|
|
201
|
+
} catch (err) {
|
|
202
|
+
const m = err instanceof Error ? err.message : String(err);
|
|
203
|
+
throw new Error(`vo-arch-defaults: invalid JSON in override ${path3}: ${m}`, { cause: err });
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
const override = parseOverride(parsed);
|
|
207
|
+
return { override, source_path: path3 };
|
|
208
|
+
} catch (err) {
|
|
209
|
+
const m = err instanceof Error ? err.message : String(err);
|
|
210
|
+
throw new Error(`vo-arch-defaults: override schema validation failed for ${path3}: ${m}`, { cause: err });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ../vo-arch-defaults/src/storage/merge.ts
|
|
215
|
+
var IMMUTABLE_FIELDS = [
|
|
216
|
+
"schema_version",
|
|
217
|
+
"rule_id"
|
|
218
|
+
];
|
|
219
|
+
function applyModification(base, patch) {
|
|
220
|
+
const cleanPatch = { ...patch };
|
|
221
|
+
for (const k of IMMUTABLE_FIELDS) {
|
|
222
|
+
delete cleanPatch[k];
|
|
223
|
+
}
|
|
224
|
+
return { ...base, ...cleanPatch };
|
|
225
|
+
}
|
|
226
|
+
function mergeCorpusWithOverride(bundled, override) {
|
|
227
|
+
if (override === null) {
|
|
228
|
+
return {
|
|
229
|
+
rules: bundled,
|
|
230
|
+
suppressed_count: 0,
|
|
231
|
+
modified_count: 0,
|
|
232
|
+
added_count: 0
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
const suppressed = new Set(override.suppressed_rule_ids);
|
|
236
|
+
let suppressedCount = 0;
|
|
237
|
+
let modifiedCount = 0;
|
|
238
|
+
const afterFilter = [];
|
|
239
|
+
for (const rule of bundled) {
|
|
240
|
+
if (suppressed.has(rule.rule_id)) {
|
|
241
|
+
suppressedCount += 1;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const patch = override.modified_rules[rule.rule_id];
|
|
245
|
+
if (patch !== void 0) {
|
|
246
|
+
afterFilter.push(applyModification(rule, patch));
|
|
247
|
+
modifiedCount += 1;
|
|
248
|
+
} else {
|
|
249
|
+
afterFilter.push(rule);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const byId = new Map(
|
|
253
|
+
afterFilter.map((r) => [r.rule_id, r])
|
|
254
|
+
);
|
|
255
|
+
let addedCount = 0;
|
|
256
|
+
for (const rule of override.added_rules) {
|
|
257
|
+
if (suppressed.has(rule.rule_id)) {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
byId.set(rule.rule_id, rule);
|
|
261
|
+
addedCount += 1;
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
rules: Array.from(byId.values()),
|
|
265
|
+
suppressed_count: suppressedCount,
|
|
266
|
+
modified_count: modifiedCount,
|
|
267
|
+
added_count: addedCount
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ../vo-arch-defaults/src/query/applies-to-stack.ts
|
|
272
|
+
function appliesToStack(rule, stacks) {
|
|
273
|
+
const required = rule.applies_when.stack;
|
|
274
|
+
if (!required || required.length === 0) return true;
|
|
275
|
+
if (stacks.length === 0) return false;
|
|
276
|
+
const want = new Set(required);
|
|
277
|
+
for (const s of stacks) {
|
|
278
|
+
if (want.has(s)) return true;
|
|
279
|
+
}
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ../vo-arch-defaults/src/query/applies-to-change.ts
|
|
284
|
+
function appliesToChangeType(rule, changeType) {
|
|
285
|
+
const required = rule.applies_when.change_types;
|
|
286
|
+
if (!required || required.length === 0) return true;
|
|
287
|
+
if (required.includes("any")) return true;
|
|
288
|
+
if (changeType === "any") return true;
|
|
289
|
+
return required.includes(changeType);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ../vo-arch-defaults/src/query/glob.ts
|
|
293
|
+
var REGEX_META = /[.+^${}()|[\]\\]/g;
|
|
294
|
+
function globToRegExp(glob) {
|
|
295
|
+
let out = "";
|
|
296
|
+
let i = 0;
|
|
297
|
+
while (i < glob.length) {
|
|
298
|
+
const c = glob[i] ?? "";
|
|
299
|
+
if (c === "*") {
|
|
300
|
+
const next = glob[i + 1];
|
|
301
|
+
if (next === "*") {
|
|
302
|
+
const after = glob[i + 2];
|
|
303
|
+
if (after === "/") {
|
|
304
|
+
out += "(?:.*/)?";
|
|
305
|
+
i += 3;
|
|
306
|
+
} else {
|
|
307
|
+
out += ".*";
|
|
308
|
+
i += 2;
|
|
309
|
+
}
|
|
310
|
+
} else {
|
|
311
|
+
out += "[^/]*";
|
|
312
|
+
i += 1;
|
|
313
|
+
}
|
|
314
|
+
} else if (c === "?") {
|
|
315
|
+
out += "[^/]";
|
|
316
|
+
i += 1;
|
|
317
|
+
} else if (c === "{") {
|
|
318
|
+
const closeIdx = glob.indexOf("}", i + 1);
|
|
319
|
+
if (closeIdx < 0) {
|
|
320
|
+
out += "\\{";
|
|
321
|
+
i += 1;
|
|
322
|
+
} else {
|
|
323
|
+
const inner = glob.slice(i + 1, closeIdx);
|
|
324
|
+
if (inner.length === 0) {
|
|
325
|
+
i = closeIdx + 1;
|
|
326
|
+
} else {
|
|
327
|
+
const opts = inner.split(",");
|
|
328
|
+
const escapedOpts = opts.map((o) => o.replace(REGEX_META, "\\$&"));
|
|
329
|
+
out += "(?:" + escapedOpts.join("|") + ")";
|
|
330
|
+
i = closeIdx + 1;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
} else {
|
|
334
|
+
out += c.replace(REGEX_META, "\\$&");
|
|
335
|
+
i += 1;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return new RegExp("^" + out + "$");
|
|
339
|
+
}
|
|
340
|
+
function matchesAnyGlob(path3, globs) {
|
|
341
|
+
for (const g of globs) {
|
|
342
|
+
if (globToRegExp(g).test(path3)) return true;
|
|
343
|
+
}
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ../vo-arch-defaults/src/query/applies-to-files.ts
|
|
348
|
+
function appliesToFiles(rule, filePaths) {
|
|
349
|
+
const include = rule.applies_when.file_globs;
|
|
350
|
+
const exclude = rule.applies_when.not_file_globs;
|
|
351
|
+
const noInclude = !include || include.length === 0;
|
|
352
|
+
const noExclude = !exclude || exclude.length === 0;
|
|
353
|
+
if (noInclude && noExclude) return true;
|
|
354
|
+
if (filePaths.length === 0) {
|
|
355
|
+
return noInclude;
|
|
356
|
+
}
|
|
357
|
+
for (const p of filePaths) {
|
|
358
|
+
const isIncluded = noInclude || include !== void 0 && matchesAnyGlob(p, include);
|
|
359
|
+
if (!isIncluded) continue;
|
|
360
|
+
const isExcluded = !noExclude && exclude !== void 0 && matchesAnyGlob(p, exclude);
|
|
361
|
+
if (isExcluded) continue;
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// ../vo-arch-defaults/src/analyze/diff-parser.ts
|
|
368
|
+
function stripPathPrefix(p) {
|
|
369
|
+
if (p.startsWith("a/") || p.startsWith("b/")) return p.slice(2);
|
|
370
|
+
return p;
|
|
371
|
+
}
|
|
372
|
+
function parseUnifiedDiff(text) {
|
|
373
|
+
const lines = text.split(/\r?\n/);
|
|
374
|
+
const files = [];
|
|
375
|
+
let current = null;
|
|
376
|
+
let newLine = 0;
|
|
377
|
+
for (let i = 0; i < lines.length; i++) {
|
|
378
|
+
const raw = lines[i] ?? "";
|
|
379
|
+
if (raw.startsWith("diff --git ")) {
|
|
380
|
+
const m = /^diff --git (\S+) (\S+)$/.exec(raw);
|
|
381
|
+
if (m && m[1] && m[2]) {
|
|
382
|
+
current = {
|
|
383
|
+
path: stripPathPrefix(m[2]),
|
|
384
|
+
previous_path: stripPathPrefix(m[1]),
|
|
385
|
+
change_type: "edit",
|
|
386
|
+
// refined as we see new file mode / deleted file mode / rename
|
|
387
|
+
added_lines: []
|
|
388
|
+
};
|
|
389
|
+
if (current.previous_path === current.path) current.previous_path = null;
|
|
390
|
+
files.push(current);
|
|
391
|
+
}
|
|
392
|
+
newLine = 0;
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
if (!current) continue;
|
|
396
|
+
if (raw.startsWith("new file mode ")) {
|
|
397
|
+
current.change_type = "new-file";
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
if (raw.startsWith("deleted file mode ")) {
|
|
401
|
+
current.change_type = "delete";
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
if (raw.startsWith("rename from ")) {
|
|
405
|
+
current.previous_path = raw.slice("rename from ".length).trim();
|
|
406
|
+
current.change_type = "rename";
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
if (raw.startsWith("rename to ")) {
|
|
410
|
+
current.path = raw.slice("rename to ".length).trim();
|
|
411
|
+
current.change_type = "rename";
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
const hunk = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(raw);
|
|
415
|
+
if (hunk && hunk[1]) {
|
|
416
|
+
newLine = parseInt(hunk[1], 10);
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
if (raw.startsWith("+++") || raw.startsWith("---")) {
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
if (raw.startsWith("+") && !raw.startsWith("+++")) {
|
|
423
|
+
current.added_lines.push({ line: newLine, text: raw.slice(1) });
|
|
424
|
+
newLine += 1;
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
if (raw.startsWith("-") && !raw.startsWith("---")) {
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
if (raw.startsWith("\\")) {
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
if (newLine > 0) newLine += 1;
|
|
434
|
+
}
|
|
435
|
+
return { files };
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// ../vo-arch-defaults/src/analyze/evidence-matchers/regex-matcher.ts
|
|
439
|
+
var MAX_EXCERPT = 200;
|
|
440
|
+
function sanitizeLine(s) {
|
|
441
|
+
let out = s.replace(/^\s+/, "");
|
|
442
|
+
if (out.length > MAX_EXCERPT) out = out.slice(0, MAX_EXCERPT) + "\u2026";
|
|
443
|
+
return out;
|
|
444
|
+
}
|
|
445
|
+
function runRegexMatcher(matcher, file) {
|
|
446
|
+
if (matcher.kind !== "regex" || !matcher.pattern) return [];
|
|
447
|
+
let re;
|
|
448
|
+
try {
|
|
449
|
+
re = new RegExp(matcher.pattern);
|
|
450
|
+
} catch {
|
|
451
|
+
return [];
|
|
452
|
+
}
|
|
453
|
+
const hits = [];
|
|
454
|
+
for (const { line, text } of file.added_lines) {
|
|
455
|
+
if (re.test(text)) {
|
|
456
|
+
hits.push({
|
|
457
|
+
matcher_kind: "regex",
|
|
458
|
+
matcher_description: matcher.description,
|
|
459
|
+
file: file.path,
|
|
460
|
+
line,
|
|
461
|
+
excerpt: sanitizeLine(text)
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return hits;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// ../vo-arch-defaults/src/analyze/evidence-matchers/import-detector.ts
|
|
469
|
+
var STATIC_IMPORT = /import\s+(?:[^'"\n]{0,200}?from\s+)?(['"])([^'"]+)\1/;
|
|
470
|
+
var DYNAMIC_IMPORT = /import\(\s*(['"])([^'"]+)\1\s*\)/;
|
|
471
|
+
var REQUIRE_CALL = /require\(\s*(['"])([^'"]+)\1\s*\)/;
|
|
472
|
+
var MAX_EXCERPT2 = 200;
|
|
473
|
+
function sanitize(s) {
|
|
474
|
+
let out = s.replace(/^\s+/, "");
|
|
475
|
+
if (out.length > MAX_EXCERPT2) out = out.slice(0, MAX_EXCERPT2) + "\u2026";
|
|
476
|
+
return out;
|
|
477
|
+
}
|
|
478
|
+
function tryExtractSpec(line) {
|
|
479
|
+
const m = STATIC_IMPORT.exec(line) ?? DYNAMIC_IMPORT.exec(line) ?? REQUIRE_CALL.exec(line);
|
|
480
|
+
if (!m || !m[2]) return null;
|
|
481
|
+
return m[2];
|
|
482
|
+
}
|
|
483
|
+
function specMatches(spec, matcher) {
|
|
484
|
+
const cfg = matcher.config ?? {};
|
|
485
|
+
const from = typeof cfg.from === "string" ? cfg.from : null;
|
|
486
|
+
const mode = typeof cfg.match === "string" ? cfg.match : "exact";
|
|
487
|
+
if (from === null) return false;
|
|
488
|
+
if (mode === "regex") {
|
|
489
|
+
try {
|
|
490
|
+
return new RegExp(from).test(spec);
|
|
491
|
+
} catch {
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
if (mode === "prefix") {
|
|
496
|
+
return spec === from || spec.startsWith(from + "/");
|
|
497
|
+
}
|
|
498
|
+
return spec === from;
|
|
499
|
+
}
|
|
500
|
+
function runImportDetector(matcher, file) {
|
|
501
|
+
if (matcher.kind !== "import-detector") return [];
|
|
502
|
+
const hits = [];
|
|
503
|
+
for (const { line, text } of file.added_lines) {
|
|
504
|
+
const spec = tryExtractSpec(text);
|
|
505
|
+
if (spec === null) continue;
|
|
506
|
+
if (!specMatches(spec, matcher)) continue;
|
|
507
|
+
hits.push({
|
|
508
|
+
matcher_kind: "import-detector",
|
|
509
|
+
matcher_description: matcher.description,
|
|
510
|
+
file: file.path,
|
|
511
|
+
line,
|
|
512
|
+
excerpt: sanitize(text)
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
return hits;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// ../vo-arch-defaults/src/analyze/evidence-matchers/file-size.ts
|
|
519
|
+
function runFileSizeMatcher(matcher, file) {
|
|
520
|
+
if (matcher.kind !== "file-size") return [];
|
|
521
|
+
const cfg = matcher.config ?? {};
|
|
522
|
+
const maxLines = typeof cfg.max_lines === "number" ? cfg.max_lines : null;
|
|
523
|
+
const explicitMaxAdded = typeof cfg.max_added_lines === "number" ? cfg.max_added_lines : null;
|
|
524
|
+
const cap = explicitMaxAdded ?? maxLines;
|
|
525
|
+
if (cap === null || cap <= 0) return [];
|
|
526
|
+
if (file.added_lines.length <= cap) return [];
|
|
527
|
+
const sample = file.added_lines[0]?.text ?? "";
|
|
528
|
+
return [
|
|
529
|
+
{
|
|
530
|
+
matcher_kind: "file-size",
|
|
531
|
+
matcher_description: `${matcher.description} (heuristic: ${file.added_lines.length} added lines > cap ${cap})`,
|
|
532
|
+
file: file.path,
|
|
533
|
+
excerpt: sample.slice(0, 100)
|
|
534
|
+
}
|
|
535
|
+
];
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// ../vo-arch-defaults/src/analyze/evidence-matchers/package-json.ts
|
|
539
|
+
function valueMatches(value, cfg) {
|
|
540
|
+
if (typeof cfg.forbidden_prefix === "string" && value.startsWith(cfg.forbidden_prefix)) {
|
|
541
|
+
return true;
|
|
542
|
+
}
|
|
543
|
+
if (typeof cfg.forbidden_exact === "string" && value === cfg.forbidden_exact) {
|
|
544
|
+
return true;
|
|
545
|
+
}
|
|
546
|
+
if (typeof cfg.forbidden_regex === "string") {
|
|
547
|
+
try {
|
|
548
|
+
if (new RegExp(cfg.forbidden_regex).test(value)) return true;
|
|
549
|
+
} catch {
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return false;
|
|
553
|
+
}
|
|
554
|
+
function runPackageJsonMatcher(matcher, file) {
|
|
555
|
+
if (matcher.kind !== "package-json-field") return [];
|
|
556
|
+
if (!file.path.endsWith("package.json")) return [];
|
|
557
|
+
const cfg = matcher.config ?? {};
|
|
558
|
+
const field = typeof cfg.field === "string" ? cfg.field : null;
|
|
559
|
+
if (field === null) return [];
|
|
560
|
+
const fieldRe = new RegExp(`"${field}"\\s*:\\s*"([^"]+)"`);
|
|
561
|
+
const hits = [];
|
|
562
|
+
for (const { line, text } of file.added_lines) {
|
|
563
|
+
const m = fieldRe.exec(text);
|
|
564
|
+
if (!m || !m[1]) continue;
|
|
565
|
+
if (valueMatches(m[1], cfg)) {
|
|
566
|
+
hits.push({
|
|
567
|
+
matcher_kind: "package-json-field",
|
|
568
|
+
matcher_description: matcher.description,
|
|
569
|
+
file: file.path,
|
|
570
|
+
line,
|
|
571
|
+
excerpt: text.trim().slice(0, 200)
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
return hits;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// ../vo-arch-defaults/src/analyze/evidence-matchers/index.ts
|
|
579
|
+
function runEvidenceMatcher(matcher, file) {
|
|
580
|
+
switch (matcher.kind) {
|
|
581
|
+
case "regex":
|
|
582
|
+
return runRegexMatcher(matcher, file);
|
|
583
|
+
case "import-detector":
|
|
584
|
+
return runImportDetector(matcher, file);
|
|
585
|
+
case "file-size":
|
|
586
|
+
return runFileSizeMatcher(matcher, file);
|
|
587
|
+
case "package-json-field":
|
|
588
|
+
return runPackageJsonMatcher(matcher, file);
|
|
589
|
+
case "ast-pattern":
|
|
590
|
+
case "custom":
|
|
591
|
+
return [];
|
|
592
|
+
default:
|
|
593
|
+
return [];
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// ../vo-arch-defaults/src/query/staleness.ts
|
|
598
|
+
var DEFAULT_STALENESS_THRESHOLD_DAYS = 365;
|
|
599
|
+
function computeStaleRules(rules, opts = {}) {
|
|
600
|
+
const threshold = opts.thresholdDays ?? DEFAULT_STALENESS_THRESHOLD_DAYS;
|
|
601
|
+
if (!Number.isFinite(threshold)) return [];
|
|
602
|
+
if (threshold <= 0) return [];
|
|
603
|
+
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
604
|
+
const nowMs = now.getTime();
|
|
605
|
+
if (!Number.isFinite(nowMs)) return [];
|
|
606
|
+
const out = [];
|
|
607
|
+
for (const rule of rules) {
|
|
608
|
+
const verifiedMs = Date.parse(rule.last_verified + "T00:00:00Z");
|
|
609
|
+
if (!Number.isFinite(verifiedMs)) continue;
|
|
610
|
+
const daysStale = Math.floor((nowMs - verifiedMs) / 864e5);
|
|
611
|
+
if (daysStale > threshold) {
|
|
612
|
+
out.push({
|
|
613
|
+
rule_id: rule.rule_id,
|
|
614
|
+
last_verified: rule.last_verified,
|
|
615
|
+
days_stale: daysStale
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
out.sort((a, b) => b.days_stale - a.days_stale);
|
|
620
|
+
return out;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// ../vo-arch-defaults/src/query/run-query.ts
|
|
624
|
+
function findApplicableRules(input, opts = {}) {
|
|
625
|
+
const bundled = opts.rules !== void 0 ? opts.rules : loadBundledCorpus({ corpusDir: opts.corpusDir }).rules;
|
|
626
|
+
const override = opts.override !== void 0 ? opts.override : loadTenantOverride({ path: opts.tenantOverridePath }).override;
|
|
627
|
+
const merged = mergeCorpusWithOverride(bundled, override);
|
|
628
|
+
const parsedDiff = input.diff_excerpt ? parseUnifiedDiff(input.diff_excerpt) : null;
|
|
629
|
+
const categoryFilter = input.categories !== void 0 ? new Set(input.categories) : null;
|
|
630
|
+
const tagAnyFilter = input.tags_any !== void 0 ? new Set(input.tags_any) : null;
|
|
631
|
+
const applicable = [];
|
|
632
|
+
for (const rule of merged.rules) {
|
|
633
|
+
const stackOk = appliesToStack(rule, input.stack);
|
|
634
|
+
const changeOk = appliesToChangeType(rule, input.change_type);
|
|
635
|
+
const filesOk = appliesToFiles(rule, input.file_paths);
|
|
636
|
+
if (!stackOk || !changeOk || !filesOk) continue;
|
|
637
|
+
if (categoryFilter !== null && !categoryFilter.has(rule.category)) continue;
|
|
638
|
+
if (tagAnyFilter !== null) {
|
|
639
|
+
let tagHit = false;
|
|
640
|
+
for (const t of rule.tags) {
|
|
641
|
+
if (tagAnyFilter.has(t)) {
|
|
642
|
+
tagHit = true;
|
|
643
|
+
break;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
if (!tagHit) continue;
|
|
647
|
+
}
|
|
648
|
+
const hits = [];
|
|
649
|
+
if (parsedDiff !== null) {
|
|
650
|
+
for (const file of parsedDiff.files) {
|
|
651
|
+
const filePathArr = [file.path];
|
|
652
|
+
if (!appliesToFiles(rule, filePathArr)) continue;
|
|
653
|
+
for (const matcher of rule.evidence_of_violation) {
|
|
654
|
+
const fileHits = runEvidenceMatcher(matcher, file);
|
|
655
|
+
for (const h of fileHits) hits.push(h);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
applicable.push({
|
|
660
|
+
rule,
|
|
661
|
+
reason: {
|
|
662
|
+
stack_matched: stackOk,
|
|
663
|
+
change_type_matched: changeOk,
|
|
664
|
+
file_glob_matched: filesOk
|
|
665
|
+
},
|
|
666
|
+
evidence: hits
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
const stale_rules = computeStaleRules(merged.rules, {
|
|
670
|
+
...opts.stalenessThresholdDays !== void 0 ? { thresholdDays: opts.stalenessThresholdDays } : {},
|
|
671
|
+
...opts.now !== void 0 ? { now: opts.now } : {}
|
|
672
|
+
});
|
|
673
|
+
return {
|
|
674
|
+
applicable,
|
|
675
|
+
considered_count: merged.rules.length,
|
|
676
|
+
suppressed_count: merged.suppressed_count,
|
|
677
|
+
stale_rules
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// ../vo-arch-defaults/src/query/corpus-version.ts
|
|
682
|
+
import { createHash } from "node:crypto";
|
|
683
|
+
|
|
684
|
+
// ../vo-arch-defaults/src/query/resolve-stack.ts
|
|
685
|
+
function resolveStack(override, fallback) {
|
|
686
|
+
if (override !== null && override.tenant_stack !== void 0) {
|
|
687
|
+
return override.tenant_stack;
|
|
688
|
+
}
|
|
689
|
+
return fallback;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// ../vo-arch-defaults/src/pii-sanitize.ts
|
|
693
|
+
var SANITIZE_DEFAULT_MAX_LEN = 200;
|
|
694
|
+
function sanitizeExcerpt(raw, maxLen = SANITIZE_DEFAULT_MAX_LEN) {
|
|
695
|
+
let s = raw;
|
|
696
|
+
s = s.replace(/eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{5,}/g, "[REDACTED_JWT]");
|
|
697
|
+
s = s.replace(/\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g, "[REDACTED_AWS_KEY]");
|
|
698
|
+
s = s.replace(/\bAIza[0-9A-Za-z_-]{35}\b/g, "[REDACTED_GOOGLE_KEY]");
|
|
699
|
+
s = s.replace(/\bgh[pousr]_[A-Za-z0-9]{36,}\b/g, "[REDACTED_GITHUB_TOKEN]");
|
|
700
|
+
s = s.replace(/\bsk_(?:live|test)_[A-Za-z0-9_-]{20,}/g, "[REDACTED_STRIPE_KEY]");
|
|
701
|
+
s = s.replace(/sk-[A-Za-z0-9]{16,}/g, "[REDACTED_KEY]");
|
|
702
|
+
s = s.replace(/Bearer\s+[A-Za-z0-9_\-.]{16,}/g, "Bearer [REDACTED]");
|
|
703
|
+
s = s.replace(/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g, "[REDACTED_EMAIL]");
|
|
704
|
+
s = s.replace(/\b\d{3}-\d{2}-\d{4}\b/g, "[REDACTED_SSN]");
|
|
705
|
+
s = s.replace(/(?<![A-Za-z0-9])([\\/])(Users|home)\1[^\\/\s'"`]+/g, "$1$2$1[USER]");
|
|
706
|
+
if (s.length > maxLen) s = s.slice(0, maxLen) + "\u2026";
|
|
707
|
+
return s;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// src/logging/events-writer.ts
|
|
711
|
+
var DEFAULT_EVENTS_MAX_BYTES = 50 * 1024 * 1024;
|
|
712
|
+
var DEFAULT_EVENTS_KEEP_ROTATED = 10;
|
|
713
|
+
function defaultEventsPath() {
|
|
714
|
+
const envPath = process.env["VO_MCP_EVENTS_PATH"];
|
|
715
|
+
if (envPath && envPath.length > 0) return envPath;
|
|
716
|
+
return join3(homedir2(), ".claude", "vo-mcp-events.jsonl");
|
|
717
|
+
}
|
|
718
|
+
function envPositiveInt(name) {
|
|
719
|
+
const raw = process.env[name];
|
|
720
|
+
if (raw === void 0 || raw.length === 0) return null;
|
|
721
|
+
const n = Number(raw);
|
|
722
|
+
return Number.isFinite(n) && n > 0 && Number.isInteger(n) ? n : null;
|
|
723
|
+
}
|
|
724
|
+
function rotateNow(filePath) {
|
|
725
|
+
try {
|
|
726
|
+
if (!statSync2(filePath).isFile()) return false;
|
|
727
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
728
|
+
const rotatedPath = `${filePath}.${ts}.gz`;
|
|
729
|
+
const contents = readFileSync3(filePath);
|
|
730
|
+
writeFileSync(rotatedPath, gzipSync(contents));
|
|
731
|
+
try {
|
|
732
|
+
chmodSync(rotatedPath, 384);
|
|
733
|
+
} catch {
|
|
734
|
+
}
|
|
735
|
+
unlinkSync(filePath);
|
|
736
|
+
return true;
|
|
737
|
+
} catch (err) {
|
|
738
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
739
|
+
process.stderr.write(`[vo-mcp] events log rotation failed: ${msg}
|
|
740
|
+
`);
|
|
741
|
+
return false;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
function pruneRotated(filePath, keep) {
|
|
745
|
+
try {
|
|
746
|
+
const dir = dirname2(filePath);
|
|
747
|
+
const baseName = basename(filePath);
|
|
748
|
+
const prefix = `${baseName}.`;
|
|
749
|
+
const rotated = [];
|
|
750
|
+
for (const entry of readdirSync2(dir)) {
|
|
751
|
+
if (!entry.startsWith(prefix) || !entry.endsWith(".gz")) continue;
|
|
752
|
+
const full = join3(dir, entry);
|
|
753
|
+
try {
|
|
754
|
+
rotated.push({ full, mtimeMs: statSync2(full).mtimeMs });
|
|
755
|
+
} catch {
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
rotated.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
759
|
+
for (const r of rotated.slice(keep)) {
|
|
760
|
+
try {
|
|
761
|
+
unlinkSync(r.full);
|
|
762
|
+
} catch {
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
} catch {
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
function createFileEventsWriter(opts = {}) {
|
|
769
|
+
const filePath = opts.path ?? defaultEventsPath();
|
|
770
|
+
const maxBytes = opts.maxBytes ?? envPositiveInt("VO_MCP_EVENTS_MAX_BYTES") ?? DEFAULT_EVENTS_MAX_BYTES;
|
|
771
|
+
const keepRotated = opts.keepRotated ?? envPositiveInt("VO_MCP_EVENTS_KEEP_ROTATED") ?? DEFAULT_EVENTS_KEEP_ROTATED;
|
|
772
|
+
mkdirSync(dirname2(filePath), { recursive: true, mode: 448 });
|
|
773
|
+
let permsApplied = false;
|
|
774
|
+
let currentBytes = 0;
|
|
775
|
+
try {
|
|
776
|
+
currentBytes = statSync2(filePath).size;
|
|
777
|
+
} catch {
|
|
778
|
+
}
|
|
779
|
+
return {
|
|
780
|
+
append(event) {
|
|
781
|
+
const line = JSON.stringify(event) + "\n";
|
|
782
|
+
const lineBytes = Buffer.byteLength(line, "utf8");
|
|
783
|
+
if (currentBytes > 0 && currentBytes + lineBytes > maxBytes) {
|
|
784
|
+
if (rotateNow(filePath)) {
|
|
785
|
+
pruneRotated(filePath, keepRotated);
|
|
786
|
+
currentBytes = 0;
|
|
787
|
+
permsApplied = false;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
try {
|
|
791
|
+
appendFileSync(filePath, line, "utf8");
|
|
792
|
+
currentBytes += lineBytes;
|
|
793
|
+
} catch (err) {
|
|
794
|
+
const code = err.code;
|
|
795
|
+
if (code === "ENOSPC") {
|
|
796
|
+
process.stderr.write(
|
|
797
|
+
`[vo-mcp] events log write failed (disk full): event dropped
|
|
798
|
+
`
|
|
799
|
+
);
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
throw err;
|
|
803
|
+
}
|
|
804
|
+
if (!permsApplied) {
|
|
805
|
+
try {
|
|
806
|
+
chmodSync(filePath, 384);
|
|
807
|
+
} catch {
|
|
808
|
+
}
|
|
809
|
+
permsApplied = true;
|
|
810
|
+
}
|
|
811
|
+
},
|
|
812
|
+
path() {
|
|
813
|
+
return filePath;
|
|
814
|
+
},
|
|
815
|
+
close() {
|
|
816
|
+
}
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
function createMemoryEventsWriter() {
|
|
820
|
+
const events = [];
|
|
821
|
+
return {
|
|
822
|
+
events,
|
|
823
|
+
append(event) {
|
|
824
|
+
events.push(event);
|
|
825
|
+
},
|
|
826
|
+
path() {
|
|
827
|
+
return "memory";
|
|
828
|
+
},
|
|
829
|
+
close() {
|
|
830
|
+
}
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// src/tools/common.ts
|
|
835
|
+
function readVoMcpVersion() {
|
|
836
|
+
try {
|
|
837
|
+
const here = dirname3(fileURLToPath2(import.meta.url));
|
|
838
|
+
const pkgPath = join4(here, "..", "..", "package.json");
|
|
839
|
+
const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
840
|
+
return typeof pkg.version === "string" ? pkg.version : "0.0.0-unknown";
|
|
841
|
+
} catch {
|
|
842
|
+
return "0.0.0-unknown";
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
var VO_MCP_VERSION = readVoMcpVersion();
|
|
846
|
+
function bytesOf(s) {
|
|
847
|
+
return Buffer.byteLength(s, "utf8");
|
|
848
|
+
}
|
|
849
|
+
function sha256Hex(s) {
|
|
850
|
+
return createHash2("sha256").update(s, "utf8").digest("hex");
|
|
851
|
+
}
|
|
852
|
+
function invalidParams(toolName, message) {
|
|
853
|
+
return new McpError(ErrorCode.InvalidParams, `${toolName}: ${message}`);
|
|
854
|
+
}
|
|
855
|
+
function methodNotFound(toolName, knownTools) {
|
|
856
|
+
return new McpError(
|
|
857
|
+
ErrorCode.MethodNotFound,
|
|
858
|
+
`Unknown tool: ${toolName}. Registered tools: ${knownTools.join(", ")}`
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
function assertWithinByteCap(toolName, fieldName, value, maxBytes) {
|
|
862
|
+
const bytes = Buffer.byteLength(value, "utf8");
|
|
863
|
+
if (bytes > maxBytes) {
|
|
864
|
+
throw invalidParams(
|
|
865
|
+
toolName,
|
|
866
|
+
`input field '${fieldName}' exceeds ${maxBytes}-byte cap (got ${bytes} bytes). Trim the input or split across multiple calls.`
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
function buildBaseEvent(args) {
|
|
871
|
+
return {
|
|
872
|
+
schema_version: 1,
|
|
873
|
+
event_id: args.eventId ?? randomUUID(),
|
|
874
|
+
ts: args.now.toISOString(),
|
|
875
|
+
tenant_id: args.session.tenantId,
|
|
876
|
+
operator_id: args.session.operatorId,
|
|
877
|
+
session_id: args.session.sessionId,
|
|
878
|
+
client_id: args.session.clientId,
|
|
879
|
+
mode: args.session.mode,
|
|
880
|
+
tool: args.tool,
|
|
881
|
+
gate_type: args.gateType,
|
|
882
|
+
input_hash: args.inputHash,
|
|
883
|
+
input_excerpt: sanitizeExcerpt(args.inputExcerpt),
|
|
884
|
+
input_size_bytes: args.inputSizeBytes,
|
|
885
|
+
per_model_verdicts: [],
|
|
886
|
+
synthesized_verdict: null,
|
|
887
|
+
consensus_confidence: null,
|
|
888
|
+
per_model_tokens_in: null,
|
|
889
|
+
per_model_tokens_out: null,
|
|
890
|
+
total_cost_usd: null,
|
|
891
|
+
duration_ms: null,
|
|
892
|
+
dev_override: null,
|
|
893
|
+
downstream_outcome: null,
|
|
894
|
+
vo_mcp_version: VO_MCP_VERSION,
|
|
895
|
+
consensus_engine_version: null,
|
|
896
|
+
cache_hit: false
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
function jsonContent(value) {
|
|
900
|
+
return {
|
|
901
|
+
content: [{ type: "text", text: JSON.stringify(value, null, 2) }]
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
function toEventPerModelVerdicts(src) {
|
|
905
|
+
return src.map((v) => {
|
|
906
|
+
const sanitizedExcerpt = sanitizeExcerpt(v.raw_response_excerpt);
|
|
907
|
+
return {
|
|
908
|
+
model: v.model,
|
|
909
|
+
model_id: v.model,
|
|
910
|
+
provider: v.provider,
|
|
911
|
+
verdict: v.verdict,
|
|
912
|
+
confidence: v.confidence,
|
|
913
|
+
duration_ms: v.duration_ms,
|
|
914
|
+
raw_response_excerpt: sanitizedExcerpt,
|
|
915
|
+
// Hash over the sanitized excerpt — engine doesn't yet thread the full
|
|
916
|
+
// raw response. Documented limitation on `raw_response_hash` in types.ts.
|
|
917
|
+
raw_response_hash: sha256Hex(v.raw_response_excerpt),
|
|
918
|
+
reasoning_summary: typeof v.reasoning_excerpt === "string" && v.reasoning_excerpt.length > 0 ? sanitizeExcerpt(v.reasoning_excerpt, 500) : null,
|
|
919
|
+
// Engine doesn't yet emit cited_sources; ship empty array so cloud
|
|
920
|
+
// ingestion doesn't NPE on `Array.isArray()` checks.
|
|
921
|
+
cited_sources: [],
|
|
922
|
+
error: v.error ?? null
|
|
923
|
+
};
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
function toEventSynthesizedVerdict(src) {
|
|
927
|
+
return {
|
|
928
|
+
verdict: src.verdict,
|
|
929
|
+
confidence: src.confidence,
|
|
930
|
+
reasoning_excerpt: sanitizeExcerpt(src.reasoning_excerpt)
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// src/modes/local.ts
|
|
935
|
+
function createLocalMode() {
|
|
936
|
+
return {
|
|
937
|
+
mode: "local",
|
|
938
|
+
tenantId: "local",
|
|
939
|
+
operatorId: "local",
|
|
940
|
+
isCloudConnected() {
|
|
941
|
+
return false;
|
|
942
|
+
}
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// src/tools/check-assertion-strength.ts
|
|
947
|
+
var TOOL_NAME = "vo_check_assertion_strength";
|
|
948
|
+
var GATE_TYPE = "ratchet";
|
|
949
|
+
var MAX_SOURCE_BYTES = 512 * 1024;
|
|
950
|
+
var inputSchema = {
|
|
951
|
+
type: "object",
|
|
952
|
+
properties: {
|
|
953
|
+
source: {
|
|
954
|
+
type: "string",
|
|
955
|
+
description: "Full test file body to analyze."
|
|
956
|
+
},
|
|
957
|
+
file_path: {
|
|
958
|
+
type: "string",
|
|
959
|
+
description: "Optional path hint for nicer findings messages."
|
|
960
|
+
},
|
|
961
|
+
language: {
|
|
962
|
+
type: "string",
|
|
963
|
+
enum: ["ts", "tsx", "js", "jsx", "py"],
|
|
964
|
+
description: "Optional language hint."
|
|
965
|
+
}
|
|
966
|
+
},
|
|
967
|
+
required: ["source"],
|
|
968
|
+
additionalProperties: false
|
|
969
|
+
};
|
|
970
|
+
var description = "Analyzes a test file's assertions and returns a strength score (0-100), an overall verdict (strong/weak/hollow), and per-assertion findings. Hollow patterns flagged include toBeDefined()-only checks, toBeTruthy/toBeFalsy against literals, and tautological self-comparisons. Strong patterns rewarded include toBe, toEqual, toMatchObject, toContain, toHaveBeenCalledWith, toStrictEqual. Use BEFORE accepting a generated or hand-written test, to catch tests that 'pass' without verifying product truth. Open-shell ratchet \u2014 institutional thresholds load from a closed ratchet library; the scoring shape is stable across versions.";
|
|
971
|
+
function isToolInput(v) {
|
|
972
|
+
if (typeof v !== "object" || v === null) return false;
|
|
973
|
+
const o = v;
|
|
974
|
+
if (typeof o["source"] !== "string") return false;
|
|
975
|
+
if (o["file_path"] !== void 0 && typeof o["file_path"] !== "string") return false;
|
|
976
|
+
if (o["language"] !== void 0 && typeof o["language"] !== "string") return false;
|
|
977
|
+
return true;
|
|
978
|
+
}
|
|
979
|
+
async function handleCheckAssertionStrength(deps, rawInput, _signal) {
|
|
980
|
+
if (!isToolInput(rawInput)) {
|
|
981
|
+
throw invalidParams(
|
|
982
|
+
TOOL_NAME,
|
|
983
|
+
"invalid input. Required: { source: string }. Optional: file_path, language."
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
assertWithinByteCap(TOOL_NAME, "source", rawInput.source, MAX_SOURCE_BYTES);
|
|
987
|
+
const key = deps.cache.keyFor(TOOL_NAME, rawInput);
|
|
988
|
+
const excerpt = rawInput.source.slice(0, 300);
|
|
989
|
+
const inputSizeBytes = bytesOf(rawInput.source);
|
|
990
|
+
const cached = deps.cache.get(key);
|
|
991
|
+
if (cached) {
|
|
992
|
+
const envelope2 = {
|
|
993
|
+
...cached.value,
|
|
994
|
+
cache: { hit: true, key }
|
|
995
|
+
};
|
|
996
|
+
deps.events.append({
|
|
997
|
+
...buildBaseEvent({
|
|
998
|
+
tool: TOOL_NAME,
|
|
999
|
+
gateType: GATE_TYPE,
|
|
1000
|
+
inputHash: key,
|
|
1001
|
+
inputExcerpt: excerpt,
|
|
1002
|
+
inputSizeBytes,
|
|
1003
|
+
session: deps.session,
|
|
1004
|
+
now: deps.now()
|
|
1005
|
+
}),
|
|
1006
|
+
cache_hit: true
|
|
1007
|
+
});
|
|
1008
|
+
return jsonContent(envelope2);
|
|
1009
|
+
}
|
|
1010
|
+
const result = await deps.ratchets.checkAssertionStrength({
|
|
1011
|
+
source: rawInput.source,
|
|
1012
|
+
...rawInput.file_path !== void 0 ? { filePath: rawInput.file_path } : {},
|
|
1013
|
+
...rawInput.language !== void 0 ? { language: rawInput.language } : {}
|
|
1014
|
+
});
|
|
1015
|
+
const payload = {
|
|
1016
|
+
score: result.score,
|
|
1017
|
+
max_score: result.maxScore,
|
|
1018
|
+
verdict: result.verdict,
|
|
1019
|
+
per_assertion_findings: result.findings,
|
|
1020
|
+
summary: result.summary
|
|
1021
|
+
};
|
|
1022
|
+
const envelope = {
|
|
1023
|
+
tool: TOOL_NAME,
|
|
1024
|
+
schema_version: 1,
|
|
1025
|
+
cache: { hit: false, key },
|
|
1026
|
+
payload
|
|
1027
|
+
};
|
|
1028
|
+
deps.cache.set(key, envelope);
|
|
1029
|
+
deps.events.append(
|
|
1030
|
+
buildBaseEvent({
|
|
1031
|
+
tool: TOOL_NAME,
|
|
1032
|
+
gateType: GATE_TYPE,
|
|
1033
|
+
inputHash: key,
|
|
1034
|
+
inputExcerpt: excerpt,
|
|
1035
|
+
inputSizeBytes,
|
|
1036
|
+
session: deps.session,
|
|
1037
|
+
now: deps.now()
|
|
1038
|
+
})
|
|
1039
|
+
);
|
|
1040
|
+
return jsonContent(envelope);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// src/tools/architecture-review-kb-prefilter.ts
|
|
1044
|
+
var DEFAULT_STACK = [
|
|
1045
|
+
"node",
|
|
1046
|
+
"node-pnpm-monorepo",
|
|
1047
|
+
"typescript-strict",
|
|
1048
|
+
"firebase-cloud-functions",
|
|
1049
|
+
"react-frontend"
|
|
1050
|
+
];
|
|
1051
|
+
var _cachedEffectiveStack = null;
|
|
1052
|
+
function getEffectiveStack() {
|
|
1053
|
+
if (_cachedEffectiveStack === null) {
|
|
1054
|
+
try {
|
|
1055
|
+
const override = loadTenantOverride().override;
|
|
1056
|
+
_cachedEffectiveStack = resolveStack(override, DEFAULT_STACK);
|
|
1057
|
+
} catch {
|
|
1058
|
+
process.stderr.write(
|
|
1059
|
+
`[vo-mcp] vo-arch-defaults tenant override unreadable; falling back to DEFAULT_STACK
|
|
1060
|
+
`
|
|
1061
|
+
);
|
|
1062
|
+
_cachedEffectiveStack = DEFAULT_STACK;
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
return _cachedEffectiveStack;
|
|
1066
|
+
}
|
|
1067
|
+
var MAX_KB_RULES = 10;
|
|
1068
|
+
var SEVERITY_RANK = {
|
|
1069
|
+
blocker: 0,
|
|
1070
|
+
warning: 1,
|
|
1071
|
+
info: 2
|
|
1072
|
+
};
|
|
1073
|
+
function findRulesForDiff(diff) {
|
|
1074
|
+
try {
|
|
1075
|
+
const parsed = parseUnifiedDiff(diff);
|
|
1076
|
+
const filePaths = parsed.files.map((f) => f.path);
|
|
1077
|
+
if (filePaths.length === 0) return { rules: [], error: null };
|
|
1078
|
+
const result = findApplicableRules({
|
|
1079
|
+
stack: getEffectiveStack(),
|
|
1080
|
+
change_type: "any",
|
|
1081
|
+
file_paths: filePaths,
|
|
1082
|
+
diff_excerpt: diff
|
|
1083
|
+
});
|
|
1084
|
+
return { rules: result.applicable, error: null };
|
|
1085
|
+
} catch (err) {
|
|
1086
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1087
|
+
process.stderr.write(`[vo-mcp] vo-arch-defaults KB unavailable: ${message}
|
|
1088
|
+
`);
|
|
1089
|
+
return { rules: [], error: message };
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
function prepareKBRules(hits) {
|
|
1093
|
+
const sorted = [...hits].sort((a, b) => {
|
|
1094
|
+
const sa = SEVERITY_RANK[a.rule.severity] ?? 99;
|
|
1095
|
+
const sb = SEVERITY_RANK[b.rule.severity] ?? 99;
|
|
1096
|
+
if (sa !== sb) return sa - sb;
|
|
1097
|
+
return a.rule.rule_id.localeCompare(b.rule.rule_id);
|
|
1098
|
+
});
|
|
1099
|
+
const topRules = sorted.slice(0, MAX_KB_RULES);
|
|
1100
|
+
const truncated = Math.max(0, sorted.length - topRules.length);
|
|
1101
|
+
return { topRules, truncated };
|
|
1102
|
+
}
|
|
1103
|
+
function formatRulesForPrompt(hits, truncated, domainLabel = "ARCHITECTURAL") {
|
|
1104
|
+
if (hits.length === 0) return "";
|
|
1105
|
+
const lines = ["", `---${domainLabel} RULES MATCHED FROM KB---`];
|
|
1106
|
+
for (const hit of hits) {
|
|
1107
|
+
const evidenceTag = hit.evidence.length > 0 ? ` (${hit.evidence.length} evidence hit${hit.evidence.length === 1 ? "" : "s"} from mechanical detector)` : "";
|
|
1108
|
+
lines.push(
|
|
1109
|
+
`- [${hit.rule.severity}] ${hit.rule.rule_id}: ${hit.rule.title}${evidenceTag}`
|
|
1110
|
+
);
|
|
1111
|
+
lines.push(` Rationale: ${hit.rule.rationale}`);
|
|
1112
|
+
if (hit.evidence.length > 0) {
|
|
1113
|
+
for (const e of hit.evidence.slice(0, 3)) {
|
|
1114
|
+
const loc = e.line !== void 0 ? `${e.file}:${e.line}` : e.file;
|
|
1115
|
+
lines.push(` - ${loc} \u2014 ${sanitizeExcerpt(e.excerpt)}`);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
if (truncated > 0) {
|
|
1120
|
+
lines.push(
|
|
1121
|
+
`... and ${truncated} more lower-severity rule${truncated === 1 ? "" : "s"} truncated to bound prompt size.`
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
lines.push("---END KB RULES---");
|
|
1125
|
+
return lines.join("\n");
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// src/tools/kb-metadata-prefilter.ts
|
|
1129
|
+
function findRulesByMetadata(opts) {
|
|
1130
|
+
if (opts.category === void 0 && opts.tagsAny === void 0) {
|
|
1131
|
+
return {
|
|
1132
|
+
rules: [],
|
|
1133
|
+
error: "findRulesByMetadata called without category or tagsAny \u2014 refusing to surface every rule"
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
try {
|
|
1137
|
+
const bundled = loadBundledCorpus().rules;
|
|
1138
|
+
const override = loadTenantOverride().override;
|
|
1139
|
+
const merged = mergeCorpusWithOverride(bundled, override);
|
|
1140
|
+
const tagAnyFilter = opts.tagsAny !== void 0 ? new Set(opts.tagsAny) : null;
|
|
1141
|
+
const hits = [];
|
|
1142
|
+
for (const rule of merged.rules) {
|
|
1143
|
+
if (opts.category !== void 0 && rule.category !== opts.category) continue;
|
|
1144
|
+
if (tagAnyFilter !== null) {
|
|
1145
|
+
let tagHit = false;
|
|
1146
|
+
for (const t of rule.tags) {
|
|
1147
|
+
if (tagAnyFilter.has(t)) {
|
|
1148
|
+
tagHit = true;
|
|
1149
|
+
break;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
if (!tagHit) continue;
|
|
1153
|
+
}
|
|
1154
|
+
hits.push({
|
|
1155
|
+
rule,
|
|
1156
|
+
// No applies-context for metadata queries; report all matched=true so
|
|
1157
|
+
// downstream KBLookupResult consumers don't NPE on the field.
|
|
1158
|
+
reason: { stack_matched: true, change_type_matched: true, file_glob_matched: true },
|
|
1159
|
+
evidence: []
|
|
1160
|
+
// No diff → no evidence matchers run.
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
return { rules: hits, error: null };
|
|
1164
|
+
} catch (err) {
|
|
1165
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1166
|
+
process.stderr.write(`[vo-mcp] vo-arch-defaults KB unavailable: ${message}
|
|
1167
|
+
`);
|
|
1168
|
+
return { rules: [], error: message };
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// src/tools/check-hollow-test.ts
|
|
1173
|
+
var TOOL_NAME2 = "vo_check_hollow_test";
|
|
1174
|
+
var GATE_TYPE2 = "plan-review";
|
|
1175
|
+
var MAX_SOURCE_BYTES2 = 512 * 1024;
|
|
1176
|
+
var inputSchema2 = {
|
|
1177
|
+
type: "object",
|
|
1178
|
+
properties: {
|
|
1179
|
+
source: { type: "string", description: "Full test file body to analyze." },
|
|
1180
|
+
file_path: { type: "string", description: "Optional path hint for nicer findings messages." }
|
|
1181
|
+
},
|
|
1182
|
+
required: ["source"],
|
|
1183
|
+
additionalProperties: false
|
|
1184
|
+
};
|
|
1185
|
+
var description2 = "Detects 'hollow' test patterns where a test passes without verifying product behavior \u2014 e.g. mocks that return whatever the test expects, assertions on local fixtures rather than real outputs, tests that exercise no real code path. Returns a consensus verdict (pass/fail/uncertain) with per-model reasoning. Complements vo_check_assertion_strength (static ratchet); this tool uses LLM judgment for patterns static analysis can't reach. Falls back to 'unimplemented' when no engine credentials are configured; the call is always logged.";
|
|
1186
|
+
function isToolInput2(v) {
|
|
1187
|
+
if (typeof v !== "object" || v === null) return false;
|
|
1188
|
+
const o = v;
|
|
1189
|
+
if (typeof o["source"] !== "string") return false;
|
|
1190
|
+
if (o["file_path"] !== void 0 && typeof o["file_path"] !== "string") return false;
|
|
1191
|
+
return true;
|
|
1192
|
+
}
|
|
1193
|
+
function buildPrompt(input, kbRules, kbTruncated) {
|
|
1194
|
+
const pathHint = input.file_path !== void 0 ? `File: ${input.file_path}
|
|
1195
|
+
` : "";
|
|
1196
|
+
const rulesBlock = formatRulesForPrompt(kbRules, kbTruncated, "TESTING");
|
|
1197
|
+
return `${pathHint}Analyze the following test source for HOLLOW patterns \u2014 tests that pass without verifying product truth. Hollow patterns include: assertions on locally-defined fixtures (not real product output), mocks that return the expected value verbatim, toBeDefined()/toBeTruthy() against literal values, tests that exercise NO real product code path, tautological self-comparisons (expect(x).toBe(x)).
|
|
1198
|
+
|
|
1199
|
+
Respond with verdict 'fail' if the test is HOLLOW (does not verify product truth), 'pass' if the test is genuinely verifying product behavior, or 'uncertain' if the determination cannot be made from the source alone.${rulesBlock}
|
|
1200
|
+
|
|
1201
|
+
---TEST SOURCE---
|
|
1202
|
+
${input.source}
|
|
1203
|
+
---END---`;
|
|
1204
|
+
}
|
|
1205
|
+
async function handleCheckHollowTest(deps, rawInput, signal) {
|
|
1206
|
+
if (!isToolInput2(rawInput)) {
|
|
1207
|
+
throw invalidParams(TOOL_NAME2, "invalid input. Required: { source: string }.");
|
|
1208
|
+
}
|
|
1209
|
+
assertWithinByteCap(TOOL_NAME2, "source", rawInput.source, MAX_SOURCE_BYTES2);
|
|
1210
|
+
const key = deps.cache.keyFor(TOOL_NAME2, rawInput);
|
|
1211
|
+
const excerpt = rawInput.source.slice(0, 300);
|
|
1212
|
+
const inputSizeBytes = bytesOf(rawInput.source);
|
|
1213
|
+
const cached = deps.cache.get(key);
|
|
1214
|
+
if (cached !== null) {
|
|
1215
|
+
const replayEvent = {
|
|
1216
|
+
...buildBaseEvent({
|
|
1217
|
+
tool: TOOL_NAME2,
|
|
1218
|
+
gateType: GATE_TYPE2,
|
|
1219
|
+
inputHash: key,
|
|
1220
|
+
inputExcerpt: excerpt,
|
|
1221
|
+
inputSizeBytes,
|
|
1222
|
+
session: deps.session,
|
|
1223
|
+
now: deps.now()
|
|
1224
|
+
}),
|
|
1225
|
+
per_model_verdicts: cached.value.payload.per_model_verdicts,
|
|
1226
|
+
synthesized_verdict: cached.value.payload.synthesized_verdict ?? null,
|
|
1227
|
+
consensus_confidence: cached.value.payload.synthesized_verdict?.confidence ?? null,
|
|
1228
|
+
consensus_engine_version: cached.value.payload.engine_version ?? null,
|
|
1229
|
+
cache_hit: true
|
|
1230
|
+
};
|
|
1231
|
+
deps.events.append(replayEvent);
|
|
1232
|
+
return jsonContent({ ...cached.value, cache: { hit: true, key } });
|
|
1233
|
+
}
|
|
1234
|
+
const kbResult = findRulesByMetadata({ category: "testing" });
|
|
1235
|
+
const { topRules, truncated: kbTruncated } = prepareKBRules(kbResult.rules);
|
|
1236
|
+
const baseEvent = buildBaseEvent({
|
|
1237
|
+
tool: TOOL_NAME2,
|
|
1238
|
+
gateType: GATE_TYPE2,
|
|
1239
|
+
inputHash: key,
|
|
1240
|
+
inputExcerpt: excerpt,
|
|
1241
|
+
inputSizeBytes,
|
|
1242
|
+
session: deps.session,
|
|
1243
|
+
now: deps.now()
|
|
1244
|
+
});
|
|
1245
|
+
let engineResult;
|
|
1246
|
+
let engineThrew = false;
|
|
1247
|
+
try {
|
|
1248
|
+
engineResult = await deps.consensus.run({
|
|
1249
|
+
gate_type: GATE_TYPE2,
|
|
1250
|
+
prompt: buildPrompt(rawInput, topRules, kbTruncated),
|
|
1251
|
+
...signal !== void 0 ? { signal } : {}
|
|
1252
|
+
});
|
|
1253
|
+
} catch (err) {
|
|
1254
|
+
engineThrew = true;
|
|
1255
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1256
|
+
engineResult = { ok: false, reason: `engine-threw: ${message}` };
|
|
1257
|
+
}
|
|
1258
|
+
if (!engineResult.ok && engineResult.reason === "cancelled") {
|
|
1259
|
+
deps.events.append({
|
|
1260
|
+
...baseEvent,
|
|
1261
|
+
downstream_outcome: {
|
|
1262
|
+
status: "cancelled",
|
|
1263
|
+
notes: "mcp notifications/cancelled \u2014 handler aborted via SDK signal"
|
|
1264
|
+
}
|
|
1265
|
+
});
|
|
1266
|
+
const e = new Error("Request cancelled by client (notifications/cancelled)");
|
|
1267
|
+
e.name = "AbortError";
|
|
1268
|
+
throw e;
|
|
1269
|
+
}
|
|
1270
|
+
if (!engineResult.ok) {
|
|
1271
|
+
const payload2 = {
|
|
1272
|
+
verdict: "unimplemented",
|
|
1273
|
+
reason: engineResult.reason,
|
|
1274
|
+
per_model_verdicts: [],
|
|
1275
|
+
gate_type: GATE_TYPE2,
|
|
1276
|
+
...engineThrew ? { degraded_mode: true } : {},
|
|
1277
|
+
...kbResult.error !== null ? { kb_unavailable: true } : {},
|
|
1278
|
+
...kbTruncated > 0 ? { kb_rules_truncated: kbTruncated } : {}
|
|
1279
|
+
};
|
|
1280
|
+
const envelope2 = {
|
|
1281
|
+
tool: TOOL_NAME2,
|
|
1282
|
+
schema_version: 1,
|
|
1283
|
+
cache: { hit: false, key },
|
|
1284
|
+
payload: payload2
|
|
1285
|
+
};
|
|
1286
|
+
deps.events.append(baseEvent);
|
|
1287
|
+
return jsonContent(envelope2);
|
|
1288
|
+
}
|
|
1289
|
+
const perModelForEvent = toEventPerModelVerdicts(engineResult.per_model_verdicts);
|
|
1290
|
+
const synthForEvent = toEventSynthesizedVerdict(engineResult.synthesized_verdict);
|
|
1291
|
+
const enrichedEvent = {
|
|
1292
|
+
...baseEvent,
|
|
1293
|
+
per_model_verdicts: perModelForEvent,
|
|
1294
|
+
synthesized_verdict: synthForEvent,
|
|
1295
|
+
consensus_confidence: engineResult.synthesized_verdict.confidence,
|
|
1296
|
+
duration_ms: engineResult.duration_ms,
|
|
1297
|
+
consensus_engine_version: engineResult.engine_version
|
|
1298
|
+
};
|
|
1299
|
+
const payload = {
|
|
1300
|
+
verdict: engineResult.synthesized_verdict.verdict,
|
|
1301
|
+
reason: engineResult.synthesized_verdict.dissent_summary ?? `panel agreed (${engineResult.per_model_verdicts.length} models, engine=${engineResult.engine_version})`,
|
|
1302
|
+
per_model_verdicts: perModelForEvent,
|
|
1303
|
+
synthesized_verdict: synthForEvent,
|
|
1304
|
+
engine_version: engineResult.engine_version,
|
|
1305
|
+
degraded: engineResult.degraded,
|
|
1306
|
+
gate_type: GATE_TYPE2,
|
|
1307
|
+
...kbResult.error !== null ? { kb_unavailable: true } : {},
|
|
1308
|
+
...kbTruncated > 0 ? { kb_rules_truncated: kbTruncated } : {}
|
|
1309
|
+
};
|
|
1310
|
+
const envelope = {
|
|
1311
|
+
tool: TOOL_NAME2,
|
|
1312
|
+
schema_version: 1,
|
|
1313
|
+
cache: { hit: false, key },
|
|
1314
|
+
payload
|
|
1315
|
+
};
|
|
1316
|
+
deps.cache.set(key, envelope);
|
|
1317
|
+
deps.events.append(enrichedEvent);
|
|
1318
|
+
return jsonContent(envelope);
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
// src/tools/verify-answer.ts
|
|
1322
|
+
var TOOL_NAME3 = "vo_verify_answer";
|
|
1323
|
+
var SHALLOW_GATE = "mid-exec-verify";
|
|
1324
|
+
var DEEP_GATE = "final-deep-verify";
|
|
1325
|
+
var MAX_VALUE_BYTES = 256 * 1024;
|
|
1326
|
+
var inputSchema3 = {
|
|
1327
|
+
type: "object",
|
|
1328
|
+
properties: {
|
|
1329
|
+
expected: {
|
|
1330
|
+
description: "The expected value (string, number, or structured JSON)."
|
|
1331
|
+
},
|
|
1332
|
+
observed: {
|
|
1333
|
+
description: "The observed value to compare against expected."
|
|
1334
|
+
},
|
|
1335
|
+
domain: {
|
|
1336
|
+
type: "string",
|
|
1337
|
+
description: "Domain hint for semantic comparison (e.g. tax, code, prose)."
|
|
1338
|
+
},
|
|
1339
|
+
deep: {
|
|
1340
|
+
type: "boolean",
|
|
1341
|
+
description: "When true, routes to the final-deep-verify gate (deliberation synthesizer, 4-model expensive panel, max 2 rounds). Default false \u2192 mid-exec-verify gate (weighted, 3-model cheap panel)."
|
|
1342
|
+
}
|
|
1343
|
+
},
|
|
1344
|
+
required: ["expected", "observed"],
|
|
1345
|
+
additionalProperties: false
|
|
1346
|
+
};
|
|
1347
|
+
var description3 = 'Compares an expected value against an observed value via consensus fan-out and returns whether they are semantically equivalent (handles formatting drift, unit conversions, paraphrase). Default routes to the mid-exec-verify gate (cheap 3-model panel); pass deep:true to escalate to final-deep-verify (4-model deliberation). Returns per-model verdicts and a synthesized pass/fail/uncertain. Falls back to "unimplemented" when engine credentials are absent; the call is always logged.';
|
|
1348
|
+
function isToolInput3(v) {
|
|
1349
|
+
if (typeof v !== "object" || v === null) return false;
|
|
1350
|
+
const o = v;
|
|
1351
|
+
if (!("expected" in o) || !("observed" in o)) return false;
|
|
1352
|
+
if (o["domain"] !== void 0 && typeof o["domain"] !== "string") return false;
|
|
1353
|
+
if (o["deep"] !== void 0 && typeof o["deep"] !== "boolean") return false;
|
|
1354
|
+
return true;
|
|
1355
|
+
}
|
|
1356
|
+
function safeStringify(v) {
|
|
1357
|
+
try {
|
|
1358
|
+
return JSON.stringify(v);
|
|
1359
|
+
} catch {
|
|
1360
|
+
return String(v);
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
function buildPrompt2(input, kbRules, kbTruncated) {
|
|
1364
|
+
const domain = input.domain !== void 0 ? ` (domain: ${input.domain})` : "";
|
|
1365
|
+
const rulesBlock = formatRulesForPrompt(kbRules, kbTruncated, "SECURITY");
|
|
1366
|
+
return `Judge whether the OBSERVED value is semantically equivalent to the EXPECTED value${domain}. Treat formatting drift (whitespace, casing, currency symbols, unit notation) and paraphrase as equivalent; treat numerically different values, wrong units, or wrong factual content as NOT equivalent.
|
|
1367
|
+
|
|
1368
|
+
Respond with verdict 'pass' when equivalent, 'fail' when not equivalent, or 'uncertain' when the determination requires information not present in the inputs.${rulesBlock}
|
|
1369
|
+
|
|
1370
|
+
---EXPECTED---
|
|
1371
|
+
${safeStringify(input.expected)}
|
|
1372
|
+
---OBSERVED---
|
|
1373
|
+
${safeStringify(input.observed)}
|
|
1374
|
+
---END---`;
|
|
1375
|
+
}
|
|
1376
|
+
async function handleVerifyAnswer(deps, rawInput, signal) {
|
|
1377
|
+
if (!isToolInput3(rawInput)) {
|
|
1378
|
+
throw invalidParams(TOOL_NAME3, "invalid input. Required: { expected, observed }.");
|
|
1379
|
+
}
|
|
1380
|
+
const expectedStr = safeStringify(rawInput.expected);
|
|
1381
|
+
const observedStr = safeStringify(rawInput.observed);
|
|
1382
|
+
assertWithinByteCap(TOOL_NAME3, "expected", expectedStr, MAX_VALUE_BYTES);
|
|
1383
|
+
assertWithinByteCap(TOOL_NAME3, "observed", observedStr, MAX_VALUE_BYTES);
|
|
1384
|
+
const gateType = rawInput.deep === true ? DEEP_GATE : SHALLOW_GATE;
|
|
1385
|
+
const cacheInput = {
|
|
1386
|
+
expected: rawInput.expected,
|
|
1387
|
+
observed: rawInput.observed,
|
|
1388
|
+
...rawInput.domain !== void 0 ? { domain: rawInput.domain } : {},
|
|
1389
|
+
gate_type: gateType
|
|
1390
|
+
};
|
|
1391
|
+
const key = deps.cache.keyFor(TOOL_NAME3, cacheInput);
|
|
1392
|
+
const excerpt = safeStringify(rawInput).slice(0, 300);
|
|
1393
|
+
const inputSizeBytes = bytesOf(expectedStr) + bytesOf(observedStr);
|
|
1394
|
+
const isDeep = gateType === DEEP_GATE;
|
|
1395
|
+
const kbResult = isDeep ? findRulesByMetadata({ category: "security" }) : { rules: [], error: null };
|
|
1396
|
+
const { topRules, truncated: kbTruncated } = prepareKBRules(kbResult.rules);
|
|
1397
|
+
const cached = deps.cache.get(key);
|
|
1398
|
+
if (cached !== null) {
|
|
1399
|
+
const replayEvent = {
|
|
1400
|
+
...buildBaseEvent({
|
|
1401
|
+
tool: TOOL_NAME3,
|
|
1402
|
+
gateType,
|
|
1403
|
+
inputHash: key,
|
|
1404
|
+
inputExcerpt: excerpt,
|
|
1405
|
+
inputSizeBytes,
|
|
1406
|
+
session: deps.session,
|
|
1407
|
+
now: deps.now()
|
|
1408
|
+
}),
|
|
1409
|
+
per_model_verdicts: cached.value.payload.per_model_verdicts,
|
|
1410
|
+
synthesized_verdict: cached.value.payload.synthesized_verdict ?? null,
|
|
1411
|
+
consensus_confidence: cached.value.payload.synthesized_verdict?.confidence ?? null,
|
|
1412
|
+
consensus_engine_version: cached.value.payload.engine_version ?? null,
|
|
1413
|
+
cache_hit: true
|
|
1414
|
+
};
|
|
1415
|
+
deps.events.append(replayEvent);
|
|
1416
|
+
return jsonContent({ ...cached.value, cache: { hit: true, key } });
|
|
1417
|
+
}
|
|
1418
|
+
const baseEvent = buildBaseEvent({
|
|
1419
|
+
tool: TOOL_NAME3,
|
|
1420
|
+
gateType,
|
|
1421
|
+
inputHash: key,
|
|
1422
|
+
inputExcerpt: excerpt,
|
|
1423
|
+
inputSizeBytes,
|
|
1424
|
+
session: deps.session,
|
|
1425
|
+
now: deps.now()
|
|
1426
|
+
});
|
|
1427
|
+
let engineResult;
|
|
1428
|
+
let engineThrew = false;
|
|
1429
|
+
try {
|
|
1430
|
+
engineResult = await deps.consensus.run({
|
|
1431
|
+
gate_type: gateType,
|
|
1432
|
+
prompt: buildPrompt2(rawInput, topRules, kbTruncated),
|
|
1433
|
+
...signal !== void 0 ? { signal } : {}
|
|
1434
|
+
});
|
|
1435
|
+
} catch (err) {
|
|
1436
|
+
engineThrew = true;
|
|
1437
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1438
|
+
engineResult = { ok: false, reason: `engine-threw: ${message}` };
|
|
1439
|
+
}
|
|
1440
|
+
if (!engineResult.ok && engineResult.reason === "cancelled") {
|
|
1441
|
+
deps.events.append({
|
|
1442
|
+
...baseEvent,
|
|
1443
|
+
downstream_outcome: {
|
|
1444
|
+
status: "cancelled",
|
|
1445
|
+
notes: "mcp notifications/cancelled \u2014 handler aborted via SDK signal"
|
|
1446
|
+
}
|
|
1447
|
+
});
|
|
1448
|
+
const e = new Error("Request cancelled by client (notifications/cancelled)");
|
|
1449
|
+
e.name = "AbortError";
|
|
1450
|
+
throw e;
|
|
1451
|
+
}
|
|
1452
|
+
if (!engineResult.ok) {
|
|
1453
|
+
const payload2 = {
|
|
1454
|
+
verdict: "unimplemented",
|
|
1455
|
+
reason: engineResult.reason,
|
|
1456
|
+
per_model_verdicts: [],
|
|
1457
|
+
gate_type: gateType,
|
|
1458
|
+
...engineThrew ? { degraded_mode: true } : {},
|
|
1459
|
+
...kbResult.error !== null ? { kb_unavailable: true } : {},
|
|
1460
|
+
...kbTruncated > 0 ? { kb_rules_truncated: kbTruncated } : {}
|
|
1461
|
+
};
|
|
1462
|
+
const envelope2 = {
|
|
1463
|
+
tool: TOOL_NAME3,
|
|
1464
|
+
schema_version: 1,
|
|
1465
|
+
cache: { hit: false, key },
|
|
1466
|
+
payload: payload2
|
|
1467
|
+
};
|
|
1468
|
+
deps.events.append(baseEvent);
|
|
1469
|
+
return jsonContent(envelope2);
|
|
1470
|
+
}
|
|
1471
|
+
const perModelForEvent = toEventPerModelVerdicts(engineResult.per_model_verdicts);
|
|
1472
|
+
const synthForEvent = toEventSynthesizedVerdict(engineResult.synthesized_verdict);
|
|
1473
|
+
const enrichedEvent = {
|
|
1474
|
+
...baseEvent,
|
|
1475
|
+
per_model_verdicts: perModelForEvent,
|
|
1476
|
+
synthesized_verdict: synthForEvent,
|
|
1477
|
+
consensus_confidence: engineResult.synthesized_verdict.confidence,
|
|
1478
|
+
duration_ms: engineResult.duration_ms,
|
|
1479
|
+
consensus_engine_version: engineResult.engine_version
|
|
1480
|
+
};
|
|
1481
|
+
const payload = {
|
|
1482
|
+
verdict: engineResult.synthesized_verdict.verdict,
|
|
1483
|
+
reason: engineResult.synthesized_verdict.dissent_summary ?? `panel agreed (${engineResult.per_model_verdicts.length} models, engine=${engineResult.engine_version})`,
|
|
1484
|
+
per_model_verdicts: perModelForEvent,
|
|
1485
|
+
synthesized_verdict: synthForEvent,
|
|
1486
|
+
engine_version: engineResult.engine_version,
|
|
1487
|
+
degraded: engineResult.degraded,
|
|
1488
|
+
gate_type: gateType,
|
|
1489
|
+
...kbResult.error !== null ? { kb_unavailable: true } : {},
|
|
1490
|
+
...kbTruncated > 0 ? { kb_rules_truncated: kbTruncated } : {}
|
|
1491
|
+
};
|
|
1492
|
+
const envelope = {
|
|
1493
|
+
tool: TOOL_NAME3,
|
|
1494
|
+
schema_version: 1,
|
|
1495
|
+
cache: { hit: false, key },
|
|
1496
|
+
payload
|
|
1497
|
+
};
|
|
1498
|
+
deps.cache.set(key, envelope);
|
|
1499
|
+
deps.events.append(enrichedEvent);
|
|
1500
|
+
return jsonContent(envelope);
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
// src/consensus/gate-types.ts
|
|
1504
|
+
var LEGACY_GATE_TYPES = [
|
|
1505
|
+
"test_assertion",
|
|
1506
|
+
"architecture_review",
|
|
1507
|
+
"factual_grounding",
|
|
1508
|
+
"verify_answer",
|
|
1509
|
+
"other"
|
|
1510
|
+
];
|
|
1511
|
+
var KNOWN_GATE_TYPES = [
|
|
1512
|
+
"plan-review",
|
|
1513
|
+
"mid-exec-verify",
|
|
1514
|
+
"final-deep-verify",
|
|
1515
|
+
"architecture-review"
|
|
1516
|
+
];
|
|
1517
|
+
var ALL_RECOGNIZED_GATE_TYPES = [
|
|
1518
|
+
...LEGACY_GATE_TYPES,
|
|
1519
|
+
...KNOWN_GATE_TYPES
|
|
1520
|
+
];
|
|
1521
|
+
|
|
1522
|
+
// src/tools/consensus-judgment.ts
|
|
1523
|
+
var TOOL_NAME4 = "vo_consensus_judgment";
|
|
1524
|
+
var MAX_PROMPT_BYTES = 64 * 1024;
|
|
1525
|
+
var MAX_CONTEXT_BYTES = 256 * 1024;
|
|
1526
|
+
var ALL_GATE_TYPES = ALL_RECOGNIZED_GATE_TYPES;
|
|
1527
|
+
var DEFAULT_GATE_TYPE = "plan-review";
|
|
1528
|
+
var inputSchema4 = {
|
|
1529
|
+
type: "object",
|
|
1530
|
+
properties: {
|
|
1531
|
+
prompt: {
|
|
1532
|
+
type: "string",
|
|
1533
|
+
description: "The judgment prompt \u2014 what you want multiple models to assess."
|
|
1534
|
+
},
|
|
1535
|
+
gate_type: {
|
|
1536
|
+
type: "string",
|
|
1537
|
+
enum: [...ALL_GATE_TYPES],
|
|
1538
|
+
description: "Which gate this judgment serves. Phase 2 kebab-case names (plan-review, mid-exec-verify, final-deep-verify, architecture-review) drive the engine\u2019s synthesizer choice. Phase 1 underscore names back-compat to strict-consensus. Default: plan-review."
|
|
1539
|
+
},
|
|
1540
|
+
context: {
|
|
1541
|
+
type: "object",
|
|
1542
|
+
description: "Tool-specific context (e.g. source file, diff, observed value).",
|
|
1543
|
+
additionalProperties: true
|
|
1544
|
+
}
|
|
1545
|
+
},
|
|
1546
|
+
required: ["prompt"],
|
|
1547
|
+
additionalProperties: false
|
|
1548
|
+
};
|
|
1549
|
+
var description4 = "Submit a prompt to multiple LLMs and return a synthesized consensus verdict with per-model verdicts visible. Use for high-stakes judgment calls where a single model's verdict is too risky (architecture reviews, weak-vs-strong-assertion calls, factual grounding against authoritative sources). Returns a real synthesized verdict when the closed consensus engine is wired in with credentials; falls back to 'unimplemented' otherwise. The call IS always logged for dataset and unit-economics audit (V1 launch gate #7).";
|
|
1550
|
+
function isToolInput4(v) {
|
|
1551
|
+
if (typeof v !== "object" || v === null) return false;
|
|
1552
|
+
const o = v;
|
|
1553
|
+
if (typeof o["prompt"] !== "string") return false;
|
|
1554
|
+
if (o["gate_type"] !== void 0 && typeof o["gate_type"] !== "string") return false;
|
|
1555
|
+
return true;
|
|
1556
|
+
}
|
|
1557
|
+
function isAcceptedGateType(v) {
|
|
1558
|
+
return ALL_GATE_TYPES.includes(v);
|
|
1559
|
+
}
|
|
1560
|
+
async function handleConsensusJudgment(deps, rawInput, signal) {
|
|
1561
|
+
if (!isToolInput4(rawInput)) {
|
|
1562
|
+
throw invalidParams(
|
|
1563
|
+
TOOL_NAME4,
|
|
1564
|
+
"invalid input. Required: { prompt: string, gate_type?: string }."
|
|
1565
|
+
);
|
|
1566
|
+
}
|
|
1567
|
+
assertWithinByteCap(TOOL_NAME4, "prompt", rawInput.prompt, MAX_PROMPT_BYTES);
|
|
1568
|
+
let contextStrForSize = "";
|
|
1569
|
+
if (rawInput.context !== void 0) {
|
|
1570
|
+
try {
|
|
1571
|
+
contextStrForSize = JSON.stringify(rawInput.context);
|
|
1572
|
+
} catch {
|
|
1573
|
+
throw invalidParams(TOOL_NAME4, "input field 'context' is not JSON-serializable.");
|
|
1574
|
+
}
|
|
1575
|
+
assertWithinByteCap(TOOL_NAME4, "context", contextStrForSize, MAX_CONTEXT_BYTES);
|
|
1576
|
+
}
|
|
1577
|
+
const rawGate = rawInput.gate_type ?? DEFAULT_GATE_TYPE;
|
|
1578
|
+
const gateType = isAcceptedGateType(rawGate) ? rawGate : DEFAULT_GATE_TYPE;
|
|
1579
|
+
const inputSizeBytes = bytesOf(rawInput.prompt) + bytesOf(contextStrForSize);
|
|
1580
|
+
const cacheInput = {
|
|
1581
|
+
prompt: rawInput.prompt,
|
|
1582
|
+
gate_type: gateType,
|
|
1583
|
+
...rawInput.context !== void 0 ? { context: rawInput.context } : {}
|
|
1584
|
+
};
|
|
1585
|
+
const key = deps.cache.keyFor(TOOL_NAME4, cacheInput);
|
|
1586
|
+
const excerpt = rawInput.prompt.slice(0, 300);
|
|
1587
|
+
const cached = deps.cache.get(key);
|
|
1588
|
+
if (cached !== null) {
|
|
1589
|
+
const replayEvent = buildBaseEvent({
|
|
1590
|
+
tool: TOOL_NAME4,
|
|
1591
|
+
gateType,
|
|
1592
|
+
inputHash: key,
|
|
1593
|
+
inputExcerpt: excerpt,
|
|
1594
|
+
inputSizeBytes,
|
|
1595
|
+
session: deps.session,
|
|
1596
|
+
now: deps.now()
|
|
1597
|
+
});
|
|
1598
|
+
const enrichedReplay = {
|
|
1599
|
+
...replayEvent,
|
|
1600
|
+
per_model_verdicts: cached.value.payload.per_model_verdicts,
|
|
1601
|
+
synthesized_verdict: cached.value.payload.synthesized_verdict ?? null,
|
|
1602
|
+
consensus_confidence: cached.value.payload.synthesized_verdict?.confidence ?? null,
|
|
1603
|
+
consensus_engine_version: cached.value.payload.engine_version ?? null,
|
|
1604
|
+
cache_hit: true
|
|
1605
|
+
};
|
|
1606
|
+
deps.events.append(enrichedReplay);
|
|
1607
|
+
const replayEnvelope = {
|
|
1608
|
+
...cached.value,
|
|
1609
|
+
cache: { hit: true, key }
|
|
1610
|
+
};
|
|
1611
|
+
return jsonContent(replayEnvelope);
|
|
1612
|
+
}
|
|
1613
|
+
const baseEvent = buildBaseEvent({
|
|
1614
|
+
tool: TOOL_NAME4,
|
|
1615
|
+
gateType,
|
|
1616
|
+
inputHash: key,
|
|
1617
|
+
inputExcerpt: excerpt,
|
|
1618
|
+
inputSizeBytes,
|
|
1619
|
+
session: deps.session,
|
|
1620
|
+
now: deps.now()
|
|
1621
|
+
});
|
|
1622
|
+
let engineResult;
|
|
1623
|
+
let engineThrew = false;
|
|
1624
|
+
try {
|
|
1625
|
+
engineResult = await deps.consensus.run({
|
|
1626
|
+
gate_type: gateType,
|
|
1627
|
+
prompt: rawInput.prompt,
|
|
1628
|
+
...rawInput.context !== void 0 ? { caller_context: rawInput.context } : {},
|
|
1629
|
+
...signal !== void 0 ? { signal } : {}
|
|
1630
|
+
});
|
|
1631
|
+
} catch (err) {
|
|
1632
|
+
engineThrew = true;
|
|
1633
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1634
|
+
engineResult = { ok: false, reason: `engine-threw: ${message}` };
|
|
1635
|
+
}
|
|
1636
|
+
if (!engineResult.ok && engineResult.reason === "cancelled") {
|
|
1637
|
+
deps.events.append({
|
|
1638
|
+
...baseEvent,
|
|
1639
|
+
downstream_outcome: {
|
|
1640
|
+
status: "cancelled",
|
|
1641
|
+
notes: "mcp notifications/cancelled \u2014 handler aborted via SDK signal"
|
|
1642
|
+
}
|
|
1643
|
+
});
|
|
1644
|
+
const e = new Error("Request cancelled by client (notifications/cancelled)");
|
|
1645
|
+
e.name = "AbortError";
|
|
1646
|
+
throw e;
|
|
1647
|
+
}
|
|
1648
|
+
if (!engineResult.ok) {
|
|
1649
|
+
const payload2 = {
|
|
1650
|
+
verdict: "unimplemented",
|
|
1651
|
+
reason: engineResult.reason,
|
|
1652
|
+
per_model_verdicts: [],
|
|
1653
|
+
gate_type: gateType,
|
|
1654
|
+
...engineThrew ? { degraded_mode: true } : {}
|
|
1655
|
+
};
|
|
1656
|
+
const envelope2 = {
|
|
1657
|
+
tool: TOOL_NAME4,
|
|
1658
|
+
schema_version: 1,
|
|
1659
|
+
cache: { hit: false, key },
|
|
1660
|
+
payload: payload2
|
|
1661
|
+
};
|
|
1662
|
+
deps.events.append(baseEvent);
|
|
1663
|
+
return jsonContent(envelope2);
|
|
1664
|
+
}
|
|
1665
|
+
const perModelForEvent = toEventPerModelVerdicts(engineResult.per_model_verdicts);
|
|
1666
|
+
const synthForEvent = toEventSynthesizedVerdict(engineResult.synthesized_verdict);
|
|
1667
|
+
const enrichedEvent = {
|
|
1668
|
+
...baseEvent,
|
|
1669
|
+
consensus_confidence: engineResult.synthesized_verdict.confidence,
|
|
1670
|
+
duration_ms: engineResult.duration_ms,
|
|
1671
|
+
consensus_engine_version: engineResult.engine_version,
|
|
1672
|
+
per_model_verdicts: perModelForEvent,
|
|
1673
|
+
synthesized_verdict: synthForEvent
|
|
1674
|
+
};
|
|
1675
|
+
const payload = {
|
|
1676
|
+
verdict: engineResult.synthesized_verdict.verdict,
|
|
1677
|
+
reason: engineResult.synthesized_verdict.dissent_summary ?? `panel agreed (${engineResult.per_model_verdicts.length} models, engine=${engineResult.engine_version})`,
|
|
1678
|
+
per_model_verdicts: perModelForEvent,
|
|
1679
|
+
synthesized_verdict: synthForEvent,
|
|
1680
|
+
engine_version: engineResult.engine_version,
|
|
1681
|
+
degraded: engineResult.degraded,
|
|
1682
|
+
gate_type: gateType
|
|
1683
|
+
};
|
|
1684
|
+
const envelope = {
|
|
1685
|
+
tool: TOOL_NAME4,
|
|
1686
|
+
schema_version: 1,
|
|
1687
|
+
cache: { hit: false, key },
|
|
1688
|
+
payload
|
|
1689
|
+
};
|
|
1690
|
+
deps.cache.set(key, envelope);
|
|
1691
|
+
deps.events.append(enrichedEvent);
|
|
1692
|
+
return jsonContent(envelope);
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
// src/tools/architecture-review.ts
|
|
1696
|
+
var TOOL_NAME5 = "vo_architecture_review";
|
|
1697
|
+
var GATE_TYPE3 = "architecture-review";
|
|
1698
|
+
var MAX_DIFF_BYTES = 1024 * 1024;
|
|
1699
|
+
var MAX_SUMMARY_BYTES = 16 * 1024;
|
|
1700
|
+
var ACCEPTED_CHANGE_TYPES = [
|
|
1701
|
+
"refactor",
|
|
1702
|
+
"new_feature",
|
|
1703
|
+
"bug_fix",
|
|
1704
|
+
"dep_update",
|
|
1705
|
+
"config",
|
|
1706
|
+
"docs",
|
|
1707
|
+
"other"
|
|
1708
|
+
];
|
|
1709
|
+
var inputSchema5 = {
|
|
1710
|
+
type: "object",
|
|
1711
|
+
properties: {
|
|
1712
|
+
diff: {
|
|
1713
|
+
type: "string",
|
|
1714
|
+
description: "Unified diff of the proposed change."
|
|
1715
|
+
},
|
|
1716
|
+
change_type: {
|
|
1717
|
+
type: "string",
|
|
1718
|
+
enum: [...ACCEPTED_CHANGE_TYPES],
|
|
1719
|
+
description: "Classification of the change."
|
|
1720
|
+
},
|
|
1721
|
+
summary: {
|
|
1722
|
+
type: "string",
|
|
1723
|
+
description: "One-paragraph summary of intent / motivation."
|
|
1724
|
+
}
|
|
1725
|
+
},
|
|
1726
|
+
required: ["diff", "change_type"],
|
|
1727
|
+
additionalProperties: false
|
|
1728
|
+
};
|
|
1729
|
+
var description5 = "Senior-architect review of a proposed diff against the project's architectural defaults \u2014 naming conventions, boundary discipline, error-handling shape, test honesty. Runs the architecture-review gate (human-tiebreak synthesizer, expensive 3-model panel). When the panel disagrees, the response includes an `escalation` field recommending operator review (the verdict still resolves via majority-vote \u2014 the engine does NOT block). Falls back to 'unimplemented' when engine credentials are absent; the call is always logged.";
|
|
1730
|
+
function isToolInput5(v) {
|
|
1731
|
+
if (typeof v !== "object" || v === null) return false;
|
|
1732
|
+
const o = v;
|
|
1733
|
+
if (typeof o["diff"] !== "string" || typeof o["change_type"] !== "string") return false;
|
|
1734
|
+
if (o["summary"] !== void 0 && typeof o["summary"] !== "string") return false;
|
|
1735
|
+
return true;
|
|
1736
|
+
}
|
|
1737
|
+
function isAcceptedChangeType(v) {
|
|
1738
|
+
return ACCEPTED_CHANGE_TYPES.includes(v);
|
|
1739
|
+
}
|
|
1740
|
+
function buildPrompt3(input, changeType, rules, truncated) {
|
|
1741
|
+
const summary = input.summary !== void 0 ? `Summary: ${input.summary}
|
|
1742
|
+
` : "";
|
|
1743
|
+
const rulesBlock = formatRulesForPrompt(rules, truncated);
|
|
1744
|
+
return `Senior architecture review (change type: ${changeType}).
|
|
1745
|
+
${summary}Review the following diff against architectural defaults: naming conventions, boundary discipline (no leaky abstractions across packages), error-handling shape (typed errors, no swallowed throws), test honesty (assertions verify product truth \u2014 not mocks-of-mocks).
|
|
1746
|
+
|
|
1747
|
+
Respond with verdict 'pass' if the change is sound, 'fail' if it introduces architectural regressions or breaks defaults, or 'uncertain' if context outside the diff is needed.${rulesBlock}
|
|
1748
|
+
|
|
1749
|
+
---DIFF---
|
|
1750
|
+
${input.diff}
|
|
1751
|
+
---END---`;
|
|
1752
|
+
}
|
|
1753
|
+
async function handleArchitectureReview(deps, rawInput, signal) {
|
|
1754
|
+
if (!isToolInput5(rawInput)) {
|
|
1755
|
+
throw invalidParams(
|
|
1756
|
+
TOOL_NAME5,
|
|
1757
|
+
"invalid input. Required: { diff: string, change_type: string }."
|
|
1758
|
+
);
|
|
1759
|
+
}
|
|
1760
|
+
assertWithinByteCap(TOOL_NAME5, "diff", rawInput.diff, MAX_DIFF_BYTES);
|
|
1761
|
+
if (rawInput.summary !== void 0) {
|
|
1762
|
+
assertWithinByteCap(TOOL_NAME5, "summary", rawInput.summary, MAX_SUMMARY_BYTES);
|
|
1763
|
+
}
|
|
1764
|
+
const changeType = isAcceptedChangeType(rawInput.change_type) ? rawInput.change_type : "other";
|
|
1765
|
+
const cacheInput = {
|
|
1766
|
+
diff: rawInput.diff,
|
|
1767
|
+
change_type: changeType,
|
|
1768
|
+
...rawInput.summary !== void 0 ? { summary: rawInput.summary } : {},
|
|
1769
|
+
gate_type: GATE_TYPE3
|
|
1770
|
+
};
|
|
1771
|
+
const key = deps.cache.keyFor(TOOL_NAME5, cacheInput);
|
|
1772
|
+
const excerpt = rawInput.diff.slice(0, 300);
|
|
1773
|
+
const inputSizeBytes = bytesOf(rawInput.diff) + (rawInput.summary !== void 0 ? bytesOf(rawInput.summary) : 0);
|
|
1774
|
+
const cached = deps.cache.get(key);
|
|
1775
|
+
if (cached !== null) {
|
|
1776
|
+
const replayEvent = {
|
|
1777
|
+
...buildBaseEvent({
|
|
1778
|
+
tool: TOOL_NAME5,
|
|
1779
|
+
gateType: GATE_TYPE3,
|
|
1780
|
+
inputHash: key,
|
|
1781
|
+
inputExcerpt: excerpt,
|
|
1782
|
+
inputSizeBytes,
|
|
1783
|
+
session: deps.session,
|
|
1784
|
+
now: deps.now()
|
|
1785
|
+
}),
|
|
1786
|
+
per_model_verdicts: cached.value.payload.per_model_verdicts,
|
|
1787
|
+
synthesized_verdict: cached.value.payload.synthesized_verdict ?? null,
|
|
1788
|
+
consensus_confidence: cached.value.payload.synthesized_verdict?.confidence ?? null,
|
|
1789
|
+
consensus_engine_version: cached.value.payload.engine_version ?? null,
|
|
1790
|
+
cache_hit: true
|
|
1791
|
+
};
|
|
1792
|
+
deps.events.append(replayEvent);
|
|
1793
|
+
return jsonContent({ ...cached.value, cache: { hit: true, key } });
|
|
1794
|
+
}
|
|
1795
|
+
const kbResult = findRulesForDiff(rawInput.diff);
|
|
1796
|
+
const { topRules, truncated: kbTruncated } = prepareKBRules(kbResult.rules);
|
|
1797
|
+
const baseEvent = buildBaseEvent({
|
|
1798
|
+
tool: TOOL_NAME5,
|
|
1799
|
+
gateType: GATE_TYPE3,
|
|
1800
|
+
inputHash: key,
|
|
1801
|
+
inputExcerpt: excerpt,
|
|
1802
|
+
inputSizeBytes,
|
|
1803
|
+
session: deps.session,
|
|
1804
|
+
now: deps.now()
|
|
1805
|
+
});
|
|
1806
|
+
let engineResult;
|
|
1807
|
+
let engineThrew = false;
|
|
1808
|
+
try {
|
|
1809
|
+
engineResult = await deps.consensus.run({
|
|
1810
|
+
gate_type: GATE_TYPE3,
|
|
1811
|
+
prompt: buildPrompt3(rawInput, changeType, topRules, kbTruncated),
|
|
1812
|
+
...signal !== void 0 ? { signal } : {}
|
|
1813
|
+
});
|
|
1814
|
+
} catch (err) {
|
|
1815
|
+
engineThrew = true;
|
|
1816
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1817
|
+
engineResult = { ok: false, reason: `engine-threw: ${message}` };
|
|
1818
|
+
}
|
|
1819
|
+
if (!engineResult.ok && engineResult.reason === "cancelled") {
|
|
1820
|
+
deps.events.append({
|
|
1821
|
+
...baseEvent,
|
|
1822
|
+
downstream_outcome: {
|
|
1823
|
+
status: "cancelled",
|
|
1824
|
+
notes: "mcp notifications/cancelled \u2014 handler aborted via SDK signal"
|
|
1825
|
+
}
|
|
1826
|
+
});
|
|
1827
|
+
const e = new Error("Request cancelled by client (notifications/cancelled)");
|
|
1828
|
+
e.name = "AbortError";
|
|
1829
|
+
throw e;
|
|
1830
|
+
}
|
|
1831
|
+
if (!engineResult.ok) {
|
|
1832
|
+
const payload2 = {
|
|
1833
|
+
verdict: "unimplemented",
|
|
1834
|
+
reason: engineResult.reason,
|
|
1835
|
+
per_model_verdicts: [],
|
|
1836
|
+
gate_type: GATE_TYPE3,
|
|
1837
|
+
...engineThrew ? { degraded_mode: true } : {},
|
|
1838
|
+
...kbResult.error !== null ? { kb_unavailable: true } : {},
|
|
1839
|
+
...kbTruncated > 0 ? { kb_rules_truncated: kbTruncated } : {}
|
|
1840
|
+
};
|
|
1841
|
+
const envelope2 = {
|
|
1842
|
+
tool: TOOL_NAME5,
|
|
1843
|
+
schema_version: 1,
|
|
1844
|
+
cache: { hit: false, key },
|
|
1845
|
+
payload: payload2
|
|
1846
|
+
};
|
|
1847
|
+
deps.events.append(baseEvent);
|
|
1848
|
+
return jsonContent(envelope2);
|
|
1849
|
+
}
|
|
1850
|
+
const perModelForEvent = toEventPerModelVerdicts(engineResult.per_model_verdicts);
|
|
1851
|
+
const synthForEvent = toEventSynthesizedVerdict(engineResult.synthesized_verdict);
|
|
1852
|
+
const enrichedEvent = {
|
|
1853
|
+
...baseEvent,
|
|
1854
|
+
per_model_verdicts: perModelForEvent,
|
|
1855
|
+
synthesized_verdict: synthForEvent,
|
|
1856
|
+
consensus_confidence: engineResult.synthesized_verdict.confidence,
|
|
1857
|
+
duration_ms: engineResult.duration_ms,
|
|
1858
|
+
consensus_engine_version: engineResult.engine_version
|
|
1859
|
+
};
|
|
1860
|
+
const escalationRequired = engineResult.escalation_required === true || engineResult.escalation_required === void 0 && engineResult.synthesized_verdict.dissent_summary !== null;
|
|
1861
|
+
const escalationReason = engineResult.escalation_reason ?? engineResult.synthesized_verdict.dissent_summary ?? "";
|
|
1862
|
+
const payload = {
|
|
1863
|
+
verdict: engineResult.synthesized_verdict.verdict,
|
|
1864
|
+
reason: engineResult.synthesized_verdict.dissent_summary ?? `panel agreed (${engineResult.per_model_verdicts.length} models, engine=${engineResult.engine_version})`,
|
|
1865
|
+
per_model_verdicts: perModelForEvent,
|
|
1866
|
+
synthesized_verdict: synthForEvent,
|
|
1867
|
+
engine_version: engineResult.engine_version,
|
|
1868
|
+
degraded: engineResult.degraded,
|
|
1869
|
+
gate_type: GATE_TYPE3,
|
|
1870
|
+
...escalationRequired ? {
|
|
1871
|
+
escalation: {
|
|
1872
|
+
required: true,
|
|
1873
|
+
reason: sanitizeExcerpt(
|
|
1874
|
+
escalationReason.length > 0 ? escalationReason : "panel disagreement \u2014 operator review recommended"
|
|
1875
|
+
)
|
|
1876
|
+
}
|
|
1877
|
+
} : {},
|
|
1878
|
+
...kbResult.error !== null ? { kb_unavailable: true } : {},
|
|
1879
|
+
...kbTruncated > 0 ? { kb_rules_truncated: kbTruncated } : {}
|
|
1880
|
+
};
|
|
1881
|
+
const envelope = {
|
|
1882
|
+
tool: TOOL_NAME5,
|
|
1883
|
+
schema_version: 1,
|
|
1884
|
+
cache: { hit: false, key },
|
|
1885
|
+
payload
|
|
1886
|
+
};
|
|
1887
|
+
deps.cache.set(key, envelope);
|
|
1888
|
+
deps.events.append(enrichedEvent);
|
|
1889
|
+
return jsonContent(envelope);
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
// ../vo-ratchets/src/config.ts
|
|
1893
|
+
import { readFile } from "node:fs/promises";
|
|
1894
|
+
import path from "node:path";
|
|
1895
|
+
import { z as z3 } from "zod";
|
|
1896
|
+
var ratchetIdSchema = z3.enum([
|
|
1897
|
+
"assertion-strength",
|
|
1898
|
+
"hollow-tests",
|
|
1899
|
+
"verify-answer",
|
|
1900
|
+
"fix-strength",
|
|
1901
|
+
"qa-honesty"
|
|
1902
|
+
]);
|
|
1903
|
+
var thresholdSchema = z3.enum(["strict", "medium", "permissive"]);
|
|
1904
|
+
var userConfigSchema = z3.object({
|
|
1905
|
+
enabled: z3.record(ratchetIdSchema, z3.boolean()).optional(),
|
|
1906
|
+
thresholds: z3.record(ratchetIdSchema, thresholdSchema).optional(),
|
|
1907
|
+
allowlist: z3.object({
|
|
1908
|
+
paths: z3.array(z3.string()).optional(),
|
|
1909
|
+
rules: z3.array(z3.string()).optional()
|
|
1910
|
+
}).optional(),
|
|
1911
|
+
reportOnly: z3.boolean().optional(),
|
|
1912
|
+
paths: z3.array(z3.string()).optional()
|
|
1913
|
+
}).strict();
|
|
1914
|
+
var DEFAULT_CONFIG = {
|
|
1915
|
+
enabled: {
|
|
1916
|
+
"assertion-strength": true,
|
|
1917
|
+
"hollow-tests": true,
|
|
1918
|
+
"verify-answer": true,
|
|
1919
|
+
"fix-strength": true,
|
|
1920
|
+
"qa-honesty": true
|
|
1921
|
+
},
|
|
1922
|
+
thresholds: {
|
|
1923
|
+
"assertion-strength": "medium",
|
|
1924
|
+
"hollow-tests": "medium",
|
|
1925
|
+
"verify-answer": "medium",
|
|
1926
|
+
"fix-strength": "medium",
|
|
1927
|
+
"qa-honesty": "medium"
|
|
1928
|
+
},
|
|
1929
|
+
allowlist: {
|
|
1930
|
+
paths: [],
|
|
1931
|
+
rules: []
|
|
1932
|
+
},
|
|
1933
|
+
reportOnly: false,
|
|
1934
|
+
paths: []
|
|
1935
|
+
};
|
|
1936
|
+
function resolveConfig(userConfig) {
|
|
1937
|
+
const parsed = userConfigSchema.parse(userConfig ?? {});
|
|
1938
|
+
const enabled = {
|
|
1939
|
+
...DEFAULT_CONFIG.enabled,
|
|
1940
|
+
...parsed.enabled ?? {}
|
|
1941
|
+
};
|
|
1942
|
+
const thresholds = {
|
|
1943
|
+
...DEFAULT_CONFIG.thresholds,
|
|
1944
|
+
...parsed.thresholds ?? {}
|
|
1945
|
+
};
|
|
1946
|
+
const allowlist = {
|
|
1947
|
+
paths: parsed.allowlist?.paths ?? [],
|
|
1948
|
+
rules: parsed.allowlist?.rules ?? []
|
|
1949
|
+
};
|
|
1950
|
+
return {
|
|
1951
|
+
enabled,
|
|
1952
|
+
thresholds,
|
|
1953
|
+
allowlist,
|
|
1954
|
+
reportOnly: parsed.reportOnly ?? DEFAULT_CONFIG.reportOnly,
|
|
1955
|
+
paths: parsed.paths ?? DEFAULT_CONFIG.paths
|
|
1956
|
+
};
|
|
1957
|
+
}
|
|
1958
|
+
function applyOnlyFilter(config, only) {
|
|
1959
|
+
if (!only) return config;
|
|
1960
|
+
const enabled = {
|
|
1961
|
+
"assertion-strength": false,
|
|
1962
|
+
"hollow-tests": false,
|
|
1963
|
+
"verify-answer": false,
|
|
1964
|
+
"fix-strength": false,
|
|
1965
|
+
"qa-honesty": false
|
|
1966
|
+
};
|
|
1967
|
+
enabled[only] = true;
|
|
1968
|
+
return { ...config, enabled };
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
// ../vo-ratchets/src/util/walk.ts
|
|
1972
|
+
import { readdir, readFile as readFile2 } from "node:fs/promises";
|
|
1973
|
+
import path2 from "node:path";
|
|
1974
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
1975
|
+
"node_modules",
|
|
1976
|
+
".git",
|
|
1977
|
+
"dist",
|
|
1978
|
+
"build",
|
|
1979
|
+
"out",
|
|
1980
|
+
"lib",
|
|
1981
|
+
".next",
|
|
1982
|
+
".nuxt",
|
|
1983
|
+
".svelte-kit",
|
|
1984
|
+
".turbo",
|
|
1985
|
+
"coverage",
|
|
1986
|
+
".coverage",
|
|
1987
|
+
".cache",
|
|
1988
|
+
".parcel-cache",
|
|
1989
|
+
".vscode",
|
|
1990
|
+
".idea",
|
|
1991
|
+
".claude",
|
|
1992
|
+
".agent-worktrees",
|
|
1993
|
+
"test-results"
|
|
1994
|
+
]);
|
|
1995
|
+
function toForwardSlashes(p) {
|
|
1996
|
+
return p.replace(/\\/g, "/");
|
|
1997
|
+
}
|
|
1998
|
+
async function walkFiles(cwd, filter) {
|
|
1999
|
+
const results = [];
|
|
2000
|
+
await walkDir(cwd, cwd, filter, results);
|
|
2001
|
+
results.sort();
|
|
2002
|
+
return results;
|
|
2003
|
+
}
|
|
2004
|
+
async function walkDir(root, current, filter, results) {
|
|
2005
|
+
let entries;
|
|
2006
|
+
try {
|
|
2007
|
+
entries = await readdir(current, { withFileTypes: true });
|
|
2008
|
+
} catch {
|
|
2009
|
+
return;
|
|
2010
|
+
}
|
|
2011
|
+
for (const entry of entries) {
|
|
2012
|
+
const absolute = path2.join(current, entry.name);
|
|
2013
|
+
if (entry.isDirectory()) {
|
|
2014
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
2015
|
+
await walkDir(root, absolute, filter, results);
|
|
2016
|
+
continue;
|
|
2017
|
+
}
|
|
2018
|
+
if (!entry.isFile()) continue;
|
|
2019
|
+
const relative = toForwardSlashes(path2.relative(root, absolute));
|
|
2020
|
+
if (!filter(relative)) continue;
|
|
2021
|
+
results.push(relative);
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
async function readRelative(cwd, relativePath) {
|
|
2025
|
+
try {
|
|
2026
|
+
return await readFile2(path2.resolve(cwd, relativePath), "utf8");
|
|
2027
|
+
} catch {
|
|
2028
|
+
return "";
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
function pathMatchesAny(relativePath, patterns) {
|
|
2032
|
+
for (const pattern of patterns) {
|
|
2033
|
+
if (matchGlob(relativePath, pattern)) return true;
|
|
2034
|
+
}
|
|
2035
|
+
return false;
|
|
2036
|
+
}
|
|
2037
|
+
function matchGlob(input, pattern) {
|
|
2038
|
+
const regexSource = globToRegex(pattern);
|
|
2039
|
+
return new RegExp(`^${regexSource}$`).test(input);
|
|
2040
|
+
}
|
|
2041
|
+
function globToRegex(pattern) {
|
|
2042
|
+
const doubleStarToken = "\0DOUBLE_STAR\0";
|
|
2043
|
+
const singleStarToken = "\0SINGLE_STAR\0";
|
|
2044
|
+
let work = pattern.replace(/\*\*/g, doubleStarToken).replace(/\*/g, singleStarToken);
|
|
2045
|
+
work = work.replace(/[.+?^${}()|[\]\\/]/g, (m) => `\\${m}`);
|
|
2046
|
+
work = work.replace(new RegExp(singleStarToken, "g"), "[^/]*");
|
|
2047
|
+
work = work.replace(new RegExp(doubleStarToken, "g"), ".*");
|
|
2048
|
+
return work;
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
// ../vo-ratchets/src/util/test-files.ts
|
|
2052
|
+
var TEST_FILE_RE = /(?:^|\/)[^/]+\.(?:test|spec)\.(?:ts|tsx|js|jsx|mjs|cjs)$/u;
|
|
2053
|
+
var TEST_TSX_RE = /\.test\.tsx$/u;
|
|
2054
|
+
function isTestFile(filePath) {
|
|
2055
|
+
return TEST_FILE_RE.test(toForwardSlashes(filePath));
|
|
2056
|
+
}
|
|
2057
|
+
function isComponentTestFile(filePath) {
|
|
2058
|
+
return TEST_TSX_RE.test(toForwardSlashes(filePath));
|
|
2059
|
+
}
|
|
2060
|
+
function maskStringsAndComments(content) {
|
|
2061
|
+
let out = "";
|
|
2062
|
+
let quote = null;
|
|
2063
|
+
let escaped = false;
|
|
2064
|
+
let lineComment = false;
|
|
2065
|
+
let blockComment = false;
|
|
2066
|
+
const text = String(content || "");
|
|
2067
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
2068
|
+
const char = text[index] ?? "";
|
|
2069
|
+
const next = text[index + 1] ?? "";
|
|
2070
|
+
if (lineComment) {
|
|
2071
|
+
if (char === "\r" || char === "\n") {
|
|
2072
|
+
out += char;
|
|
2073
|
+
lineComment = false;
|
|
2074
|
+
} else {
|
|
2075
|
+
out += " ";
|
|
2076
|
+
}
|
|
2077
|
+
continue;
|
|
2078
|
+
}
|
|
2079
|
+
if (blockComment) {
|
|
2080
|
+
if (char === "*" && next === "/") {
|
|
2081
|
+
out += " ";
|
|
2082
|
+
index += 1;
|
|
2083
|
+
blockComment = false;
|
|
2084
|
+
} else if (char === "\r" || char === "\n") {
|
|
2085
|
+
out += char;
|
|
2086
|
+
} else {
|
|
2087
|
+
out += " ";
|
|
2088
|
+
}
|
|
2089
|
+
continue;
|
|
2090
|
+
}
|
|
2091
|
+
if (!quote) {
|
|
2092
|
+
if (char === "/" && next === "/") {
|
|
2093
|
+
out += " ";
|
|
2094
|
+
index += 1;
|
|
2095
|
+
lineComment = true;
|
|
2096
|
+
continue;
|
|
2097
|
+
}
|
|
2098
|
+
if (char === "/" && next === "*") {
|
|
2099
|
+
out += " ";
|
|
2100
|
+
index += 1;
|
|
2101
|
+
blockComment = true;
|
|
2102
|
+
continue;
|
|
2103
|
+
}
|
|
2104
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
2105
|
+
quote = char;
|
|
2106
|
+
out += char;
|
|
2107
|
+
continue;
|
|
2108
|
+
}
|
|
2109
|
+
out += char;
|
|
2110
|
+
continue;
|
|
2111
|
+
}
|
|
2112
|
+
if (char === "\r" || char === "\n") {
|
|
2113
|
+
out += char;
|
|
2114
|
+
if (quote !== "`") {
|
|
2115
|
+
quote = null;
|
|
2116
|
+
escaped = false;
|
|
2117
|
+
}
|
|
2118
|
+
continue;
|
|
2119
|
+
}
|
|
2120
|
+
if (escaped) {
|
|
2121
|
+
out += " ";
|
|
2122
|
+
escaped = false;
|
|
2123
|
+
continue;
|
|
2124
|
+
}
|
|
2125
|
+
if (char === "\\") {
|
|
2126
|
+
out += " ";
|
|
2127
|
+
escaped = true;
|
|
2128
|
+
continue;
|
|
2129
|
+
}
|
|
2130
|
+
if (char === quote) {
|
|
2131
|
+
out += char;
|
|
2132
|
+
quote = null;
|
|
2133
|
+
continue;
|
|
2134
|
+
}
|
|
2135
|
+
out += " ";
|
|
2136
|
+
}
|
|
2137
|
+
return out;
|
|
2138
|
+
}
|
|
2139
|
+
function countMatches(content, regex) {
|
|
2140
|
+
const re = new RegExp(regex.source, regex.flags);
|
|
2141
|
+
return (String(content || "").match(re) || []).length;
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
// ../vo-ratchets/src/detectors/assertion-strength.ts
|
|
2145
|
+
var RATCHET_ID = "assertion-strength";
|
|
2146
|
+
var ALLOW_MARKER_RE = /\/\/\s*vo-ratchets-allow-assertion-strength\s*:\s*\S/iu;
|
|
2147
|
+
var ANY_MATCHER_RE = /\.(?:not\s*\.\s*)?to[A-Z]\w*\s*\(/gu;
|
|
2148
|
+
var TRIVIAL_MATCHER_RE = new RegExp(
|
|
2149
|
+
"\\.(?:toBeDefined|toBeTruthy|toBeFalsy|toBeNull|toBeUndefined)\\s*\\(\\s*\\)|\\.toBe\\s*\\(\\s*(?:true|false)\\s*\\)|\\.(?:not\\s*\\.\\s*)?toThrow\\s*\\(\\s*\\)",
|
|
2150
|
+
"gu"
|
|
2151
|
+
);
|
|
2152
|
+
var TO_HAVE_BEEN_CALLED_RE = /\.toHaveBeenCalled\s*\(\s*\)/u;
|
|
2153
|
+
var TO_HAVE_BEEN_CALLED_WITH_RE = /\.toHaveBeenCalledWith\s*\(/u;
|
|
2154
|
+
var MOCK_CALLS_LENGTH_RE = /\.mock\.calls\.length\b/u;
|
|
2155
|
+
var NOT_TO_THROW_RE = /\.not\s*\.\s*toThrow\s*\(/u;
|
|
2156
|
+
var LITERAL_TRUE_RE = /\b(?:expect|assert(?:\.ok)?)\s*\(\s*true\s*\)/u;
|
|
2157
|
+
function hasAllowMarker(content) {
|
|
2158
|
+
return ALLOW_MARKER_RE.test(content);
|
|
2159
|
+
}
|
|
2160
|
+
function inspectFile({
|
|
2161
|
+
filePath,
|
|
2162
|
+
content,
|
|
2163
|
+
threshold
|
|
2164
|
+
}) {
|
|
2165
|
+
if (!isTestFile(filePath)) return [];
|
|
2166
|
+
if (hasAllowMarker(content)) return [];
|
|
2167
|
+
const masked = maskStringsAndComments(content);
|
|
2168
|
+
const hits = [];
|
|
2169
|
+
if (LITERAL_TRUE_RE.test(masked)) {
|
|
2170
|
+
hits.push({
|
|
2171
|
+
rule: "literal-true",
|
|
2172
|
+
severity: "error",
|
|
2173
|
+
message: "Found `expect(true)` or `assert(true)`. Literal-true assertions pass by construction and prove no behavior. Replace with a value-comparing matcher against the actual output."
|
|
2174
|
+
});
|
|
2175
|
+
}
|
|
2176
|
+
const totalMatchers = countMatches(masked, ANY_MATCHER_RE);
|
|
2177
|
+
if (totalMatchers > 0 && countMockCallsLengthAssertions(masked) === totalMatchers) {
|
|
2178
|
+
hits.push({
|
|
2179
|
+
rule: "mock-calls-length-only",
|
|
2180
|
+
severity: "error",
|
|
2181
|
+
message: "Every assertion in this file checks `.mock.calls.length`. Verifying a mock was called N times without verifying the arguments or the effect on the unit under test does not prove behavior. Add at least one assertion against the system's actual output."
|
|
2182
|
+
});
|
|
2183
|
+
}
|
|
2184
|
+
if (totalMatchers > 0 && isNotToThrowOnly(masked, totalMatchers)) {
|
|
2185
|
+
hits.push({
|
|
2186
|
+
rule: "not-to-throw-only",
|
|
2187
|
+
severity: "error",
|
|
2188
|
+
message: 'The only assertion is `expect(...).not.toThrow()`. "Doesn\'t crash" is a smoke check, not a behavioral test. Verify the return value or side effect, not just the absence of an exception.'
|
|
2189
|
+
});
|
|
2190
|
+
}
|
|
2191
|
+
if (shouldFlagBareToHaveBeenCalled(masked, threshold)) {
|
|
2192
|
+
hits.push({
|
|
2193
|
+
rule: "to-have-been-called",
|
|
2194
|
+
severity: threshold === "permissive" ? "warning" : "error",
|
|
2195
|
+
message: "`toHaveBeenCalled()` appears without any `toHaveBeenCalledWith(...)` or value-comparing assertion in this file. Verifying that a mock fired tells you nothing about whether it was called correctly. Pair with `toHaveBeenCalledWith(...)` or assert on the resulting state."
|
|
2196
|
+
});
|
|
2197
|
+
}
|
|
2198
|
+
if (totalMatchers > 0) {
|
|
2199
|
+
const trivialMatchers = countMatches(masked, TRIVIAL_MATCHER_RE);
|
|
2200
|
+
if (trivialMatchers === totalMatchers && threshold !== "permissive") {
|
|
2201
|
+
hits.push({
|
|
2202
|
+
rule: "trivial-only",
|
|
2203
|
+
severity: "error",
|
|
2204
|
+
message: `All ${totalMatchers} expect() matcher${totalMatchers === 1 ? "" : "s"} are trivial (toBeDefined / toBeTruthy / toBeFalsy / toBeNull / toBeUndefined / toBe(true|false) / not.toThrow). These prove the file imported and ran but verify no behavior. Add at least one value-comparing matcher: toEqual, toStrictEqual, toBe(<value>), toMatch, toMatchObject, toContain, toHaveLength, toHaveProperty.`
|
|
2205
|
+
});
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
return hits;
|
|
2209
|
+
}
|
|
2210
|
+
function countMockCallsLengthAssertions(masked) {
|
|
2211
|
+
let count = 0;
|
|
2212
|
+
for (const line of masked.split(/\r?\n/u)) {
|
|
2213
|
+
if (!MOCK_CALLS_LENGTH_RE.test(line)) continue;
|
|
2214
|
+
if (!/\.(?:not\s*\.\s*)?to[A-Z]\w*\s*\(/u.test(line)) continue;
|
|
2215
|
+
count += 1;
|
|
2216
|
+
}
|
|
2217
|
+
return count;
|
|
2218
|
+
}
|
|
2219
|
+
function isNotToThrowOnly(masked, totalMatchers) {
|
|
2220
|
+
if (!NOT_TO_THROW_RE.test(masked)) return false;
|
|
2221
|
+
const notToThrowCount = countMatches(masked, /\.not\s*\.\s*toThrow\s*\(/gu);
|
|
2222
|
+
return notToThrowCount === totalMatchers;
|
|
2223
|
+
}
|
|
2224
|
+
function shouldFlagBareToHaveBeenCalled(masked, threshold) {
|
|
2225
|
+
if (!TO_HAVE_BEEN_CALLED_RE.test(masked)) return false;
|
|
2226
|
+
if (TO_HAVE_BEEN_CALLED_WITH_RE.test(masked)) return false;
|
|
2227
|
+
const totalMatchers = countMatches(masked, ANY_MATCHER_RE);
|
|
2228
|
+
const trivialMatchers = countMatches(masked, TRIVIAL_MATCHER_RE);
|
|
2229
|
+
const valueComparing = totalMatchers - trivialMatchers - countMatches(masked, /\.toHaveBeenCalled\s*\(\s*\)/gu);
|
|
2230
|
+
if (threshold === "permissive") return false;
|
|
2231
|
+
if (threshold === "medium" && valueComparing >= 1) return false;
|
|
2232
|
+
return true;
|
|
2233
|
+
}
|
|
2234
|
+
var assertionStrengthDetector = {
|
|
2235
|
+
id: RATCHET_ID,
|
|
2236
|
+
stub: false,
|
|
2237
|
+
async run(input) {
|
|
2238
|
+
const threshold = input.config.thresholds[RATCHET_ID];
|
|
2239
|
+
const files = await walkFiles(input.cwd, (rel) => isTestFile(rel));
|
|
2240
|
+
const findings = [];
|
|
2241
|
+
for (const file of files) {
|
|
2242
|
+
const content = await readRelative(input.cwd, file);
|
|
2243
|
+
for (const hit of inspectFile({ filePath: file, content, threshold })) {
|
|
2244
|
+
findings.push({
|
|
2245
|
+
ratchet: RATCHET_ID,
|
|
2246
|
+
rule: hit.rule,
|
|
2247
|
+
file,
|
|
2248
|
+
severity: hit.severity,
|
|
2249
|
+
message: hit.message
|
|
2250
|
+
});
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
return {
|
|
2254
|
+
ratchet: RATCHET_ID,
|
|
2255
|
+
ran: true,
|
|
2256
|
+
findings,
|
|
2257
|
+
filesScanned: files.length,
|
|
2258
|
+
stub: false
|
|
2259
|
+
};
|
|
2260
|
+
}
|
|
2261
|
+
};
|
|
2262
|
+
|
|
2263
|
+
// ../vo-ratchets/src/detectors/fix-strength.ts
|
|
2264
|
+
import { spawnSync } from "node:child_process";
|
|
2265
|
+
var RATCHET_ID2 = "fix-strength";
|
|
2266
|
+
var FILE_ALLOW_RE = /\/\/\s*vo-ratchets-allow-fix-strength\s*:\s*\S/iu;
|
|
2267
|
+
var ALLOW_MARKER_RE2 = /\b(?:fix-strength-allow|verify-answer-allow)\s*:\s*\S/iu;
|
|
2268
|
+
var DEFAULT_STRONG_PATTERNS = [
|
|
2269
|
+
/\.toBe\s*\(\s*(?:-?\d+(?:\.\d+)?|true|false|null|undefined|"[^"\n]*"|'[^'\n]*'|`[^`\n]*`)/u,
|
|
2270
|
+
/\.toEqual\s*\(\s*(?:-?\d+(?:\.\d+)?|true|false|null|undefined|"[^"\n]*"|'[^'\n]*'|`[^`\n]*`|\[|\{)/u,
|
|
2271
|
+
/\.toStrictEqual\s*\(/u,
|
|
2272
|
+
/\.toBeCloseTo\s*\(/u,
|
|
2273
|
+
/\.toMatchObject\s*\(/u,
|
|
2274
|
+
/\.toMatchInlineSnapshot\s*\(/u
|
|
2275
|
+
];
|
|
2276
|
+
var DEFAULT_WEAK_PATTERNS = [
|
|
2277
|
+
/\.toBeGreaterThan\s*\(/u,
|
|
2278
|
+
/\.toBeGreaterThanOrEqual\s*\(/u,
|
|
2279
|
+
/\.toBeLessThan\s*\(/u,
|
|
2280
|
+
/\.toBeLessThanOrEqual\s*\(/u,
|
|
2281
|
+
/\.toBeTruthy\s*\(\s*\)/u,
|
|
2282
|
+
/\.toBeFalsy\s*\(\s*\)/u
|
|
2283
|
+
];
|
|
2284
|
+
function buildHelperPatterns(names) {
|
|
2285
|
+
const result = [];
|
|
2286
|
+
for (const name of names) {
|
|
2287
|
+
if (!/^[A-Za-z_$][\w$]*$/u.test(name)) continue;
|
|
2288
|
+
result.push(new RegExp(`\\b${name}\\s*\\(`, "u"));
|
|
2289
|
+
}
|
|
2290
|
+
return result;
|
|
2291
|
+
}
|
|
2292
|
+
function countMatchesInSource(source, patterns) {
|
|
2293
|
+
if (typeof source !== "string" || !source) return 0;
|
|
2294
|
+
let total = 0;
|
|
2295
|
+
const lines = source.split(/\r?\n/u);
|
|
2296
|
+
for (const line of lines) {
|
|
2297
|
+
const stripped = line.trim();
|
|
2298
|
+
if (!stripped) continue;
|
|
2299
|
+
if (stripped.startsWith("//") || stripped.startsWith("*")) continue;
|
|
2300
|
+
for (const re of patterns) {
|
|
2301
|
+
const matches = line.match(new RegExp(re.source, `${re.flags}g`));
|
|
2302
|
+
if (matches) total += matches.length;
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
return total;
|
|
2306
|
+
}
|
|
2307
|
+
function countStrong(source, extras = {}) {
|
|
2308
|
+
const patterns = [
|
|
2309
|
+
...DEFAULT_STRONG_PATTERNS,
|
|
2310
|
+
...buildHelperPatterns(extras.extraStrongHelpers ?? []),
|
|
2311
|
+
...extras.extraStrongPatterns ?? []
|
|
2312
|
+
];
|
|
2313
|
+
return countMatchesInSource(source, patterns);
|
|
2314
|
+
}
|
|
2315
|
+
function countWeak(source, extras = {}) {
|
|
2316
|
+
const patterns = [
|
|
2317
|
+
...DEFAULT_WEAK_PATTERNS,
|
|
2318
|
+
...extras.extraWeakPatterns ?? []
|
|
2319
|
+
];
|
|
2320
|
+
return countMatchesInSource(source, patterns);
|
|
2321
|
+
}
|
|
2322
|
+
function compareStrengthBetweenVersions(input) {
|
|
2323
|
+
const { filePath, preSource, postSource } = input;
|
|
2324
|
+
const preStrong = countStrong(preSource, input);
|
|
2325
|
+
const preWeak = countWeak(preSource, input);
|
|
2326
|
+
const postStrong = countStrong(postSource, input);
|
|
2327
|
+
const postWeak = countWeak(postSource, input);
|
|
2328
|
+
const pre = { strong: preStrong, weak: preWeak };
|
|
2329
|
+
const post = { strong: postStrong, weak: postWeak };
|
|
2330
|
+
if (FILE_ALLOW_RE.test(preSource) || FILE_ALLOW_RE.test(postSource)) {
|
|
2331
|
+
return { filePath, verdict: "ok", pre, post, reason: "file-allow-marker" };
|
|
2332
|
+
}
|
|
2333
|
+
const sources = `${preSource ?? ""}
|
|
2334
|
+
${postSource ?? ""}`;
|
|
2335
|
+
if (ALLOW_MARKER_RE2.test(sources)) {
|
|
2336
|
+
return { filePath, verdict: "ok", pre, post, reason: "line-allow-marker" };
|
|
2337
|
+
}
|
|
2338
|
+
if (preStrong === 0) {
|
|
2339
|
+
return { filePath, verdict: "no-strong-pre", pre, post };
|
|
2340
|
+
}
|
|
2341
|
+
const strongLost = preStrong > postStrong;
|
|
2342
|
+
const weakRose = postWeak > preWeak;
|
|
2343
|
+
if (strongLost && weakRose) {
|
|
2344
|
+
return { filePath, verdict: "strength-decreased", pre, post };
|
|
2345
|
+
}
|
|
2346
|
+
if (strongLost && !weakRose) {
|
|
2347
|
+
return { filePath, verdict: "strong-removed", pre, post };
|
|
2348
|
+
}
|
|
2349
|
+
return { filePath, verdict: "ok", pre, post };
|
|
2350
|
+
}
|
|
2351
|
+
async function inspectChanges(input) {
|
|
2352
|
+
const findings = [];
|
|
2353
|
+
for (const file of input.changedFiles) {
|
|
2354
|
+
if (!isTestFile(file)) continue;
|
|
2355
|
+
const [pre, post] = await Promise.all([
|
|
2356
|
+
input.readBase(file),
|
|
2357
|
+
input.readHead(file)
|
|
2358
|
+
]);
|
|
2359
|
+
const verdict = compareStrengthBetweenVersions({
|
|
2360
|
+
filePath: file,
|
|
2361
|
+
preSource: pre,
|
|
2362
|
+
postSource: post,
|
|
2363
|
+
// Conditional-spread the optional `extra*` overrides so
|
|
2364
|
+
// exactOptionalPropertyTypes is satisfied (undefined isn't assignable
|
|
2365
|
+
// to `readonly T[]` when the property is non-optional in the target).
|
|
2366
|
+
...input.extraStrongHelpers !== void 0 ? { extraStrongHelpers: input.extraStrongHelpers } : {},
|
|
2367
|
+
...input.extraStrongPatterns !== void 0 ? { extraStrongPatterns: input.extraStrongPatterns } : {},
|
|
2368
|
+
...input.extraWeakPatterns !== void 0 ? { extraWeakPatterns: input.extraWeakPatterns } : {}
|
|
2369
|
+
});
|
|
2370
|
+
if (verdict.verdict === "strength-decreased") {
|
|
2371
|
+
findings.push({
|
|
2372
|
+
rule: "strength-decreased",
|
|
2373
|
+
file,
|
|
2374
|
+
severity: input.threshold === "permissive" ? "warning" : "error",
|
|
2375
|
+
message: `${file} weakened: pre had ${verdict.pre.strong} strong / ${verdict.pre.weak} weak; post has ${verdict.post.strong} strong / ${verdict.post.weak} weak. A fix must not replace strong literal-pinned assertions with comparator-only ones. Restore the strong assertion, pin a new expected value, or add \`// fix-strength-allow: <reason>\`.`
|
|
2376
|
+
});
|
|
2377
|
+
continue;
|
|
2378
|
+
}
|
|
2379
|
+
if (verdict.verdict === "strong-removed") {
|
|
2380
|
+
findings.push({
|
|
2381
|
+
rule: "strong-removed",
|
|
2382
|
+
file,
|
|
2383
|
+
severity: input.threshold === "strict" ? "error" : "warning",
|
|
2384
|
+
message: `${file} dropped strong assertion(s) without compensation: pre ${verdict.pre.strong} -> post ${verdict.post.strong}. If this was intentional (test moved/split, fixture corrected), add \`// fix-strength-allow: <reason>\`.`
|
|
2385
|
+
});
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
return findings;
|
|
2389
|
+
}
|
|
2390
|
+
function readGitFile(cwd, ref, filePath) {
|
|
2391
|
+
const result = spawnSync("git", ["show", `${ref}:${filePath}`], {
|
|
2392
|
+
cwd,
|
|
2393
|
+
encoding: "utf8",
|
|
2394
|
+
maxBuffer: 16 * 1024 * 1024
|
|
2395
|
+
});
|
|
2396
|
+
if (result.status !== 0) return "";
|
|
2397
|
+
return result.stdout || "";
|
|
2398
|
+
}
|
|
2399
|
+
function listChangedFiles(cwd, baseRef, headRef) {
|
|
2400
|
+
const result = spawnSync(
|
|
2401
|
+
"git",
|
|
2402
|
+
["diff", "--name-only", "--diff-filter=AMR", `${baseRef}...${headRef}`],
|
|
2403
|
+
{ cwd, encoding: "utf8", maxBuffer: 16 * 1024 * 1024 }
|
|
2404
|
+
);
|
|
2405
|
+
if (result.status !== 0) return [];
|
|
2406
|
+
return (result.stdout || "").split(/\r?\n/u).map((line) => line.trim()).filter(Boolean);
|
|
2407
|
+
}
|
|
2408
|
+
var fixStrengthDetector = {
|
|
2409
|
+
id: RATCHET_ID2,
|
|
2410
|
+
stub: false,
|
|
2411
|
+
async run(input) {
|
|
2412
|
+
const threshold = input.config.thresholds[RATCHET_ID2];
|
|
2413
|
+
const baseRef = process.env["VO_RATCHETS_FIX_STRENGTH_BASE"] || "";
|
|
2414
|
+
const headRef = process.env["VO_RATCHETS_FIX_STRENGTH_HEAD"] || "";
|
|
2415
|
+
if (!baseRef || !headRef) {
|
|
2416
|
+
return {
|
|
2417
|
+
ratchet: RATCHET_ID2,
|
|
2418
|
+
ran: false,
|
|
2419
|
+
findings: [],
|
|
2420
|
+
filesScanned: 0,
|
|
2421
|
+
stub: false
|
|
2422
|
+
};
|
|
2423
|
+
}
|
|
2424
|
+
const changedFiles = listChangedFiles(input.cwd, baseRef, headRef);
|
|
2425
|
+
const findings = await inspectChanges({
|
|
2426
|
+
changedFiles,
|
|
2427
|
+
readBase: async (file) => readGitFile(input.cwd, baseRef, file),
|
|
2428
|
+
readHead: async (file) => readGitFile(input.cwd, headRef, file),
|
|
2429
|
+
threshold
|
|
2430
|
+
});
|
|
2431
|
+
const ratchetFindings = findings.map((f) => ({
|
|
2432
|
+
ratchet: RATCHET_ID2,
|
|
2433
|
+
rule: f.rule,
|
|
2434
|
+
file: f.file,
|
|
2435
|
+
severity: f.severity,
|
|
2436
|
+
message: f.message
|
|
2437
|
+
}));
|
|
2438
|
+
return {
|
|
2439
|
+
ratchet: RATCHET_ID2,
|
|
2440
|
+
ran: true,
|
|
2441
|
+
findings: ratchetFindings,
|
|
2442
|
+
filesScanned: changedFiles.filter(isTestFile).length,
|
|
2443
|
+
stub: false
|
|
2444
|
+
};
|
|
2445
|
+
}
|
|
2446
|
+
};
|
|
2447
|
+
|
|
2448
|
+
// ../vo-ratchets/src/detectors/hollow-tests.ts
|
|
2449
|
+
var RATCHET_ID3 = "hollow-tests";
|
|
2450
|
+
var FILE_ALLOW_RE2 = /\/\/\s*vo-ratchets-allow-hollow-tests\s*:\s*\S/iu;
|
|
2451
|
+
var SKIP_ALLOW_RE = /\/\/\s*vo-ratchets-allow-hollow-tests-skip\s*:\s*\S/iu;
|
|
2452
|
+
var IT_COUNTING_RE = /(?<![.\w])(?:it|test)(?:\.only)?\s*\(/gu;
|
|
2453
|
+
var IT_SKIP_RE = /(?<![.\w])(?:it|test)\s*\.\s*(?:skip|todo)\s*\(/u;
|
|
2454
|
+
var USER_EVENT_RE = /\buserEvent\s*\.\s*\w+\s*\(/u;
|
|
2455
|
+
var FIRE_EVENT_RE = /\bfireEvent\s*\.\s*\w+\s*\(/u;
|
|
2456
|
+
var AWAIT_USER_RE = /\bawait\s+user\s*\.\s*\w+\s*\(/u;
|
|
2457
|
+
var ANY_MATCHER_RE2 = /\.(?:not\s*\.\s*)?to[A-Z]\w*\s*\(/gu;
|
|
2458
|
+
var SWALLOWING_CATCH_RE = /catch\s*(?:\([^)]*\))?\s*\{\s*(?:\/\/[^\n]*\n\s*)?\}/u;
|
|
2459
|
+
var CATCH_RETURN_TRUE_RE = /catch\s*(?:\([^)]*\))?\s*\{\s*return\s+true\s*;?\s*\}/u;
|
|
2460
|
+
var EMPTY_IT_RE = /(?<![.\w])(?:it|test)\s*\(\s*['"`][^'"`\n]+['"`]\s*,\s*(?:async\s*)?(?:\(\s*\)\s*=>|function\s*\(\s*\))\s*\{\s*\}\s*\)/gu;
|
|
2461
|
+
var FLOOR_BY_THRESHOLD = {
|
|
2462
|
+
strict: 4,
|
|
2463
|
+
medium: 3,
|
|
2464
|
+
permissive: 1
|
|
2465
|
+
};
|
|
2466
|
+
function hasFileScopeAllowMarker(content) {
|
|
2467
|
+
const lines = String(content || "").split(/\r?\n/u);
|
|
2468
|
+
let firstImport = -1;
|
|
2469
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
2470
|
+
if (/^\s*import\s/u.test(lines[index] || "")) {
|
|
2471
|
+
firstImport = index;
|
|
2472
|
+
break;
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
const cap = firstImport >= 0 ? firstImport : Math.min(20, lines.length - 1);
|
|
2476
|
+
for (let index = 0; index <= cap; index += 1) {
|
|
2477
|
+
if (FILE_ALLOW_RE2.test(lines[index] || "")) return true;
|
|
2478
|
+
}
|
|
2479
|
+
return false;
|
|
2480
|
+
}
|
|
2481
|
+
function countAllowedSkips(content) {
|
|
2482
|
+
const lines = String(content || "").split(/\r?\n/u);
|
|
2483
|
+
let count = 0;
|
|
2484
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
2485
|
+
if (!IT_SKIP_RE.test(lines[index] || "")) continue;
|
|
2486
|
+
const previous = lines[index - 1] || "";
|
|
2487
|
+
if (SKIP_ALLOW_RE.test(previous)) count += 1;
|
|
2488
|
+
}
|
|
2489
|
+
return count;
|
|
2490
|
+
}
|
|
2491
|
+
function inspectFile2({
|
|
2492
|
+
filePath,
|
|
2493
|
+
content,
|
|
2494
|
+
threshold
|
|
2495
|
+
}) {
|
|
2496
|
+
if (!isTestFile(filePath)) return [];
|
|
2497
|
+
if (hasFileScopeAllowMarker(content)) return [];
|
|
2498
|
+
const masked = maskStringsAndComments(content);
|
|
2499
|
+
const hits = [];
|
|
2500
|
+
const emptyBodyCount = countMatches(masked, EMPTY_IT_RE);
|
|
2501
|
+
if (emptyBodyCount > 0) {
|
|
2502
|
+
hits.push({
|
|
2503
|
+
rule: "empty-test-body",
|
|
2504
|
+
severity: "error",
|
|
2505
|
+
message: `Found ${emptyBodyCount} it()/test() block${emptyBodyCount === 1 ? "" : "s"} with an empty body. An empty test passes by construction and verifies nothing. Either implement the test or remove the placeholder; if you genuinely want a planned-but-not-yet-written marker, use \`.todo()\`.`
|
|
2506
|
+
});
|
|
2507
|
+
}
|
|
2508
|
+
const floor = FLOOR_BY_THRESHOLD[threshold];
|
|
2509
|
+
const itCount = countMatches(masked, IT_COUNTING_RE);
|
|
2510
|
+
const allowedSkipCount = countAllowedSkips(content);
|
|
2511
|
+
const effectiveCount = itCount + allowedSkipCount;
|
|
2512
|
+
if (effectiveCount < floor && itCount + allowedSkipCount > 0) {
|
|
2513
|
+
const skipNote = allowedSkipCount > 0 ? ` (+ ${allowedSkipCount} allow-marked skip${allowedSkipCount === 1 ? "" : "s"})` : "";
|
|
2514
|
+
hits.push({
|
|
2515
|
+
rule: "it-count-floor",
|
|
2516
|
+
severity: threshold === "strict" ? "error" : "warning",
|
|
2517
|
+
message: `Found ${itCount} it()/test() block${itCount === 1 ? "" : "s"}${skipNote}. Threshold (${threshold}) requires ${floor}+ tests, including at least one interaction case and one edge/error case. To exempt this file, add \`// vo-ratchets-allow-hollow-tests: <why>\` above the first import.`
|
|
2518
|
+
});
|
|
2519
|
+
}
|
|
2520
|
+
if (isComponentTestFile(filePath) && threshold !== "permissive") {
|
|
2521
|
+
const hasInteraction = USER_EVENT_RE.test(masked) || FIRE_EVENT_RE.test(masked) || AWAIT_USER_RE.test(masked);
|
|
2522
|
+
const hasAnyMatcher = countMatches(masked, ANY_MATCHER_RE2) > 0;
|
|
2523
|
+
if (!hasInteraction && hasAnyMatcher) {
|
|
2524
|
+
hits.push({
|
|
2525
|
+
rule: "no-interaction",
|
|
2526
|
+
severity: "warning",
|
|
2527
|
+
message: ".test.tsx file has no userEvent / fireEvent / `await user.*` call. Component tests must verify behavior, not just render. Use `const user = userEvent.setup()` then `await user.click(...)`, or `fireEvent.X(...)` for events userEvent does not support."
|
|
2528
|
+
});
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
if (SWALLOWING_CATCH_RE.test(masked) || CATCH_RETURN_TRUE_RE.test(masked)) {
|
|
2532
|
+
hits.push({
|
|
2533
|
+
rule: "swallowing-catch",
|
|
2534
|
+
severity: "error",
|
|
2535
|
+
message: 'Test body contains an empty `catch {}` or `catch { return true; }`. Swallowing exceptions turns "the system threw" into a passing test. Either assert on the expected error (`expect(() => ...).toThrow(...)`) or let the exception bubble.'
|
|
2536
|
+
});
|
|
2537
|
+
}
|
|
2538
|
+
return hits;
|
|
2539
|
+
}
|
|
2540
|
+
var hollowTestsDetector = {
|
|
2541
|
+
id: RATCHET_ID3,
|
|
2542
|
+
stub: false,
|
|
2543
|
+
async run(input) {
|
|
2544
|
+
const threshold = input.config.thresholds[RATCHET_ID3];
|
|
2545
|
+
const files = await walkFiles(input.cwd, (rel) => isTestFile(rel));
|
|
2546
|
+
const findings = [];
|
|
2547
|
+
for (const file of files) {
|
|
2548
|
+
const content = await readRelative(input.cwd, file);
|
|
2549
|
+
for (const hit of inspectFile2({ filePath: file, content, threshold })) {
|
|
2550
|
+
findings.push({
|
|
2551
|
+
ratchet: RATCHET_ID3,
|
|
2552
|
+
rule: hit.rule,
|
|
2553
|
+
file,
|
|
2554
|
+
severity: hit.severity,
|
|
2555
|
+
message: hit.message
|
|
2556
|
+
});
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
return {
|
|
2560
|
+
ratchet: RATCHET_ID3,
|
|
2561
|
+
ran: true,
|
|
2562
|
+
findings,
|
|
2563
|
+
filesScanned: files.length,
|
|
2564
|
+
stub: false
|
|
2565
|
+
};
|
|
2566
|
+
}
|
|
2567
|
+
};
|
|
2568
|
+
|
|
2569
|
+
// ../vo-ratchets/src/detectors/qa-honesty.ts
|
|
2570
|
+
var RATCHET_ID4 = "qa-honesty";
|
|
2571
|
+
function isTesterMjsPath(filePath) {
|
|
2572
|
+
const lc = filePath.toLowerCase();
|
|
2573
|
+
if (!lc.endsWith(".mjs")) return false;
|
|
2574
|
+
const slash = lc.lastIndexOf("/");
|
|
2575
|
+
const basename2 = slash >= 0 ? lc.slice(slash + 1) : lc;
|
|
2576
|
+
return basename2.includes("tester");
|
|
2577
|
+
}
|
|
2578
|
+
var FILE_ALLOW_RE3 = /\/\/\s*vo-ratchets-allow-qa-honesty\s*:\s*\S/iu;
|
|
2579
|
+
var LINE_ALLOW_GENERIC_RE = /\b(?:qa-honesty-allow|vo-qa-allow-[a-z0-9-]+)\s*:\s*\S/iu;
|
|
2580
|
+
var REPORTER_PASS_RE = /\breporter\s*\.\s*pass\s*\(/u;
|
|
2581
|
+
var ASSERTION_RE = /\b(?:expect|assert(?:\.ok|Slow|Quality)?)\s*\(/u;
|
|
2582
|
+
var CATCH_RETURN_TRUE_RE2 = /catch\s*(?:\([^)]*\))?\s*\{[^{}]*?\breturn\s+true\b/u;
|
|
2583
|
+
var RENDER_RE = /\brender\s*\(/u;
|
|
2584
|
+
var USER_EVENT_RE2 = /\buserEvent\s*\.\s*\w+\s*\(/u;
|
|
2585
|
+
var FIRE_EVENT_RE2 = /\bfireEvent\s*\.\s*\w+\s*\(/u;
|
|
2586
|
+
var TO_BE_IN_DOCUMENT_RE = /\.toBeInTheDocument\s*\(\s*\)/u;
|
|
2587
|
+
var TO_HAVE_TEXT_CONTENT_RE = /\.toHaveTextContent\s*\(/u;
|
|
2588
|
+
var TRUTHY_RE = /\bexpect\s*\(\s*([A-Za-z_$][\w$]*)\s*\)\s*\.\s*toBeTruthy\s*\(\s*\)/u;
|
|
2589
|
+
var CALLABLE_RESULT_ASSIGN_RE = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*await\s+[A-Za-z_$][\w$.]*\s*\(/u;
|
|
2590
|
+
var INVALID_ARGUMENT_RETURN_TRUE_RE = /\bINVALID_ARGUMENT\b[^;\n]*return\s+true/iu;
|
|
2591
|
+
var VALUE_COMPARING_MATCHER_RE = /\.(?:toBe|toEqual|toStrictEqual|toBeCloseTo|toContain|toHaveLength|toMatch|toMatchObject|toHaveProperty)\s*\(/u;
|
|
2592
|
+
var SMOKE_TEST_RE = /\.smoke\.test\.(?:ts|tsx|js|jsx|mjs|cjs)$/u;
|
|
2593
|
+
function hasFileAllowMarker(content) {
|
|
2594
|
+
return FILE_ALLOW_RE3.test(content);
|
|
2595
|
+
}
|
|
2596
|
+
function hasLineAllowMarkerNearby(lines, index) {
|
|
2597
|
+
const start = Math.max(0, index - 3);
|
|
2598
|
+
for (let cursor = start; cursor <= index; cursor += 1) {
|
|
2599
|
+
if (LINE_ALLOW_GENERIC_RE.test(lines[cursor] ?? "")) return true;
|
|
2600
|
+
}
|
|
2601
|
+
return false;
|
|
2602
|
+
}
|
|
2603
|
+
function inspectFile3({
|
|
2604
|
+
filePath,
|
|
2605
|
+
content,
|
|
2606
|
+
threshold
|
|
2607
|
+
}) {
|
|
2608
|
+
const isTesterMjs = isTesterMjsPath(filePath);
|
|
2609
|
+
if (!isTestFile(filePath) && !isTesterMjs) return [];
|
|
2610
|
+
if (hasFileAllowMarker(content)) return [];
|
|
2611
|
+
const masked = maskStringsAndComments(content);
|
|
2612
|
+
const maskedLines = masked.split(/\r?\n/u);
|
|
2613
|
+
const rawLines = content.split(/\r?\n/u);
|
|
2614
|
+
const hits = [];
|
|
2615
|
+
const fileHasAssertion = ASSERTION_RE.test(masked);
|
|
2616
|
+
for (let i = 0; i < maskedLines.length; i += 1) {
|
|
2617
|
+
const maskedLine = maskedLines[i] ?? "";
|
|
2618
|
+
if (!REPORTER_PASS_RE.test(maskedLine)) continue;
|
|
2619
|
+
if (fileHasAssertion) continue;
|
|
2620
|
+
if (hasLineAllowMarkerNearby(rawLines, i)) continue;
|
|
2621
|
+
hits.push({
|
|
2622
|
+
rule: "reporter-pass-without-evidence",
|
|
2623
|
+
severity: "error",
|
|
2624
|
+
line: i + 1,
|
|
2625
|
+
message: "`reporter.pass(...)` called but no `expect(...)` / `assert(...)` appears anywhere in this file. Reporting success without proving it is a fake green. Add at least one value-comparing assertion before the pass call."
|
|
2626
|
+
});
|
|
2627
|
+
}
|
|
2628
|
+
if (CATCH_RETURN_TRUE_RE2.test(masked)) {
|
|
2629
|
+
let firstHit;
|
|
2630
|
+
for (let i = 0; i < maskedLines.length; i += 1) {
|
|
2631
|
+
if (/\breturn\s+true\b/u.test(maskedLines[i] ?? "")) {
|
|
2632
|
+
firstHit = i + 1;
|
|
2633
|
+
break;
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
const hitLine = firstHit ?? 1;
|
|
2637
|
+
if (firstHit === void 0 || !hasLineAllowMarkerNearby(rawLines, firstHit - 1)) {
|
|
2638
|
+
hits.push({
|
|
2639
|
+
rule: "catch-return-true",
|
|
2640
|
+
severity: "error",
|
|
2641
|
+
line: hitLine,
|
|
2642
|
+
message: "A catch block returns `true`. Swallowing exceptions turns errors (including INVALID_ARGUMENT) into passing tests. Either assert on the expected error or let the exception bubble. To exempt this block, add `// qa-honesty-allow: <reason>`."
|
|
2643
|
+
});
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
if (isComponentTestFile(filePath) && !SMOKE_TEST_RE.test(filePath) && threshold !== "permissive") {
|
|
2647
|
+
const hasRender = RENDER_RE.test(masked);
|
|
2648
|
+
const hasInteraction = USER_EVENT_RE2.test(masked) || FIRE_EVENT_RE2.test(masked);
|
|
2649
|
+
const onlyTextMatchers = (TO_BE_IN_DOCUMENT_RE.test(masked) || TO_HAVE_TEXT_CONTENT_RE.test(masked)) && !VALUE_COMPARING_MATCHER_RE.test(masked);
|
|
2650
|
+
if (hasRender && !hasInteraction && onlyTextMatchers) {
|
|
2651
|
+
hits.push({
|
|
2652
|
+
rule: "render-text-only",
|
|
2653
|
+
severity: "warning",
|
|
2654
|
+
message: "render() followed by `toBeInTheDocument` / `toHaveTextContent` and nothing else \u2014 Tier-1 smoke parading as Tier-2 verification. Either add user interaction (`await user.click(...)`) and a value-comparing matcher, or rename the file to `*.smoke.test.tsx` to mark it as intentional smoke."
|
|
2655
|
+
});
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
if (FIRE_EVENT_RE2.test(masked) && !USER_EVENT_RE2.test(masked)) {
|
|
2659
|
+
let firstLine;
|
|
2660
|
+
for (let i = 0; i < maskedLines.length; i += 1) {
|
|
2661
|
+
if (FIRE_EVENT_RE2.test(maskedLines[i] ?? "")) {
|
|
2662
|
+
firstLine = i + 1;
|
|
2663
|
+
break;
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
if (firstLine === void 0 || !hasLineAllowMarkerNearby(rawLines, firstLine - 1)) {
|
|
2667
|
+
hits.push({
|
|
2668
|
+
rule: "fire-event-without-user-event",
|
|
2669
|
+
severity: threshold === "strict" ? "error" : "warning",
|
|
2670
|
+
...firstLine ? { line: firstLine } : {},
|
|
2671
|
+
message: "`fireEvent.X(...)` used without any `userEvent.X(...)` in the same file. userEvent models real human interaction (focus, debounce, pointer events); fireEvent short-circuits that. Prefer userEvent unless the event genuinely cannot be modeled (in which case add a `// qa-honesty-allow: <reason>` marker)."
|
|
2672
|
+
});
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
const callableVars = /* @__PURE__ */ new Set();
|
|
2676
|
+
for (const line of maskedLines) {
|
|
2677
|
+
const match = CALLABLE_RESULT_ASSIGN_RE.exec(line);
|
|
2678
|
+
if (match) callableVars.add(match[1] ?? "");
|
|
2679
|
+
}
|
|
2680
|
+
for (let i = 0; i < maskedLines.length; i += 1) {
|
|
2681
|
+
const maskedLine = maskedLines[i] ?? "";
|
|
2682
|
+
const truthyMatch = TRUTHY_RE.exec(maskedLine);
|
|
2683
|
+
if (!truthyMatch) continue;
|
|
2684
|
+
const variable = truthyMatch[1];
|
|
2685
|
+
if (!variable || !callableVars.has(variable)) continue;
|
|
2686
|
+
if (hasLineAllowMarkerNearby(rawLines, i)) continue;
|
|
2687
|
+
hits.push({
|
|
2688
|
+
rule: "loose-truthy-result",
|
|
2689
|
+
severity: "error",
|
|
2690
|
+
line: i + 1,
|
|
2691
|
+
message: `\`expect(${variable}).toBeTruthy()\` where \`${variable}\` was assigned from an awaited call. Truthy proves the callable returned something, not the right something. Assert on specific fields or values from the result.`
|
|
2692
|
+
});
|
|
2693
|
+
}
|
|
2694
|
+
for (let i = 0; i < rawLines.length; i += 1) {
|
|
2695
|
+
const rawLine = rawLines[i] ?? "";
|
|
2696
|
+
const slashIdx = rawLine.indexOf("//");
|
|
2697
|
+
const codeOnly = slashIdx >= 0 ? rawLine.slice(0, slashIdx) : rawLine;
|
|
2698
|
+
if (!INVALID_ARGUMENT_RETURN_TRUE_RE.test(codeOnly)) continue;
|
|
2699
|
+
if (hasLineAllowMarkerNearby(rawLines, i)) continue;
|
|
2700
|
+
hits.push({
|
|
2701
|
+
rule: "invalid-argument-pass-risk",
|
|
2702
|
+
severity: "error",
|
|
2703
|
+
line: i + 1,
|
|
2704
|
+
message: "INVALID_ARGUMENT error path returns `true`. This turns a validation failure into a passing test \u2014 the system rejected the call but the test claims pass. Assert on the rejection explicitly."
|
|
2705
|
+
});
|
|
2706
|
+
}
|
|
2707
|
+
const snapshotMatchers = (masked.match(/\.(?:toMatchSnapshot|toMatchInlineSnapshot)\s*\(/gu) || []).length;
|
|
2708
|
+
const valueMatchers = (masked.match(/\.(?:toBe|toEqual|toStrictEqual|toBeCloseTo|toContain|toHaveLength|toMatch|toMatchObject|toHaveProperty)\s*\(/gu) || []).length;
|
|
2709
|
+
if (snapshotMatchers > 0 && valueMatchers === 0 && threshold !== "permissive") {
|
|
2710
|
+
hits.push({
|
|
2711
|
+
rule: "snapshot-heavy",
|
|
2712
|
+
severity: "warning",
|
|
2713
|
+
message: `Every assertion in this file is a snapshot (${snapshotMatchers} toMatchSnapshot / toMatchInlineSnapshot). Snapshots detect drift but rarely prove the verified product answer. Pair with at least one value-comparing matcher (toEqual / toBe / toMatchObject).`
|
|
2714
|
+
});
|
|
2715
|
+
}
|
|
2716
|
+
return hits;
|
|
2717
|
+
}
|
|
2718
|
+
var qaHonestyDetector = {
|
|
2719
|
+
id: RATCHET_ID4,
|
|
2720
|
+
stub: false,
|
|
2721
|
+
async run(input) {
|
|
2722
|
+
const threshold = input.config.thresholds[RATCHET_ID4];
|
|
2723
|
+
const files = await walkFiles(input.cwd, (rel) => {
|
|
2724
|
+
if (isTestFile(rel)) return true;
|
|
2725
|
+
return isTesterMjsPath(rel);
|
|
2726
|
+
});
|
|
2727
|
+
const findings = [];
|
|
2728
|
+
for (const file of files) {
|
|
2729
|
+
const content = await readRelative(input.cwd, file);
|
|
2730
|
+
for (const hit of inspectFile3({ filePath: file, content, threshold })) {
|
|
2731
|
+
findings.push({
|
|
2732
|
+
ratchet: RATCHET_ID4,
|
|
2733
|
+
rule: hit.rule,
|
|
2734
|
+
file,
|
|
2735
|
+
severity: hit.severity,
|
|
2736
|
+
message: hit.message,
|
|
2737
|
+
...typeof hit.line === "number" ? { line: hit.line } : {}
|
|
2738
|
+
});
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2741
|
+
return {
|
|
2742
|
+
ratchet: RATCHET_ID4,
|
|
2743
|
+
ran: true,
|
|
2744
|
+
findings,
|
|
2745
|
+
filesScanned: files.length,
|
|
2746
|
+
stub: false
|
|
2747
|
+
};
|
|
2748
|
+
}
|
|
2749
|
+
};
|
|
2750
|
+
|
|
2751
|
+
// ../vo-ratchets/src/detectors/verify-answer.ts
|
|
2752
|
+
var RATCHET_ID5 = "verify-answer";
|
|
2753
|
+
var FILE_ALLOW_RE4 = /\/\/\s*vo-ratchets-allow-verify-answer\s*:\s*\S/iu;
|
|
2754
|
+
var LINE_ALLOW_RE = /\bverify-answer-allow\s*:\s*\S/iu;
|
|
2755
|
+
var COMPARATOR_PATTERNS = [
|
|
2756
|
+
{ re: /\.toBeGreaterThan\s*\(/u, name: "toBeGreaterThan" },
|
|
2757
|
+
{ re: /\.toBeGreaterThanOrEqual\s*\(/u, name: "toBeGreaterThanOrEqual" },
|
|
2758
|
+
{ re: /\.toBeLessThan\s*\(/u, name: "toBeLessThan" },
|
|
2759
|
+
{ re: /\.toBeLessThanOrEqual\s*\(/u, name: "toBeLessThanOrEqual" },
|
|
2760
|
+
{ re: /\.toBeTruthy\s*\(\s*\)/u, name: "toBeTruthy" },
|
|
2761
|
+
{ re: /\.toBeFalsy\s*\(\s*\)/u, name: "toBeFalsy" }
|
|
2762
|
+
];
|
|
2763
|
+
var DEFAULT_PINNING_PATTERNS = [
|
|
2764
|
+
/\.toBe\s*\(\s*(?:-?\d+(?:\.\d+)?|true|false|null|undefined|"[^"\n]*"|'[^'\n]*'|`[^`\n]*`)/u,
|
|
2765
|
+
/\.toEqual\s*\(\s*(?:-?\d+(?:\.\d+)?|true|false|null|undefined|"[^"\n]*"|'[^'\n]*'|`[^`\n]*`|\[|\{)/u,
|
|
2766
|
+
/\.toStrictEqual\s*\(/u,
|
|
2767
|
+
/\.toBeCloseTo\s*\(/u,
|
|
2768
|
+
/\.toMatchObject\s*\(/u,
|
|
2769
|
+
/\.toMatchInlineSnapshot\s*\(/u
|
|
2770
|
+
];
|
|
2771
|
+
var LITERAL_TRUE_ASSERT_RE = /\b(?:expect|assert(?:\.ok)?)\s*\(\s*true\s*\)\s*(?:\.\s*toBe\s*\(\s*true\s*\)\s*)?/u;
|
|
2772
|
+
var AWAIT_CALL_RE = /\bawait\s+[A-Za-z_$][\w$.]*\s*\(/u;
|
|
2773
|
+
function hasFileAllowMarker2(content) {
|
|
2774
|
+
return FILE_ALLOW_RE4.test(content);
|
|
2775
|
+
}
|
|
2776
|
+
function lineHasPinning(line, extraHelpers) {
|
|
2777
|
+
for (const pattern of DEFAULT_PINNING_PATTERNS) {
|
|
2778
|
+
if (pattern.test(line)) return true;
|
|
2779
|
+
}
|
|
2780
|
+
for (const helper of extraHelpers) {
|
|
2781
|
+
if (!/^[A-Za-z_$][\w$]*$/u.test(helper)) continue;
|
|
2782
|
+
if (new RegExp(`\\b${helper}\\s*\\(`, "u").test(line)) return true;
|
|
2783
|
+
}
|
|
2784
|
+
return false;
|
|
2785
|
+
}
|
|
2786
|
+
function hasLineAllowMarkerNearby2(lines, index) {
|
|
2787
|
+
const start = Math.max(0, index - 3);
|
|
2788
|
+
for (let cursor = start; cursor <= index; cursor += 1) {
|
|
2789
|
+
if (LINE_ALLOW_RE.test(lines[cursor] ?? "")) return true;
|
|
2790
|
+
}
|
|
2791
|
+
return false;
|
|
2792
|
+
}
|
|
2793
|
+
function inspectFile4({
|
|
2794
|
+
filePath,
|
|
2795
|
+
content,
|
|
2796
|
+
threshold,
|
|
2797
|
+
extraPinningHelpers = []
|
|
2798
|
+
}) {
|
|
2799
|
+
if (!isTestFile(filePath)) return [];
|
|
2800
|
+
if (hasFileAllowMarker2(content)) return [];
|
|
2801
|
+
const hits = [];
|
|
2802
|
+
const masked = maskStringsAndComments(content);
|
|
2803
|
+
const maskedLines = masked.split(/\r?\n/u);
|
|
2804
|
+
const rawLines = content.split(/\r?\n/u);
|
|
2805
|
+
for (let i = 0; i < maskedLines.length; i += 1) {
|
|
2806
|
+
const maskedLine = maskedLines[i] ?? "";
|
|
2807
|
+
if (!maskedLine.trim()) continue;
|
|
2808
|
+
let matchedName = null;
|
|
2809
|
+
for (const { re, name } of COMPARATOR_PATTERNS) {
|
|
2810
|
+
if (re.test(maskedLine)) {
|
|
2811
|
+
matchedName = name;
|
|
2812
|
+
break;
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
if (!matchedName) continue;
|
|
2816
|
+
if (lineHasPinning(maskedLine, extraPinningHelpers)) continue;
|
|
2817
|
+
if (hasLineAllowMarkerNearby2(rawLines, i)) continue;
|
|
2818
|
+
hits.push({
|
|
2819
|
+
rule: "comparator-only",
|
|
2820
|
+
severity: threshold === "permissive" ? "warning" : "error",
|
|
2821
|
+
line: i + 1,
|
|
2822
|
+
message: `Comparator-only matcher \`.${matchedName}\` does not pin an expected value. A failure here can't tell the orchestrator the right answer. Use \`.toBe(<literal>)\`, \`.toEqual(<literal>)\`, \`.toBeCloseTo(<value>)\`, or \`.toMatchObject(...)\` instead. To exempt this line, add \`// verify-answer-allow: <reason>\` on the line above.`
|
|
2823
|
+
});
|
|
2824
|
+
}
|
|
2825
|
+
const fileHasAwaitCall = AWAIT_CALL_RE.test(masked);
|
|
2826
|
+
if (fileHasAwaitCall && threshold !== "permissive") {
|
|
2827
|
+
for (let i = 0; i < maskedLines.length; i += 1) {
|
|
2828
|
+
const maskedLine = maskedLines[i] ?? "";
|
|
2829
|
+
if (!LITERAL_TRUE_ASSERT_RE.test(maskedLine)) continue;
|
|
2830
|
+
if (hasLineAllowMarkerNearby2(rawLines, i)) continue;
|
|
2831
|
+
hits.push({
|
|
2832
|
+
rule: "call-succeeded-only",
|
|
2833
|
+
severity: "error",
|
|
2834
|
+
line: i + 1,
|
|
2835
|
+
message: "Found `expect(true).toBe(true)` / `assert(true)` in a file that issues a call. The test proves the callable invocation didn't throw \u2014 not that it produced the right answer. Replace with a value-comparing matcher against the actual output."
|
|
2836
|
+
});
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
return hits;
|
|
2840
|
+
}
|
|
2841
|
+
var verifyAnswerDetector = {
|
|
2842
|
+
id: RATCHET_ID5,
|
|
2843
|
+
stub: false,
|
|
2844
|
+
async run(input) {
|
|
2845
|
+
const threshold = input.config.thresholds[RATCHET_ID5];
|
|
2846
|
+
const files = await walkFiles(input.cwd, (rel) => isTestFile(rel));
|
|
2847
|
+
const findings = [];
|
|
2848
|
+
for (const file of files) {
|
|
2849
|
+
const content = await readRelative(input.cwd, file);
|
|
2850
|
+
for (const hit of inspectFile4({ filePath: file, content, threshold })) {
|
|
2851
|
+
findings.push({
|
|
2852
|
+
ratchet: RATCHET_ID5,
|
|
2853
|
+
rule: hit.rule,
|
|
2854
|
+
file,
|
|
2855
|
+
severity: hit.severity,
|
|
2856
|
+
message: hit.message,
|
|
2857
|
+
...typeof hit.line === "number" ? { line: hit.line } : {}
|
|
2858
|
+
});
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
return {
|
|
2862
|
+
ratchet: RATCHET_ID5,
|
|
2863
|
+
ran: true,
|
|
2864
|
+
findings,
|
|
2865
|
+
filesScanned: files.length,
|
|
2866
|
+
stub: false
|
|
2867
|
+
};
|
|
2868
|
+
}
|
|
2869
|
+
};
|
|
2870
|
+
|
|
2871
|
+
// ../vo-ratchets/src/detectors/index.ts
|
|
2872
|
+
var ALL_DETECTORS = [
|
|
2873
|
+
assertionStrengthDetector,
|
|
2874
|
+
hollowTestsDetector,
|
|
2875
|
+
verifyAnswerDetector,
|
|
2876
|
+
fixStrengthDetector,
|
|
2877
|
+
qaHonestyDetector
|
|
2878
|
+
];
|
|
2879
|
+
|
|
2880
|
+
// ../vo-ratchets/src/runner.ts
|
|
2881
|
+
async function runRatchets(options) {
|
|
2882
|
+
const startedAtMs = Date.now();
|
|
2883
|
+
const startedAt = new Date(startedAtMs).toISOString();
|
|
2884
|
+
const resolved = applyOnlyFilter(resolveConfig(options.config), options.only);
|
|
2885
|
+
const detectorResults = [];
|
|
2886
|
+
for (const detector of ALL_DETECTORS) {
|
|
2887
|
+
if (!resolved.enabled[detector.id]) {
|
|
2888
|
+
detectorResults.push({
|
|
2889
|
+
ratchet: detector.id,
|
|
2890
|
+
ran: false,
|
|
2891
|
+
findings: [],
|
|
2892
|
+
filesScanned: 0,
|
|
2893
|
+
stub: detector.stub
|
|
2894
|
+
});
|
|
2895
|
+
continue;
|
|
2896
|
+
}
|
|
2897
|
+
const result = await detector.run({ cwd: options.cwd, config: resolved });
|
|
2898
|
+
detectorResults.push(result);
|
|
2899
|
+
}
|
|
2900
|
+
const allFindings = detectorResults.flatMap((r) => r.findings);
|
|
2901
|
+
const { kept, suppressed } = partitionByAllowlist(allFindings, resolved.allowlist);
|
|
2902
|
+
const hasErrorFinding = kept.some((f) => f.severity === "error");
|
|
2903
|
+
const failed = !resolved.reportOnly && hasErrorFinding;
|
|
2904
|
+
return {
|
|
2905
|
+
startedAt,
|
|
2906
|
+
durationMs: Date.now() - startedAtMs,
|
|
2907
|
+
cwd: options.cwd,
|
|
2908
|
+
config: resolved,
|
|
2909
|
+
detectors: detectorResults,
|
|
2910
|
+
findings: kept,
|
|
2911
|
+
suppressedFindings: suppressed,
|
|
2912
|
+
failed
|
|
2913
|
+
};
|
|
2914
|
+
}
|
|
2915
|
+
function partitionByAllowlist(findings, allowlist) {
|
|
2916
|
+
const kept = [];
|
|
2917
|
+
const suppressed = [];
|
|
2918
|
+
for (const finding of findings) {
|
|
2919
|
+
if (pathMatchesAny(finding.file, allowlist.paths)) {
|
|
2920
|
+
suppressed.push(finding);
|
|
2921
|
+
continue;
|
|
2922
|
+
}
|
|
2923
|
+
if (allowlist.rules.includes(finding.rule)) {
|
|
2924
|
+
suppressed.push(finding);
|
|
2925
|
+
continue;
|
|
2926
|
+
}
|
|
2927
|
+
if (allowlist.rules.includes(`${finding.ratchet}:${finding.rule}`)) {
|
|
2928
|
+
suppressed.push(finding);
|
|
2929
|
+
continue;
|
|
2930
|
+
}
|
|
2931
|
+
kept.push(finding);
|
|
2932
|
+
}
|
|
2933
|
+
return { kept, suppressed };
|
|
2934
|
+
}
|
|
2935
|
+
|
|
2936
|
+
// src/tools/check-ratchets.ts
|
|
2937
|
+
var TOOL_NAME6 = "vo_check_ratchets";
|
|
2938
|
+
var GATE_TYPE4 = "ratchet";
|
|
2939
|
+
var ALL_RATCHET_IDS = [
|
|
2940
|
+
"assertion-strength",
|
|
2941
|
+
"hollow-tests",
|
|
2942
|
+
"verify-answer",
|
|
2943
|
+
"fix-strength",
|
|
2944
|
+
"qa-honesty"
|
|
2945
|
+
];
|
|
2946
|
+
var inputSchema6 = {
|
|
2947
|
+
type: "object",
|
|
2948
|
+
properties: {
|
|
2949
|
+
cwd: {
|
|
2950
|
+
type: "string",
|
|
2951
|
+
description: "Absolute path the ratchet run resolves files against. Defaults to the MCP server process cwd (the directory the operator launched it from \u2014 typically the project root)."
|
|
2952
|
+
},
|
|
2953
|
+
only: {
|
|
2954
|
+
type: "string",
|
|
2955
|
+
enum: [...ALL_RATCHET_IDS],
|
|
2956
|
+
description: "Restrict the run to a single detector. Equivalent to disabling every other detector. Omit to run all enabled detectors."
|
|
2957
|
+
},
|
|
2958
|
+
report_only: {
|
|
2959
|
+
type: "boolean",
|
|
2960
|
+
description: "When true, the run never reports `failed: true` even on error-severity findings. Useful for diagnostic scans; do NOT set in CI gates."
|
|
2961
|
+
},
|
|
2962
|
+
paths: {
|
|
2963
|
+
type: "array",
|
|
2964
|
+
items: { type: "string" },
|
|
2965
|
+
description: "Optional list of file globs / paths to scan. When omitted, each detector walks the project and applies its own file filters."
|
|
2966
|
+
}
|
|
2967
|
+
},
|
|
2968
|
+
additionalProperties: false
|
|
2969
|
+
};
|
|
2970
|
+
var description6 = "Runs the vo-ratchets library against a project directory and returns the deterministic detector report (assertion-strength, hollow-tests, verify-answer, fix-strength, qa-honesty). Use BEFORE accepting generated tests or fixes to catch hollow assertions, weak verification, and other product-truth bypass patterns. Output includes per-detector findings (file, line, severity, message), suppressed findings (allowlist hits), and a pass/fail signal. Use `only` to scope to one detector for speed. Cache is bypassed \u2014 output reflects the live filesystem state at `cwd`.";
|
|
2971
|
+
function isToolInput6(v) {
|
|
2972
|
+
if (typeof v !== "object" || v === null) return false;
|
|
2973
|
+
const o = v;
|
|
2974
|
+
if (o["cwd"] !== void 0 && typeof o["cwd"] !== "string") return false;
|
|
2975
|
+
if (o["only"] !== void 0) {
|
|
2976
|
+
if (typeof o["only"] !== "string") return false;
|
|
2977
|
+
if (!ALL_RATCHET_IDS.includes(o["only"])) return false;
|
|
2978
|
+
}
|
|
2979
|
+
if (o["report_only"] !== void 0 && typeof o["report_only"] !== "boolean") return false;
|
|
2980
|
+
if (o["paths"] !== void 0) {
|
|
2981
|
+
if (!Array.isArray(o["paths"])) return false;
|
|
2982
|
+
if (!o["paths"].every((p) => typeof p === "string")) return false;
|
|
2983
|
+
}
|
|
2984
|
+
return true;
|
|
2985
|
+
}
|
|
2986
|
+
async function handleCheckRatchets(deps, rawInput, _signal) {
|
|
2987
|
+
if (!isToolInput6(rawInput)) {
|
|
2988
|
+
throw invalidParams(
|
|
2989
|
+
TOOL_NAME6,
|
|
2990
|
+
`invalid input. All fields optional. Shape: { cwd?: string, only?: '${ALL_RATCHET_IDS.join("' | '")}', report_only?: boolean, paths?: string[] }.`
|
|
2991
|
+
);
|
|
2992
|
+
}
|
|
2993
|
+
const cwd = rawInput.cwd ?? process.cwd();
|
|
2994
|
+
const key = deps.cache.keyFor(TOOL_NAME6, rawInput);
|
|
2995
|
+
const excerpt = JSON.stringify({
|
|
2996
|
+
cwd,
|
|
2997
|
+
only: rawInput.only ?? null,
|
|
2998
|
+
report_only: rawInput.report_only ?? false,
|
|
2999
|
+
paths_count: rawInput.paths?.length ?? 0
|
|
3000
|
+
}).slice(0, 300);
|
|
3001
|
+
const inputSizeBytes = bytesOf(JSON.stringify(rawInput));
|
|
3002
|
+
const config = {};
|
|
3003
|
+
if (rawInput.report_only !== void 0) config.reportOnly = rawInput.report_only;
|
|
3004
|
+
if (rawInput.paths !== void 0) config.paths = rawInput.paths;
|
|
3005
|
+
const report = await runRatchets({
|
|
3006
|
+
cwd,
|
|
3007
|
+
...Object.keys(config).length > 0 ? { config } : {},
|
|
3008
|
+
...rawInput.only !== void 0 ? { only: rawInput.only } : {}
|
|
3009
|
+
});
|
|
3010
|
+
const payload = {
|
|
3011
|
+
failed: report.failed,
|
|
3012
|
+
started_at: report.startedAt,
|
|
3013
|
+
duration_ms: report.durationMs,
|
|
3014
|
+
cwd: report.cwd,
|
|
3015
|
+
detectors: report.detectors.map((d) => ({
|
|
3016
|
+
ratchet: d.ratchet,
|
|
3017
|
+
ran: d.ran,
|
|
3018
|
+
files_scanned: d.filesScanned,
|
|
3019
|
+
stub: d.stub,
|
|
3020
|
+
finding_count: d.findings.length
|
|
3021
|
+
})),
|
|
3022
|
+
findings: report.findings.map((f) => ({
|
|
3023
|
+
ratchet: f.ratchet,
|
|
3024
|
+
rule: f.rule,
|
|
3025
|
+
file: f.file,
|
|
3026
|
+
severity: f.severity,
|
|
3027
|
+
message: f.message,
|
|
3028
|
+
...f.line !== void 0 ? { line: f.line } : {}
|
|
3029
|
+
})),
|
|
3030
|
+
suppressed_findings: report.suppressedFindings.map((f) => ({
|
|
3031
|
+
ratchet: f.ratchet,
|
|
3032
|
+
rule: f.rule,
|
|
3033
|
+
file: f.file,
|
|
3034
|
+
severity: f.severity,
|
|
3035
|
+
message: f.message,
|
|
3036
|
+
...f.line !== void 0 ? { line: f.line } : {}
|
|
3037
|
+
})),
|
|
3038
|
+
summary: buildSummary(report)
|
|
3039
|
+
};
|
|
3040
|
+
const envelope = {
|
|
3041
|
+
tool: TOOL_NAME6,
|
|
3042
|
+
schema_version: 1,
|
|
3043
|
+
cache: { hit: false, key },
|
|
3044
|
+
payload
|
|
3045
|
+
};
|
|
3046
|
+
deps.events.append(
|
|
3047
|
+
buildBaseEvent({
|
|
3048
|
+
tool: TOOL_NAME6,
|
|
3049
|
+
gateType: GATE_TYPE4,
|
|
3050
|
+
inputHash: key,
|
|
3051
|
+
inputExcerpt: excerpt,
|
|
3052
|
+
inputSizeBytes,
|
|
3053
|
+
session: deps.session,
|
|
3054
|
+
now: deps.now()
|
|
3055
|
+
})
|
|
3056
|
+
);
|
|
3057
|
+
return jsonContent(envelope);
|
|
3058
|
+
}
|
|
3059
|
+
function buildSummary(report) {
|
|
3060
|
+
const ranCount = report.detectors.filter((d) => d.ran).length;
|
|
3061
|
+
const errorCount = report.findings.filter((f) => f.severity === "error").length;
|
|
3062
|
+
const warnCount = report.findings.filter((f) => f.severity === "warning").length;
|
|
3063
|
+
const infoCount = report.findings.filter((f) => f.severity === "info").length;
|
|
3064
|
+
const verdict = report.failed ? "fail" : "pass";
|
|
3065
|
+
return `verdict=${verdict} detectors_ran=${ranCount}/${report.detectors.length} findings=error:${errorCount} warning:${warnCount} info:${infoCount} suppressed=${report.suppressedFindings.length}`;
|
|
3066
|
+
}
|
|
3067
|
+
|
|
3068
|
+
// src/tools/decompose-dispatch.ts
|
|
3069
|
+
var TOOL_NAME7 = "vo_decompose_dispatch";
|
|
3070
|
+
var GATE_TYPE5 = "plan-review";
|
|
3071
|
+
var MAX_GOAL_BYTES = 32 * 1024;
|
|
3072
|
+
var MAX_CONTEXT_BYTES2 = 256 * 1024;
|
|
3073
|
+
var SYSTEM_PROMPT_VERSION = "v1";
|
|
3074
|
+
var SYSTEM_PROMPT = `You are a dispatch architect for an AI agent fleet operated by a non-coder founder. Given a customer goal, produce a STRUCTURED JSON dispatch plan that decomposes the work into bounded tiers and surfaces the cost shape BEFORE execution begins.
|
|
3075
|
+
|
|
3076
|
+
Tiers (use only those that apply; omit irrelevant phases):
|
|
3077
|
+
- research: subagents read code/docs and produce a finding report
|
|
3078
|
+
- build: subagents implement changes
|
|
3079
|
+
- verify: subagents run tests / smoke / consensus checks
|
|
3080
|
+
- ship: a single agent commits + PRs + merges + deploys
|
|
3081
|
+
|
|
3082
|
+
For each phase you include, specify:
|
|
3083
|
+
- subagent_count how many parallel finite tasks vs single-threaded (1)
|
|
3084
|
+
- model_recommendation model id (claude-opus-4, claude-sonnet-4, gpt-4o, etc.)
|
|
3085
|
+
- estimated_tokens_in integer best-guess input tokens for the phase
|
|
3086
|
+
- estimated_tokens_out integer best-guess output tokens for the phase
|
|
3087
|
+
- estimated_cost_usd USD number (or the string "unknown" if you cannot estimate)
|
|
3088
|
+
- kill_switch one-sentence condition that should HALT this phase
|
|
3089
|
+
- artifacts array of file paths / report names the phase produces
|
|
3090
|
+
|
|
3091
|
+
Output STRICT JSON conforming to this schema (no markdown fences, no prose outside the JSON):
|
|
3092
|
+
|
|
3093
|
+
{
|
|
3094
|
+
"summary": "<one-sentence framing of the overall work>",
|
|
3095
|
+
"phases": [
|
|
3096
|
+
{
|
|
3097
|
+
"name": "research" | "build" | "verify" | "ship",
|
|
3098
|
+
"subagent_count": <integer >= 1>,
|
|
3099
|
+
"model_recommendation": "<model-id>",
|
|
3100
|
+
"estimated_tokens_in": <integer>,
|
|
3101
|
+
"estimated_tokens_out": <integer>,
|
|
3102
|
+
"estimated_cost_usd": <number> | "unknown",
|
|
3103
|
+
"kill_switch": "<condition that should halt this phase>",
|
|
3104
|
+
"artifacts": ["<file or report this phase produces>"]
|
|
3105
|
+
}
|
|
3106
|
+
],
|
|
3107
|
+
"total_estimated_tokens": <integer> | "unknown",
|
|
3108
|
+
"total_estimated_cost_usd": <number> | "unknown",
|
|
3109
|
+
"risks": ["<risk + suggested mitigation>"],
|
|
3110
|
+
"operator_decision_points": ["<question the operator should answer before next phase starts>"]
|
|
3111
|
+
}
|
|
3112
|
+
|
|
3113
|
+
Honesty rules (NON-NEGOTIABLE):
|
|
3114
|
+
- If you cannot estimate a token cost, return the string "unknown" \u2014 do NOT bluff a number.
|
|
3115
|
+
- If a phase requires consensus verification, list it as an artifact dependency on \`vo_consensus_judgment\`, not as work-itself.
|
|
3116
|
+
- The plan is a PROPOSAL, not a commitment \u2014 the operator approves each tier separately.
|
|
3117
|
+
- Surface at least one operator_decision_point unless the goal is trivially scoped.
|
|
3118
|
+
- Surface at least one risk + mitigation. If you genuinely see no risk, say "no significant risk identified; primary failure mode is cost overrun" and explain why.
|
|
3119
|
+
- Do NOT pad the plan with phases that add no value. A 1-file change may only need a build phase.`;
|
|
3120
|
+
var inputSchema7 = {
|
|
3121
|
+
type: "object",
|
|
3122
|
+
properties: {
|
|
3123
|
+
customer_goal: {
|
|
3124
|
+
type: "string",
|
|
3125
|
+
description: "The customer's goal statement \u2014 what they want accomplished. Keep concise; large supporting material belongs in `context`."
|
|
3126
|
+
},
|
|
3127
|
+
context: {
|
|
3128
|
+
type: "object",
|
|
3129
|
+
description: "Optional context: relevant file paths, current state observations, constraints, prior decisions, deadlines. Forwarded to the engine as caller_context.",
|
|
3130
|
+
additionalProperties: true
|
|
3131
|
+
}
|
|
3132
|
+
},
|
|
3133
|
+
required: ["customer_goal"],
|
|
3134
|
+
additionalProperties: false
|
|
3135
|
+
};
|
|
3136
|
+
var description7 = "Decomposes a customer goal into a structured dispatch plan (research / build / verify / ship tiers) with subagent counts, model recommendations, token cost estimates, kill switches, and per-tier artifacts. Runs the consensus engine with gate_type='plan-review' so multiple models each propose a plan, then surfaces the synthesized JSON plan AND each model's raw plan for operator audit. Use BEFORE starting any non-trivial work to make the cost shape visible up front. Operator approves each tier separately; the plan is a proposal, not a commitment.";
|
|
3137
|
+
function isToolInput7(v) {
|
|
3138
|
+
if (typeof v !== "object" || v === null) return false;
|
|
3139
|
+
const o = v;
|
|
3140
|
+
if (typeof o["customer_goal"] !== "string") return false;
|
|
3141
|
+
if (o["context"] !== void 0 && (typeof o["context"] !== "object" || o["context"] === null)) {
|
|
3142
|
+
return false;
|
|
3143
|
+
}
|
|
3144
|
+
return true;
|
|
3145
|
+
}
|
|
3146
|
+
function tryParseDispatchPlan(text) {
|
|
3147
|
+
const trimmed = text.trim();
|
|
3148
|
+
if (trimmed.length === 0) return null;
|
|
3149
|
+
let body = trimmed;
|
|
3150
|
+
const fenceMatch = body.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/u);
|
|
3151
|
+
if (fenceMatch && fenceMatch[1]) body = fenceMatch[1].trim();
|
|
3152
|
+
const firstBrace = body.indexOf("{");
|
|
3153
|
+
const lastBrace = body.lastIndexOf("}");
|
|
3154
|
+
if (firstBrace === -1 || lastBrace === -1 || lastBrace < firstBrace) return null;
|
|
3155
|
+
const jsonStr = body.slice(firstBrace, lastBrace + 1);
|
|
3156
|
+
try {
|
|
3157
|
+
const parsed = JSON.parse(jsonStr);
|
|
3158
|
+
if (!isDispatchPlan(parsed)) return null;
|
|
3159
|
+
return parsed;
|
|
3160
|
+
} catch {
|
|
3161
|
+
return null;
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
function isDispatchPlan(v) {
|
|
3165
|
+
if (typeof v !== "object" || v === null) return false;
|
|
3166
|
+
const o = v;
|
|
3167
|
+
if (typeof o["summary"] !== "string") return false;
|
|
3168
|
+
if (!Array.isArray(o["phases"])) return false;
|
|
3169
|
+
if (!Array.isArray(o["risks"])) return false;
|
|
3170
|
+
if (!Array.isArray(o["operator_decision_points"])) return false;
|
|
3171
|
+
const totalToks = o["total_estimated_tokens"];
|
|
3172
|
+
if (typeof totalToks !== "number" && totalToks !== "unknown") return false;
|
|
3173
|
+
const totalCost = o["total_estimated_cost_usd"];
|
|
3174
|
+
if (typeof totalCost !== "number" && totalCost !== "unknown") return false;
|
|
3175
|
+
for (const phase of o["phases"]) {
|
|
3176
|
+
if (typeof phase !== "object" || phase === null) return false;
|
|
3177
|
+
const p = phase;
|
|
3178
|
+
if (typeof p["name"] !== "string") return false;
|
|
3179
|
+
if (typeof p["subagent_count"] !== "number") return false;
|
|
3180
|
+
}
|
|
3181
|
+
return true;
|
|
3182
|
+
}
|
|
3183
|
+
async function handleDecomposeDispatch(deps, rawInput, signal) {
|
|
3184
|
+
if (!isToolInput7(rawInput)) {
|
|
3185
|
+
throw invalidParams(
|
|
3186
|
+
TOOL_NAME7,
|
|
3187
|
+
"invalid input. Required: { customer_goal: string }. Optional: context (object)."
|
|
3188
|
+
);
|
|
3189
|
+
}
|
|
3190
|
+
assertWithinByteCap(TOOL_NAME7, "customer_goal", rawInput.customer_goal, MAX_GOAL_BYTES);
|
|
3191
|
+
let contextStrForSize = "";
|
|
3192
|
+
if (rawInput.context !== void 0) {
|
|
3193
|
+
try {
|
|
3194
|
+
contextStrForSize = JSON.stringify(rawInput.context);
|
|
3195
|
+
} catch {
|
|
3196
|
+
throw invalidParams(TOOL_NAME7, "input field 'context' is not JSON-serializable.");
|
|
3197
|
+
}
|
|
3198
|
+
assertWithinByteCap(TOOL_NAME7, "context", contextStrForSize, MAX_CONTEXT_BYTES2);
|
|
3199
|
+
}
|
|
3200
|
+
const cacheInput = {
|
|
3201
|
+
system_prompt_version: SYSTEM_PROMPT_VERSION,
|
|
3202
|
+
customer_goal: rawInput.customer_goal,
|
|
3203
|
+
...rawInput.context !== void 0 ? { context: rawInput.context } : {}
|
|
3204
|
+
};
|
|
3205
|
+
const key = deps.cache.keyFor(TOOL_NAME7, cacheInput);
|
|
3206
|
+
const excerpt = rawInput.customer_goal.slice(0, 300);
|
|
3207
|
+
const inputSizeBytes = bytesOf(rawInput.customer_goal) + bytesOf(contextStrForSize);
|
|
3208
|
+
const cached = deps.cache.get(key);
|
|
3209
|
+
if (cached !== null) {
|
|
3210
|
+
const replayEvent = {
|
|
3211
|
+
...buildBaseEvent({
|
|
3212
|
+
tool: TOOL_NAME7,
|
|
3213
|
+
gateType: GATE_TYPE5,
|
|
3214
|
+
inputHash: key,
|
|
3215
|
+
inputExcerpt: excerpt,
|
|
3216
|
+
inputSizeBytes,
|
|
3217
|
+
session: deps.session,
|
|
3218
|
+
now: deps.now()
|
|
3219
|
+
}),
|
|
3220
|
+
per_model_verdicts: cached.value.payload.per_model_plans.map((p) => ({
|
|
3221
|
+
model: p.model_id,
|
|
3222
|
+
model_id: p.model_id,
|
|
3223
|
+
provider: "cache-replay",
|
|
3224
|
+
verdict: p.parse_ok ? "pass" : "uncertain",
|
|
3225
|
+
confidence: p.confidence,
|
|
3226
|
+
duration_ms: 0,
|
|
3227
|
+
raw_response_excerpt: p.plan_text.slice(0, 500),
|
|
3228
|
+
raw_response_hash: "",
|
|
3229
|
+
reasoning_summary: null,
|
|
3230
|
+
cited_sources: [],
|
|
3231
|
+
error: null
|
|
3232
|
+
})),
|
|
3233
|
+
consensus_engine_version: cached.value.payload.engine_version ?? null,
|
|
3234
|
+
cache_hit: true
|
|
3235
|
+
};
|
|
3236
|
+
deps.events.append(replayEvent);
|
|
3237
|
+
const replayEnvelope = {
|
|
3238
|
+
...cached.value,
|
|
3239
|
+
cache: { hit: true, key }
|
|
3240
|
+
};
|
|
3241
|
+
return jsonContent(replayEnvelope);
|
|
3242
|
+
}
|
|
3243
|
+
const fullPrompt = `${SYSTEM_PROMPT}
|
|
3244
|
+
|
|
3245
|
+
---
|
|
3246
|
+
|
|
3247
|
+
Customer goal:
|
|
3248
|
+
${rawInput.customer_goal}
|
|
3249
|
+
|
|
3250
|
+
Produce the JSON dispatch plan now.`;
|
|
3251
|
+
const baseEvent = buildBaseEvent({
|
|
3252
|
+
tool: TOOL_NAME7,
|
|
3253
|
+
gateType: GATE_TYPE5,
|
|
3254
|
+
inputHash: key,
|
|
3255
|
+
inputExcerpt: excerpt,
|
|
3256
|
+
inputSizeBytes,
|
|
3257
|
+
session: deps.session,
|
|
3258
|
+
now: deps.now()
|
|
3259
|
+
});
|
|
3260
|
+
let engineResult;
|
|
3261
|
+
let engineThrew = false;
|
|
3262
|
+
try {
|
|
3263
|
+
engineResult = await deps.consensus.run({
|
|
3264
|
+
gate_type: GATE_TYPE5,
|
|
3265
|
+
prompt: fullPrompt,
|
|
3266
|
+
...rawInput.context !== void 0 ? { caller_context: rawInput.context } : {},
|
|
3267
|
+
...signal !== void 0 ? { signal } : {}
|
|
3268
|
+
});
|
|
3269
|
+
} catch (err) {
|
|
3270
|
+
engineThrew = true;
|
|
3271
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3272
|
+
engineResult = { ok: false, reason: `engine-threw: ${message}` };
|
|
3273
|
+
}
|
|
3274
|
+
if (!engineResult.ok && engineResult.reason === "cancelled") {
|
|
3275
|
+
deps.events.append({
|
|
3276
|
+
...baseEvent,
|
|
3277
|
+
downstream_outcome: {
|
|
3278
|
+
status: "cancelled",
|
|
3279
|
+
notes: "mcp notifications/cancelled \u2014 handler aborted via SDK signal"
|
|
3280
|
+
}
|
|
3281
|
+
});
|
|
3282
|
+
const e = new Error("Request cancelled by client (notifications/cancelled)");
|
|
3283
|
+
e.name = "AbortError";
|
|
3284
|
+
throw e;
|
|
3285
|
+
}
|
|
3286
|
+
if (!engineResult.ok) {
|
|
3287
|
+
const payload2 = {
|
|
3288
|
+
verdict: "unimplemented",
|
|
3289
|
+
reason: engineResult.reason,
|
|
3290
|
+
dispatch_plan: null,
|
|
3291
|
+
per_model_plans: [],
|
|
3292
|
+
synthesized_reasoning: "",
|
|
3293
|
+
consensus_confidence: 0,
|
|
3294
|
+
...engineThrew ? { degraded_mode: true } : {}
|
|
3295
|
+
};
|
|
3296
|
+
const envelope2 = {
|
|
3297
|
+
tool: TOOL_NAME7,
|
|
3298
|
+
schema_version: 1,
|
|
3299
|
+
cache: { hit: false, key },
|
|
3300
|
+
payload: payload2
|
|
3301
|
+
};
|
|
3302
|
+
deps.events.append(baseEvent);
|
|
3303
|
+
return jsonContent(envelope2);
|
|
3304
|
+
}
|
|
3305
|
+
const perModelPlans = engineResult.per_model_verdicts.map((v) => {
|
|
3306
|
+
const parsed = tryParseDispatchPlan(v.raw_response_excerpt);
|
|
3307
|
+
return {
|
|
3308
|
+
model_id: v.model,
|
|
3309
|
+
plan_text: v.raw_response_excerpt,
|
|
3310
|
+
confidence: v.confidence,
|
|
3311
|
+
parse_ok: parsed !== null
|
|
3312
|
+
};
|
|
3313
|
+
});
|
|
3314
|
+
const synthText = engineResult.synthesized_verdict.reasoning_excerpt;
|
|
3315
|
+
const synthPlan = tryParseDispatchPlan(synthText);
|
|
3316
|
+
const payload = {
|
|
3317
|
+
verdict: synthPlan !== null ? "pass" : "uncertain",
|
|
3318
|
+
reason: synthPlan !== null ? "Synthesized dispatch plan parsed successfully" : "Synthesized verdict did not parse as a valid dispatch plan JSON \u2014 see per_model_plans for raw outputs",
|
|
3319
|
+
dispatch_plan: synthPlan,
|
|
3320
|
+
...synthPlan === null ? { parse_error: "synthesized verdict text was not valid dispatch-plan JSON" } : {},
|
|
3321
|
+
per_model_plans: perModelPlans,
|
|
3322
|
+
synthesized_reasoning: synthText,
|
|
3323
|
+
consensus_confidence: engineResult.synthesized_verdict.confidence,
|
|
3324
|
+
engine_version: engineResult.engine_version,
|
|
3325
|
+
...engineResult.per_model_verdicts.length < 3 ? { degraded: true } : {}
|
|
3326
|
+
};
|
|
3327
|
+
const envelope = {
|
|
3328
|
+
tool: TOOL_NAME7,
|
|
3329
|
+
schema_version: 1,
|
|
3330
|
+
cache: { hit: false, key },
|
|
3331
|
+
payload
|
|
3332
|
+
};
|
|
3333
|
+
deps.cache.set(key, envelope);
|
|
3334
|
+
const enrichedEvent = {
|
|
3335
|
+
...baseEvent,
|
|
3336
|
+
consensus_confidence: engineResult.synthesized_verdict.confidence,
|
|
3337
|
+
duration_ms: engineResult.duration_ms,
|
|
3338
|
+
consensus_engine_version: engineResult.engine_version,
|
|
3339
|
+
per_model_verdicts: toEventPerModelVerdicts(engineResult.per_model_verdicts),
|
|
3340
|
+
synthesized_verdict: toEventSynthesizedVerdict(engineResult.synthesized_verdict)
|
|
3341
|
+
};
|
|
3342
|
+
deps.events.append(enrichedEvent);
|
|
3343
|
+
return jsonContent(envelope);
|
|
3344
|
+
}
|
|
3345
|
+
|
|
3346
|
+
// src/cloud/credential-store.ts
|
|
3347
|
+
import { homedir as homedir3 } from "node:os";
|
|
3348
|
+
import { join as join5, dirname as dirname4 } from "node:path";
|
|
3349
|
+
import {
|
|
3350
|
+
existsSync as existsSync3,
|
|
3351
|
+
mkdirSync as mkdirSync2,
|
|
3352
|
+
readFileSync as readFileSync5,
|
|
3353
|
+
writeFileSync as writeFileSync2,
|
|
3354
|
+
chmodSync as chmodSync2,
|
|
3355
|
+
rmSync
|
|
3356
|
+
} from "node:fs";
|
|
3357
|
+
|
|
3358
|
+
// src/cloud/keychain.ts
|
|
3359
|
+
import { createRequire } from "node:module";
|
|
3360
|
+
|
|
3361
|
+
// src/cloud/admin-callable-client.ts
|
|
3362
|
+
var AdminCallableError = class extends Error {
|
|
3363
|
+
constructor(status, path3, message) {
|
|
3364
|
+
super(message);
|
|
3365
|
+
this.status = status;
|
|
3366
|
+
this.path = path3;
|
|
3367
|
+
this.name = "AdminCallableError";
|
|
3368
|
+
}
|
|
3369
|
+
status;
|
|
3370
|
+
path;
|
|
3371
|
+
};
|
|
3372
|
+
|
|
3373
|
+
// src/tools/cloud-call.ts
|
|
3374
|
+
var ADMIN_READONLY_GATE_REASON = "admin callables are in read-only mode (VO_ADMIN_CALLABLES_READONLY) \u2014 this write tool is gated to its stub. Unset VO_ADMIN_CALLABLES_READONLY to enable write tools.";
|
|
3375
|
+
async function buildCloudOrStubResponse(args) {
|
|
3376
|
+
const inputJson = JSON.stringify(args.normalizedInput);
|
|
3377
|
+
const excerpt = inputJson.slice(0, 300);
|
|
3378
|
+
const key = args.deps.cache.keyFor(args.toolName, args.normalizedInput);
|
|
3379
|
+
const event = buildBaseEvent({
|
|
3380
|
+
tool: args.toolName,
|
|
3381
|
+
gateType: args.gateType,
|
|
3382
|
+
inputHash: key,
|
|
3383
|
+
inputExcerpt: excerpt,
|
|
3384
|
+
inputSizeBytes: bytesOf(inputJson),
|
|
3385
|
+
session: args.deps.session,
|
|
3386
|
+
now: args.deps.now()
|
|
3387
|
+
});
|
|
3388
|
+
const gatedByReadonly = Boolean(args.deps.adminCallables?.readOnly) && !args.readOnly;
|
|
3389
|
+
if (!args.deps.adminCallables || gatedByReadonly) {
|
|
3390
|
+
const payload = {
|
|
3391
|
+
verdict: "unimplemented",
|
|
3392
|
+
reason: gatedByReadonly ? ADMIN_READONLY_GATE_REASON : args.stubReason,
|
|
3393
|
+
callable: args.callableName,
|
|
3394
|
+
normalized_input: args.normalizedInput
|
|
3395
|
+
};
|
|
3396
|
+
const envelope = {
|
|
3397
|
+
tool: args.toolName,
|
|
3398
|
+
schema_version: 1,
|
|
3399
|
+
cache: { hit: false, key },
|
|
3400
|
+
payload
|
|
3401
|
+
};
|
|
3402
|
+
args.deps.events.append(event);
|
|
3403
|
+
return jsonContent(envelope);
|
|
3404
|
+
}
|
|
3405
|
+
try {
|
|
3406
|
+
const result = await args.deps.adminCallables.invoke(
|
|
3407
|
+
args.adminPath,
|
|
3408
|
+
args.cloudBody ?? args.normalizedInput,
|
|
3409
|
+
args.rawEnvelope ? { rawEnvelope: true } : void 0
|
|
3410
|
+
);
|
|
3411
|
+
const payload = {
|
|
3412
|
+
verdict: "pass",
|
|
3413
|
+
reason: `forwarded to ${args.callableName} via vo-control-plane ${args.adminPath}`,
|
|
3414
|
+
callable: args.callableName,
|
|
3415
|
+
normalized_input: args.normalizedInput,
|
|
3416
|
+
response_data: result && typeof result === "object" && !Array.isArray(result) ? result : { value: result }
|
|
3417
|
+
};
|
|
3418
|
+
const envelope = {
|
|
3419
|
+
tool: args.toolName,
|
|
3420
|
+
schema_version: 1,
|
|
3421
|
+
cache: { hit: false, key },
|
|
3422
|
+
payload
|
|
3423
|
+
};
|
|
3424
|
+
args.deps.events.append(event);
|
|
3425
|
+
return jsonContent(envelope);
|
|
3426
|
+
} catch (err) {
|
|
3427
|
+
const status = err instanceof AdminCallableError ? err.status : void 0;
|
|
3428
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3429
|
+
const payload = {
|
|
3430
|
+
verdict: "fail",
|
|
3431
|
+
reason: status !== void 0 ? `vo-control-plane returned HTTP ${status}: ${message.slice(0, 300)}` : `cloud invocation failed: ${message.slice(0, 300)}`,
|
|
3432
|
+
callable: args.callableName,
|
|
3433
|
+
normalized_input: args.normalizedInput
|
|
3434
|
+
};
|
|
3435
|
+
const envelope = {
|
|
3436
|
+
tool: args.toolName,
|
|
3437
|
+
schema_version: 1,
|
|
3438
|
+
cache: { hit: false, key },
|
|
3439
|
+
payload
|
|
3440
|
+
};
|
|
3441
|
+
args.deps.events.append(event);
|
|
3442
|
+
return jsonContent(envelope);
|
|
3443
|
+
}
|
|
3444
|
+
}
|
|
3445
|
+
|
|
3446
|
+
// src/tools/heal/common-heal.ts
|
|
3447
|
+
var HEAL_STUB_REASON = 'cloud-mode not yet wired; tool surface is live, admin-callable wiring pending vo-cloud-tenant-model dispatch (see packages/vo-mcp/src/modes/cloud.ts + EXTRACTION_AUDIT.md "Stub remaining")';
|
|
3448
|
+
var HEAL_GATE_TYPE = "admin-action";
|
|
3449
|
+
|
|
3450
|
+
// src/tools/heal/trigger-heal.ts
|
|
3451
|
+
var TOOL_NAME8 = "vo_trigger_heal";
|
|
3452
|
+
var CALLABLE_NAME = "voTriggerHeal";
|
|
3453
|
+
var ADMIN_PATH = "/api/v1/admin/heal/trigger";
|
|
3454
|
+
var inputSchema8 = {
|
|
3455
|
+
type: "object",
|
|
3456
|
+
properties: {
|
|
3457
|
+
focus_page: {
|
|
3458
|
+
type: "string",
|
|
3459
|
+
description: "Optional focus page identifier (maps to a known tester via vo-heal-focus state). When set, the heal pass becomes priority-queued for that tester. Omit to trigger the auto-process queue."
|
|
3460
|
+
},
|
|
3461
|
+
force: {
|
|
3462
|
+
type: "boolean",
|
|
3463
|
+
description: "When true, request the heal pass even if a queue manager is already active. Default false."
|
|
3464
|
+
}
|
|
3465
|
+
},
|
|
3466
|
+
additionalProperties: false
|
|
3467
|
+
};
|
|
3468
|
+
var description8 = "Triggers a self-heal pass against open PRs. Optionally scope to a `focus_page` (priority queue for one tester) or omit to fire the auto-process queue. Wraps the `voTriggerHeal` admin Cloud Function. V1 stub: returns `verdict: 'unimplemented'` with structured normalized_input \u2014 cloud-mode wiring is pending. The contract is locked; consumers can call this tool today and get the correct surface without working execution.";
|
|
3469
|
+
function isToolInput8(v) {
|
|
3470
|
+
if (typeof v !== "object" || v === null) return false;
|
|
3471
|
+
const o = v;
|
|
3472
|
+
if (o["focus_page"] !== void 0 && typeof o["focus_page"] !== "string") return false;
|
|
3473
|
+
if (o["force"] !== void 0 && typeof o["force"] !== "boolean") return false;
|
|
3474
|
+
return true;
|
|
3475
|
+
}
|
|
3476
|
+
async function handleTriggerHeal(deps, rawInput, _signal) {
|
|
3477
|
+
if (!isToolInput8(rawInput)) {
|
|
3478
|
+
throw invalidParams(
|
|
3479
|
+
TOOL_NAME8,
|
|
3480
|
+
"invalid input. All fields optional. Shape: { focus_page?: string, force?: boolean }."
|
|
3481
|
+
);
|
|
3482
|
+
}
|
|
3483
|
+
const normalizedInput = {};
|
|
3484
|
+
const cloudBody = {};
|
|
3485
|
+
if (rawInput.focus_page !== void 0) {
|
|
3486
|
+
normalizedInput["focus_page"] = rawInput.focus_page;
|
|
3487
|
+
cloudBody["focusPage"] = rawInput.focus_page;
|
|
3488
|
+
}
|
|
3489
|
+
if (rawInput.force !== void 0) {
|
|
3490
|
+
normalizedInput["force"] = rawInput.force;
|
|
3491
|
+
cloudBody["force"] = rawInput.force;
|
|
3492
|
+
}
|
|
3493
|
+
return buildCloudOrStubResponse({
|
|
3494
|
+
toolName: TOOL_NAME8,
|
|
3495
|
+
callableName: CALLABLE_NAME,
|
|
3496
|
+
adminPath: ADMIN_PATH,
|
|
3497
|
+
normalizedInput,
|
|
3498
|
+
cloudBody,
|
|
3499
|
+
gateType: HEAL_GATE_TYPE,
|
|
3500
|
+
stubReason: HEAL_STUB_REASON,
|
|
3501
|
+
deps
|
|
3502
|
+
});
|
|
3503
|
+
}
|
|
3504
|
+
|
|
3505
|
+
// src/tools/heal/fix-retry.ts
|
|
3506
|
+
var TOOL_NAME9 = "vo_fix_retry";
|
|
3507
|
+
var MAX_BATCH = 50;
|
|
3508
|
+
var ADMIN_PATH_SINGLE = "/api/v1/admin/heal/retry-attempt";
|
|
3509
|
+
var ADMIN_PATH_BATCH = "/api/v1/admin/heal/retry-attempts";
|
|
3510
|
+
var inputSchema9 = {
|
|
3511
|
+
type: "object",
|
|
3512
|
+
properties: {
|
|
3513
|
+
attempt_id: {
|
|
3514
|
+
type: "string",
|
|
3515
|
+
description: "Retry a single fix attempt by id (wraps voRetryFixAttempt). Mutually exclusive with attempt_ids."
|
|
3516
|
+
},
|
|
3517
|
+
attempt_ids: {
|
|
3518
|
+
type: "array",
|
|
3519
|
+
items: { type: "string" },
|
|
3520
|
+
description: "Retry up to 50 fix attempts by id (wraps voRetryFixAttempts). Mutually exclusive with attempt_id."
|
|
3521
|
+
}
|
|
3522
|
+
},
|
|
3523
|
+
additionalProperties: false
|
|
3524
|
+
};
|
|
3525
|
+
var description9 = "Retries one or more failed fix attempts by id. Pass `attempt_id` for the single case or `attempt_ids` (up to 50) for the batch case. Wraps `voRetryFixAttempt` / `voRetryFixAttempts` admin Cloud Functions. V1 stub: returns `verdict: 'unimplemented'` \u2014 cloud-mode wiring pending.";
|
|
3526
|
+
function isToolInput9(v) {
|
|
3527
|
+
if (typeof v !== "object" || v === null) return false;
|
|
3528
|
+
const o = v;
|
|
3529
|
+
if (o["attempt_id"] !== void 0 && typeof o["attempt_id"] !== "string") return false;
|
|
3530
|
+
if (o["attempt_ids"] !== void 0) {
|
|
3531
|
+
if (!Array.isArray(o["attempt_ids"])) return false;
|
|
3532
|
+
if (!o["attempt_ids"].every((id) => typeof id === "string")) return false;
|
|
3533
|
+
}
|
|
3534
|
+
return true;
|
|
3535
|
+
}
|
|
3536
|
+
async function handleFixRetry(deps, rawInput, _signal) {
|
|
3537
|
+
if (!isToolInput9(rawInput)) {
|
|
3538
|
+
throw invalidParams(
|
|
3539
|
+
TOOL_NAME9,
|
|
3540
|
+
"invalid input. Shape: { attempt_id: string } OR { attempt_ids: string[] } (mutually exclusive)."
|
|
3541
|
+
);
|
|
3542
|
+
}
|
|
3543
|
+
const { attempt_id, attempt_ids } = rawInput;
|
|
3544
|
+
const hasSingle = typeof attempt_id === "string" && attempt_id.trim().length > 0;
|
|
3545
|
+
const hasBatch = Array.isArray(attempt_ids) && attempt_ids.length > 0;
|
|
3546
|
+
if (!hasSingle && !hasBatch) {
|
|
3547
|
+
throw invalidParams(
|
|
3548
|
+
TOOL_NAME9,
|
|
3549
|
+
"either attempt_id (string) or attempt_ids (non-empty array) is required."
|
|
3550
|
+
);
|
|
3551
|
+
}
|
|
3552
|
+
if (hasSingle && hasBatch) {
|
|
3553
|
+
throw invalidParams(
|
|
3554
|
+
TOOL_NAME9,
|
|
3555
|
+
"attempt_id and attempt_ids are mutually exclusive \u2014 pass exactly one."
|
|
3556
|
+
);
|
|
3557
|
+
}
|
|
3558
|
+
if (Array.isArray(attempt_ids) && attempt_ids.length > MAX_BATCH) {
|
|
3559
|
+
throw invalidParams(
|
|
3560
|
+
TOOL_NAME9,
|
|
3561
|
+
`attempt_ids exceeds batch cap of ${MAX_BATCH} (got ${attempt_ids.length}).`
|
|
3562
|
+
);
|
|
3563
|
+
}
|
|
3564
|
+
const normalizedInput = {};
|
|
3565
|
+
const cloudBody = {};
|
|
3566
|
+
let resolvedCallable;
|
|
3567
|
+
let resolvedAdminPath;
|
|
3568
|
+
if (typeof attempt_id === "string" && attempt_id.trim().length > 0) {
|
|
3569
|
+
const trimmed = attempt_id.trim();
|
|
3570
|
+
normalizedInput["attempt_id"] = trimmed;
|
|
3571
|
+
cloudBody["attemptId"] = trimmed;
|
|
3572
|
+
resolvedCallable = "voRetryFixAttempt";
|
|
3573
|
+
resolvedAdminPath = ADMIN_PATH_SINGLE;
|
|
3574
|
+
} else if (Array.isArray(attempt_ids)) {
|
|
3575
|
+
const cleaned = [
|
|
3576
|
+
...new Set(attempt_ids.map((id) => id.trim()).filter(Boolean))
|
|
3577
|
+
].slice(0, MAX_BATCH);
|
|
3578
|
+
normalizedInput["attempt_ids"] = cleaned;
|
|
3579
|
+
cloudBody["attemptIds"] = cleaned;
|
|
3580
|
+
resolvedCallable = "voRetryFixAttempts";
|
|
3581
|
+
resolvedAdminPath = ADMIN_PATH_BATCH;
|
|
3582
|
+
} else {
|
|
3583
|
+
throw invalidParams(TOOL_NAME9, "internal validation skipped \u2014 please report.");
|
|
3584
|
+
}
|
|
3585
|
+
return buildCloudOrStubResponse({
|
|
3586
|
+
toolName: TOOL_NAME9,
|
|
3587
|
+
callableName: resolvedCallable,
|
|
3588
|
+
adminPath: resolvedAdminPath,
|
|
3589
|
+
normalizedInput,
|
|
3590
|
+
cloudBody,
|
|
3591
|
+
gateType: HEAL_GATE_TYPE,
|
|
3592
|
+
stubReason: HEAL_STUB_REASON,
|
|
3593
|
+
deps
|
|
3594
|
+
});
|
|
3595
|
+
}
|
|
3596
|
+
|
|
3597
|
+
// src/tools/heal/fix-clear.ts
|
|
3598
|
+
var TOOL_NAME10 = "vo_fix_clear";
|
|
3599
|
+
var CALLABLE_NAME2 = "voClearFixAttempt";
|
|
3600
|
+
var ADMIN_PATH2 = "/api/v1/admin/heal/clear-attempt";
|
|
3601
|
+
var inputSchema10 = {
|
|
3602
|
+
type: "object",
|
|
3603
|
+
properties: {
|
|
3604
|
+
attempt_id: {
|
|
3605
|
+
type: "string",
|
|
3606
|
+
description: "Required. Fix-attempt id in the virtual_office_attempts collection."
|
|
3607
|
+
}
|
|
3608
|
+
},
|
|
3609
|
+
required: ["attempt_id"],
|
|
3610
|
+
additionalProperties: false
|
|
3611
|
+
};
|
|
3612
|
+
var description10 = "Clears (cancels) a single fix attempt by id. Wraps `voClearFixAttempt` admin Cloud Function. V1 stub: returns `verdict: 'unimplemented'` \u2014 cloud-mode wiring pending.";
|
|
3613
|
+
function isToolInput10(v) {
|
|
3614
|
+
if (typeof v !== "object" || v === null) return false;
|
|
3615
|
+
const o = v;
|
|
3616
|
+
if (typeof o["attempt_id"] !== "string") return false;
|
|
3617
|
+
if (o["attempt_id"].trim().length === 0) return false;
|
|
3618
|
+
return true;
|
|
3619
|
+
}
|
|
3620
|
+
async function handleFixClear(deps, rawInput, _signal) {
|
|
3621
|
+
if (!isToolInput10(rawInput)) {
|
|
3622
|
+
throw invalidParams(
|
|
3623
|
+
TOOL_NAME10,
|
|
3624
|
+
"invalid input. Shape: { attempt_id: non-empty string }."
|
|
3625
|
+
);
|
|
3626
|
+
}
|
|
3627
|
+
const trimmed = rawInput.attempt_id.trim();
|
|
3628
|
+
return buildCloudOrStubResponse({
|
|
3629
|
+
toolName: TOOL_NAME10,
|
|
3630
|
+
callableName: CALLABLE_NAME2,
|
|
3631
|
+
adminPath: ADMIN_PATH2,
|
|
3632
|
+
normalizedInput: { attempt_id: trimmed },
|
|
3633
|
+
cloudBody: { attemptId: trimmed },
|
|
3634
|
+
gateType: HEAL_GATE_TYPE,
|
|
3635
|
+
stubReason: HEAL_STUB_REASON,
|
|
3636
|
+
deps
|
|
3637
|
+
});
|
|
3638
|
+
}
|
|
3639
|
+
|
|
3640
|
+
// src/tools/heal/stop-workflow.ts
|
|
3641
|
+
var TOOL_NAME11 = "vo_stop_workflow";
|
|
3642
|
+
var CALLABLE_NAME3 = "voStopWorkflow";
|
|
3643
|
+
var ADMIN_PATH3 = "/api/v1/admin/workflow/stop";
|
|
3644
|
+
var inputSchema11 = {
|
|
3645
|
+
type: "object",
|
|
3646
|
+
properties: {
|
|
3647
|
+
run_id: {
|
|
3648
|
+
type: "number",
|
|
3649
|
+
description: "Required. GitHub Actions workflow run id (positive integer)."
|
|
3650
|
+
},
|
|
3651
|
+
force: {
|
|
3652
|
+
type: "boolean",
|
|
3653
|
+
description: "Optional. When true, force-cancel even if the run looks healthy. Default false."
|
|
3654
|
+
}
|
|
3655
|
+
},
|
|
3656
|
+
required: ["run_id"],
|
|
3657
|
+
additionalProperties: false
|
|
3658
|
+
};
|
|
3659
|
+
var description11 = "Cancels a running GitHub Actions workflow by run id. Wraps `voStopWorkflow` admin Cloud Function. V1 stub: returns `verdict: 'unimplemented'` \u2014 cloud-mode wiring pending.";
|
|
3660
|
+
function isToolInput11(v) {
|
|
3661
|
+
if (typeof v !== "object" || v === null) return false;
|
|
3662
|
+
const o = v;
|
|
3663
|
+
if (typeof o["run_id"] !== "number") return false;
|
|
3664
|
+
if (!Number.isFinite(o["run_id"]) || o["run_id"] <= 0 || !Number.isInteger(o["run_id"])) {
|
|
3665
|
+
return false;
|
|
3666
|
+
}
|
|
3667
|
+
if (o["force"] !== void 0 && typeof o["force"] !== "boolean") return false;
|
|
3668
|
+
return true;
|
|
3669
|
+
}
|
|
3670
|
+
async function handleStopWorkflow(deps, rawInput, _signal) {
|
|
3671
|
+
if (!isToolInput11(rawInput)) {
|
|
3672
|
+
throw invalidParams(
|
|
3673
|
+
TOOL_NAME11,
|
|
3674
|
+
"invalid input. Shape: { run_id: positive integer, force?: boolean }."
|
|
3675
|
+
);
|
|
3676
|
+
}
|
|
3677
|
+
const normalizedInput = { run_id: rawInput.run_id };
|
|
3678
|
+
const cloudBody = { runId: rawInput.run_id };
|
|
3679
|
+
if (rawInput.force !== void 0) {
|
|
3680
|
+
normalizedInput["force"] = rawInput.force;
|
|
3681
|
+
cloudBody["force"] = rawInput.force;
|
|
3682
|
+
}
|
|
3683
|
+
return buildCloudOrStubResponse({
|
|
3684
|
+
toolName: TOOL_NAME11,
|
|
3685
|
+
callableName: CALLABLE_NAME3,
|
|
3686
|
+
adminPath: ADMIN_PATH3,
|
|
3687
|
+
normalizedInput,
|
|
3688
|
+
cloudBody,
|
|
3689
|
+
gateType: HEAL_GATE_TYPE,
|
|
3690
|
+
stubReason: HEAL_STUB_REASON,
|
|
3691
|
+
deps
|
|
3692
|
+
});
|
|
3693
|
+
}
|
|
3694
|
+
|
|
3695
|
+
// src/tools/heal/get-workflow-runs.ts
|
|
3696
|
+
var TOOL_NAME12 = "vo_get_workflow_runs";
|
|
3697
|
+
var CALLABLE_NAME4 = "voGetWorkflowRuns";
|
|
3698
|
+
var ADMIN_PATH4 = "/api/v1/admin/workflow/runs";
|
|
3699
|
+
var inputSchema12 = {
|
|
3700
|
+
type: "object",
|
|
3701
|
+
properties: {},
|
|
3702
|
+
additionalProperties: false
|
|
3703
|
+
};
|
|
3704
|
+
var description12 = "Returns the current Command Center workflow-runs snapshot (Heal, Manager, Auto-Merge, Deploy on Merge, etc.). Wraps `voGetWorkflowRuns` admin Cloud Function. Read-only diagnostic. V1 stub: returns `verdict: 'unimplemented'` \u2014 cloud-mode wiring pending.";
|
|
3705
|
+
function isToolInput12(v) {
|
|
3706
|
+
if (typeof v !== "object" || v === null) return false;
|
|
3707
|
+
return true;
|
|
3708
|
+
}
|
|
3709
|
+
async function handleGetWorkflowRuns(deps, rawInput, _signal) {
|
|
3710
|
+
if (!isToolInput12(rawInput)) {
|
|
3711
|
+
throw invalidParams(TOOL_NAME12, "invalid input. This tool takes no parameters.");
|
|
3712
|
+
}
|
|
3713
|
+
return buildCloudOrStubResponse({
|
|
3714
|
+
toolName: TOOL_NAME12,
|
|
3715
|
+
callableName: CALLABLE_NAME4,
|
|
3716
|
+
adminPath: ADMIN_PATH4,
|
|
3717
|
+
normalizedInput: {},
|
|
3718
|
+
gateType: HEAL_GATE_TYPE,
|
|
3719
|
+
stubReason: HEAL_STUB_REASON,
|
|
3720
|
+
// Read-only diagnostic — stays live under VO_ADMIN_CALLABLES_READONLY.
|
|
3721
|
+
readOnly: true,
|
|
3722
|
+
deps
|
|
3723
|
+
});
|
|
3724
|
+
}
|
|
3725
|
+
|
|
3726
|
+
// src/tools/pr/common-pr.ts
|
|
3727
|
+
var PR_STUB_REASON = 'cloud-mode not yet wired; tool surface is live, admin-callable wiring pending vo-cloud-tenant-model dispatch (see packages/vo-mcp/src/modes/cloud.ts + EXTRACTION_AUDIT.md "Stub remaining")';
|
|
3728
|
+
var PR_GATE_TYPE = "admin-action";
|
|
3729
|
+
|
|
3730
|
+
// src/tools/pr/list-pending-prs.ts
|
|
3731
|
+
var TOOL_NAME13 = "vo_list_pending_prs";
|
|
3732
|
+
var CALLABLE_NAME5 = "voListPendingPRs";
|
|
3733
|
+
var ADMIN_PATH5 = "/api/v1/admin/pr/list";
|
|
3734
|
+
var inputSchema13 = {
|
|
3735
|
+
type: "object",
|
|
3736
|
+
properties: {},
|
|
3737
|
+
additionalProperties: false
|
|
3738
|
+
};
|
|
3739
|
+
var description13 = "Lists open VO-source pull requests with blocker / source / tester / specialist-context metadata. Read-only diagnostic for Command Center reads. Wraps `voListPendingPRs` admin Cloud Function. V1 stub: returns `verdict: 'unimplemented'` \u2014 cloud-mode wiring pending.";
|
|
3740
|
+
function isToolInput13(v) {
|
|
3741
|
+
return typeof v === "object" && v !== null;
|
|
3742
|
+
}
|
|
3743
|
+
async function handleListPendingPRs(deps, rawInput, _signal) {
|
|
3744
|
+
if (!isToolInput13(rawInput)) {
|
|
3745
|
+
throw invalidParams(TOOL_NAME13, "invalid input. This tool takes no parameters.");
|
|
3746
|
+
}
|
|
3747
|
+
return buildCloudOrStubResponse({
|
|
3748
|
+
toolName: TOOL_NAME13,
|
|
3749
|
+
callableName: CALLABLE_NAME5,
|
|
3750
|
+
adminPath: ADMIN_PATH5,
|
|
3751
|
+
normalizedInput: {},
|
|
3752
|
+
gateType: PR_GATE_TYPE,
|
|
3753
|
+
stubReason: PR_STUB_REASON,
|
|
3754
|
+
// Read-only diagnostic — stays live under VO_ADMIN_CALLABLES_READONLY.
|
|
3755
|
+
readOnly: true,
|
|
3756
|
+
deps
|
|
3757
|
+
});
|
|
3758
|
+
}
|
|
3759
|
+
|
|
3760
|
+
// src/tools/pr/merge-pr.ts
|
|
3761
|
+
var TOOL_NAME14 = "vo_merge_pr";
|
|
3762
|
+
var CALLABLE_NAME6 = "voMergePR";
|
|
3763
|
+
var ADMIN_PATH6 = "/api/v1/admin/pr/merge";
|
|
3764
|
+
var inputSchema14 = {
|
|
3765
|
+
type: "object",
|
|
3766
|
+
properties: {
|
|
3767
|
+
pr_number: {
|
|
3768
|
+
type: "number",
|
|
3769
|
+
description: "Required. GitHub pull request number (positive integer)."
|
|
3770
|
+
}
|
|
3771
|
+
},
|
|
3772
|
+
required: ["pr_number"],
|
|
3773
|
+
additionalProperties: false
|
|
3774
|
+
};
|
|
3775
|
+
var description14 = "Approves + merges a single VO-source pull request by number. Wraps `voMergePR` admin Cloud Function (server-side refuses non-VO PRs with permission-denied). V1 stub: returns `verdict: 'unimplemented'` \u2014 cloud-mode wiring pending.";
|
|
3776
|
+
function isToolInput14(v) {
|
|
3777
|
+
if (typeof v !== "object" || v === null) return false;
|
|
3778
|
+
const o = v;
|
|
3779
|
+
if (typeof o["pr_number"] !== "number") return false;
|
|
3780
|
+
if (!Number.isFinite(o["pr_number"]) || o["pr_number"] < 1 || !Number.isInteger(o["pr_number"])) {
|
|
3781
|
+
return false;
|
|
3782
|
+
}
|
|
3783
|
+
return true;
|
|
3784
|
+
}
|
|
3785
|
+
async function handleMergePR(deps, rawInput, _signal) {
|
|
3786
|
+
if (!isToolInput14(rawInput)) {
|
|
3787
|
+
throw invalidParams(
|
|
3788
|
+
TOOL_NAME14,
|
|
3789
|
+
"invalid input. Shape: { pr_number: positive integer }."
|
|
3790
|
+
);
|
|
3791
|
+
}
|
|
3792
|
+
return buildCloudOrStubResponse({
|
|
3793
|
+
toolName: TOOL_NAME14,
|
|
3794
|
+
callableName: CALLABLE_NAME6,
|
|
3795
|
+
adminPath: ADMIN_PATH6,
|
|
3796
|
+
normalizedInput: { pr_number: rawInput.pr_number },
|
|
3797
|
+
cloudBody: { prNumber: rawInput.pr_number },
|
|
3798
|
+
gateType: PR_GATE_TYPE,
|
|
3799
|
+
stubReason: PR_STUB_REASON,
|
|
3800
|
+
deps
|
|
3801
|
+
});
|
|
3802
|
+
}
|
|
3803
|
+
|
|
3804
|
+
// src/tools/pr/reject-pr.ts
|
|
3805
|
+
var TOOL_NAME15 = "vo_reject_pr";
|
|
3806
|
+
var CALLABLE_NAME7 = "voRejectPR";
|
|
3807
|
+
var ADMIN_PATH7 = "/api/v1/admin/pr/reject";
|
|
3808
|
+
var inputSchema15 = {
|
|
3809
|
+
type: "object",
|
|
3810
|
+
properties: {
|
|
3811
|
+
pr_number: {
|
|
3812
|
+
type: "number",
|
|
3813
|
+
description: "Required. GitHub pull request number (positive integer)."
|
|
3814
|
+
}
|
|
3815
|
+
},
|
|
3816
|
+
required: ["pr_number"],
|
|
3817
|
+
additionalProperties: false
|
|
3818
|
+
};
|
|
3819
|
+
var description15 = "Closes a pull request without merging. No retry dispatched \u2014 use `vo_reject_and_retry` for close+retry. Wraps `voRejectPR` admin Cloud Function. V1 stub: returns `verdict: 'unimplemented'` \u2014 cloud-mode wiring pending.";
|
|
3820
|
+
function isToolInput15(v) {
|
|
3821
|
+
if (typeof v !== "object" || v === null) return false;
|
|
3822
|
+
const o = v;
|
|
3823
|
+
if (typeof o["pr_number"] !== "number") return false;
|
|
3824
|
+
if (!Number.isFinite(o["pr_number"]) || o["pr_number"] < 1 || !Number.isInteger(o["pr_number"])) {
|
|
3825
|
+
return false;
|
|
3826
|
+
}
|
|
3827
|
+
return true;
|
|
3828
|
+
}
|
|
3829
|
+
async function handleRejectPR(deps, rawInput, _signal) {
|
|
3830
|
+
if (!isToolInput15(rawInput)) {
|
|
3831
|
+
throw invalidParams(
|
|
3832
|
+
TOOL_NAME15,
|
|
3833
|
+
"invalid input. Shape: { pr_number: positive integer }."
|
|
3834
|
+
);
|
|
3835
|
+
}
|
|
3836
|
+
return buildCloudOrStubResponse({
|
|
3837
|
+
toolName: TOOL_NAME15,
|
|
3838
|
+
callableName: CALLABLE_NAME7,
|
|
3839
|
+
adminPath: ADMIN_PATH7,
|
|
3840
|
+
normalizedInput: { pr_number: rawInput.pr_number },
|
|
3841
|
+
cloudBody: { prNumber: rawInput.pr_number },
|
|
3842
|
+
gateType: PR_GATE_TYPE,
|
|
3843
|
+
stubReason: PR_STUB_REASON,
|
|
3844
|
+
deps
|
|
3845
|
+
});
|
|
3846
|
+
}
|
|
3847
|
+
|
|
3848
|
+
// src/tools/pr/approve-all-fixes.ts
|
|
3849
|
+
var TOOL_NAME16 = "vo_approve_all_fixes";
|
|
3850
|
+
var CALLABLE_NAME8 = "voApproveAllFixes";
|
|
3851
|
+
var ADMIN_PATH8 = "/api/v1/admin/pr/approve-all";
|
|
3852
|
+
var inputSchema16 = {
|
|
3853
|
+
type: "object",
|
|
3854
|
+
properties: {},
|
|
3855
|
+
additionalProperties: false
|
|
3856
|
+
};
|
|
3857
|
+
var description16 = "Iterates all open VO-source pull requests and merges (or arms auto-merge) on each. Returns counts of merged / accepted / total plus per-PR results. Wraps `voApproveAllFixes` admin Cloud Function. V1 stub: returns `verdict: 'unimplemented'` \u2014 cloud-mode wiring pending.";
|
|
3858
|
+
function isToolInput16(v) {
|
|
3859
|
+
return typeof v === "object" && v !== null;
|
|
3860
|
+
}
|
|
3861
|
+
async function handleApproveAllFixes(deps, rawInput, _signal) {
|
|
3862
|
+
if (!isToolInput16(rawInput)) {
|
|
3863
|
+
throw invalidParams(TOOL_NAME16, "invalid input. This tool takes no parameters.");
|
|
3864
|
+
}
|
|
3865
|
+
return buildCloudOrStubResponse({
|
|
3866
|
+
toolName: TOOL_NAME16,
|
|
3867
|
+
callableName: CALLABLE_NAME8,
|
|
3868
|
+
adminPath: ADMIN_PATH8,
|
|
3869
|
+
normalizedInput: {},
|
|
3870
|
+
gateType: PR_GATE_TYPE,
|
|
3871
|
+
stubReason: PR_STUB_REASON,
|
|
3872
|
+
deps
|
|
3873
|
+
});
|
|
3874
|
+
}
|
|
3875
|
+
|
|
3876
|
+
// src/tools/pr/reject-and-retry.ts
|
|
3877
|
+
var TOOL_NAME17 = "vo_reject_and_retry";
|
|
3878
|
+
var CALLABLE_NAME9 = "voRejectAndRetry";
|
|
3879
|
+
var ADMIN_PATH9 = "/api/v1/admin/pr/reject-retry";
|
|
3880
|
+
var inputSchema17 = {
|
|
3881
|
+
type: "object",
|
|
3882
|
+
properties: {
|
|
3883
|
+
pr_number: {
|
|
3884
|
+
type: "number",
|
|
3885
|
+
description: "Required. GitHub pull request number (positive integer)."
|
|
3886
|
+
}
|
|
3887
|
+
},
|
|
3888
|
+
required: ["pr_number"],
|
|
3889
|
+
additionalProperties: false
|
|
3890
|
+
};
|
|
3891
|
+
var description17 = "Closes a VO pull request and dispatches a self-heal pass to retry the same focus page. Cloud callable refuses non-VO PRs and respects the self-heal kill switch + per-PR retry block. Wraps `voRejectAndRetry` admin Cloud Function. V1 stub: returns `verdict: 'unimplemented'` \u2014 cloud-mode wiring pending.";
|
|
3892
|
+
function isToolInput17(v) {
|
|
3893
|
+
if (typeof v !== "object" || v === null) return false;
|
|
3894
|
+
const o = v;
|
|
3895
|
+
if (typeof o["pr_number"] !== "number") return false;
|
|
3896
|
+
if (!Number.isFinite(o["pr_number"]) || o["pr_number"] < 1 || !Number.isInteger(o["pr_number"])) {
|
|
3897
|
+
return false;
|
|
3898
|
+
}
|
|
3899
|
+
return true;
|
|
3900
|
+
}
|
|
3901
|
+
async function handleRejectAndRetry(deps, rawInput, _signal) {
|
|
3902
|
+
if (!isToolInput17(rawInput)) {
|
|
3903
|
+
throw invalidParams(
|
|
3904
|
+
TOOL_NAME17,
|
|
3905
|
+
"invalid input. Shape: { pr_number: positive integer }."
|
|
3906
|
+
);
|
|
3907
|
+
}
|
|
3908
|
+
return buildCloudOrStubResponse({
|
|
3909
|
+
toolName: TOOL_NAME17,
|
|
3910
|
+
callableName: CALLABLE_NAME9,
|
|
3911
|
+
adminPath: ADMIN_PATH9,
|
|
3912
|
+
normalizedInput: { pr_number: rawInput.pr_number },
|
|
3913
|
+
cloudBody: { prNumber: rawInput.pr_number },
|
|
3914
|
+
gateType: PR_GATE_TYPE,
|
|
3915
|
+
stubReason: PR_STUB_REASON,
|
|
3916
|
+
deps
|
|
3917
|
+
});
|
|
3918
|
+
}
|
|
3919
|
+
|
|
3920
|
+
// src/tools/pr/review-merge.ts
|
|
3921
|
+
var TOOL_NAME18 = "vo_review_merge";
|
|
3922
|
+
var LIST_PATH = "/api/v1/admin/pr/list";
|
|
3923
|
+
var ENGINE_GATE = "final-deep-verify";
|
|
3924
|
+
var EVENT_GATE = "merge-review";
|
|
3925
|
+
var UNAVAILABLE_REASON = "vo_review_merge needs cloud mode to fetch PR context \u2014 set VO_CONTROL_PLANE_URL + VO_CONTROL_PLANE_ADMIN_TOKEN in the MCP env. (It is read-only; it never merges.)";
|
|
3926
|
+
var inputSchema18 = {
|
|
3927
|
+
type: "object",
|
|
3928
|
+
properties: {
|
|
3929
|
+
pr_number: {
|
|
3930
|
+
type: "number",
|
|
3931
|
+
description: "GitHub pull request number (positive integer) to review for merge."
|
|
3932
|
+
},
|
|
3933
|
+
notes: {
|
|
3934
|
+
type: "string",
|
|
3935
|
+
description: "Optional reviewer notes / context to factor into the verdict."
|
|
3936
|
+
}
|
|
3937
|
+
},
|
|
3938
|
+
required: ["pr_number"],
|
|
3939
|
+
additionalProperties: false
|
|
3940
|
+
};
|
|
3941
|
+
var description18 = "READ-ONLY pre-merge review: given a PR number, fetches its CI/blocker status + source + linked bugs, runs a multi-model consensus 'should this merge?' check, and returns a recommendation (merge | hold | reject) with per-model reasoning. NEVER merges \u2014 acting on it is a separate vo_merge_pr call. A deterministic safety overlay refuses to recommend merge when the PR has an open blocker and defaults to hold when consensus is unavailable. Use BEFORE vo_merge_pr to put a verification gate in front of the Command Center. Increment 1 reviews on metadata; needs cloud mode for PR context + model keys for a real verdict.";
|
|
3942
|
+
function isToolInput18(v) {
|
|
3943
|
+
if (typeof v !== "object" || v === null) return false;
|
|
3944
|
+
const o = v;
|
|
3945
|
+
if (typeof o["pr_number"] !== "number") return false;
|
|
3946
|
+
if (o["notes"] !== void 0 && typeof o["notes"] !== "string") return false;
|
|
3947
|
+
return true;
|
|
3948
|
+
}
|
|
3949
|
+
function str(v) {
|
|
3950
|
+
return typeof v === "string" && v.length > 0 ? v : null;
|
|
3951
|
+
}
|
|
3952
|
+
function readPr(raw) {
|
|
3953
|
+
const bugsRaw = raw["bugIds"] ?? raw["bug_ids"];
|
|
3954
|
+
const bug_ids = Array.isArray(bugsRaw) ? bugsRaw.filter((b) => typeof b === "string") : [];
|
|
3955
|
+
return {
|
|
3956
|
+
number: Number(raw["number"]),
|
|
3957
|
+
title: str(raw["title"]) ?? "(untitled)",
|
|
3958
|
+
source: str(raw["source"]) ?? str(raw["prSource"]),
|
|
3959
|
+
blocker: str(raw["blockerKind"]) ?? str(raw["blocker_kind"]) ?? str(raw["blockerLabel"]),
|
|
3960
|
+
bug_ids
|
|
3961
|
+
};
|
|
3962
|
+
}
|
|
3963
|
+
function buildPrompt4(pr, notes) {
|
|
3964
|
+
const lines = [
|
|
3965
|
+
"You are a release gatekeeper deciding whether a pull request is safe to MERGE.",
|
|
3966
|
+
"Recommend exactly one of: merge / hold / reject. Be conservative \u2014 this is a high-stakes irreversible action.",
|
|
3967
|
+
"Rules: HOLD if CI is failing/blocked, there is a merge conflict, or the change is a draft. REJECT if the PR is not a legitimate VO-source change or has no clear purpose. MERGE only if it looks complete, scoped, and unblocked.",
|
|
3968
|
+
"",
|
|
3969
|
+
`PR #${pr.number}: ${pr.title}`,
|
|
3970
|
+
`Source: ${pr.source ?? "unknown"}`,
|
|
3971
|
+
`Linked bug ids: ${pr.bug_ids.length > 0 ? pr.bug_ids.join(", ") : "(none)"}`,
|
|
3972
|
+
`Open blocker: ${pr.blocker ?? "none reported"}`
|
|
3973
|
+
];
|
|
3974
|
+
if (notes !== void 0 && notes.length > 0) lines.push("", `Reviewer notes: ${notes}`);
|
|
3975
|
+
lines.push("", "Return your verdict (pass = recommend merge, fail = reject, uncertain = hold) with concise reasoning.");
|
|
3976
|
+
return lines.join("\n");
|
|
3977
|
+
}
|
|
3978
|
+
async function handleReviewMerge(deps, rawInput, signal) {
|
|
3979
|
+
if (!isToolInput18(rawInput) || !Number.isInteger(rawInput.pr_number) || rawInput.pr_number < 1) {
|
|
3980
|
+
throw invalidParams(TOOL_NAME18, "invalid input. Required: { pr_number: positive integer }. Optional: notes.");
|
|
3981
|
+
}
|
|
3982
|
+
const prNumber = rawInput.pr_number;
|
|
3983
|
+
const normalizedInput = { pr_number: prNumber };
|
|
3984
|
+
if (rawInput.notes !== void 0) normalizedInput.notes = rawInput.notes;
|
|
3985
|
+
const inputJson = JSON.stringify(normalizedInput);
|
|
3986
|
+
const key = deps.cache.keyFor(TOOL_NAME18, normalizedInput);
|
|
3987
|
+
const baseEvent = buildBaseEvent({
|
|
3988
|
+
tool: TOOL_NAME18,
|
|
3989
|
+
gateType: EVENT_GATE,
|
|
3990
|
+
inputHash: key,
|
|
3991
|
+
inputExcerpt: inputJson.slice(0, 300),
|
|
3992
|
+
inputSizeBytes: bytesOf(inputJson),
|
|
3993
|
+
session: deps.session,
|
|
3994
|
+
now: deps.now()
|
|
3995
|
+
});
|
|
3996
|
+
const emit = (payload2, eventExtra) => {
|
|
3997
|
+
deps.events.append(eventExtra ? { ...baseEvent, ...eventExtra } : baseEvent);
|
|
3998
|
+
const envelope = {
|
|
3999
|
+
tool: TOOL_NAME18,
|
|
4000
|
+
schema_version: 1,
|
|
4001
|
+
cache: { hit: false, key },
|
|
4002
|
+
payload: payload2
|
|
4003
|
+
};
|
|
4004
|
+
return jsonContent(envelope);
|
|
4005
|
+
};
|
|
4006
|
+
const emptyPayload = (recommendation2, reason2, pr2) => ({
|
|
4007
|
+
recommendation: recommendation2,
|
|
4008
|
+
verdict: null,
|
|
4009
|
+
confidence: null,
|
|
4010
|
+
reason: reason2,
|
|
4011
|
+
basis: "metadata",
|
|
4012
|
+
pr: pr2,
|
|
4013
|
+
per_model_verdicts: [],
|
|
4014
|
+
synthesized_verdict: null,
|
|
4015
|
+
engine_version: null,
|
|
4016
|
+
degraded: false,
|
|
4017
|
+
merged: false
|
|
4018
|
+
});
|
|
4019
|
+
if (!deps.adminCallables) {
|
|
4020
|
+
return emit(emptyPayload("unavailable", UNAVAILABLE_REASON, null));
|
|
4021
|
+
}
|
|
4022
|
+
let pr = null;
|
|
4023
|
+
try {
|
|
4024
|
+
const list = await deps.adminCallables.invoke(LIST_PATH, {});
|
|
4025
|
+
const entries = Array.isArray(list) ? list : [];
|
|
4026
|
+
const found = entries.find((p) => Number(p["number"]) === prNumber);
|
|
4027
|
+
if (found) pr = readPr(found);
|
|
4028
|
+
} catch (err) {
|
|
4029
|
+
const status = err instanceof AdminCallableError ? ` (HTTP ${err.status})` : "";
|
|
4030
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4031
|
+
return emit(emptyPayload("hold", `could not fetch PR context${status}: ${message.slice(0, 200)}`, null));
|
|
4032
|
+
}
|
|
4033
|
+
if (pr === null) {
|
|
4034
|
+
return emit(
|
|
4035
|
+
emptyPayload("hold", `PR #${prNumber} is not among open VO PRs (already merged/closed, or not a VO-source PR).`, null)
|
|
4036
|
+
);
|
|
4037
|
+
}
|
|
4038
|
+
const hasBlocker = pr.blocker !== null && pr.blocker !== "none";
|
|
4039
|
+
let result;
|
|
4040
|
+
let threw = false;
|
|
4041
|
+
try {
|
|
4042
|
+
result = await deps.consensus.run({
|
|
4043
|
+
gate_type: ENGINE_GATE,
|
|
4044
|
+
prompt: buildPrompt4(pr, rawInput.notes),
|
|
4045
|
+
caller_context: { pr_number: prNumber, pr_metadata: pr, basis: "metadata" },
|
|
4046
|
+
...signal !== void 0 ? { signal } : {}
|
|
4047
|
+
});
|
|
4048
|
+
} catch (err) {
|
|
4049
|
+
threw = true;
|
|
4050
|
+
result = { ok: false, reason: `engine-threw: ${err instanceof Error ? err.message : String(err)}` };
|
|
4051
|
+
}
|
|
4052
|
+
if (!result.ok && result.reason === "cancelled") {
|
|
4053
|
+
deps.events.append({
|
|
4054
|
+
...baseEvent,
|
|
4055
|
+
downstream_outcome: { status: "cancelled", notes: "mcp notifications/cancelled" }
|
|
4056
|
+
});
|
|
4057
|
+
const e = new Error("Request cancelled by client (notifications/cancelled)");
|
|
4058
|
+
e.name = "AbortError";
|
|
4059
|
+
throw e;
|
|
4060
|
+
}
|
|
4061
|
+
if (!result.ok) {
|
|
4062
|
+
const reason2 = `consensus unavailable (${result.reason}) \u2014 holding for manual review.` + (hasBlocker ? ` PR also has an open blocker: ${pr.blocker}.` : "");
|
|
4063
|
+
return emit(
|
|
4064
|
+
{ ...emptyPayload("hold", reason2, pr), degraded: threw },
|
|
4065
|
+
threw ? {} : void 0
|
|
4066
|
+
);
|
|
4067
|
+
}
|
|
4068
|
+
const verdict = result.synthesized_verdict.verdict;
|
|
4069
|
+
let recommendation = verdict === "pass" ? "merge" : verdict === "fail" ? "reject" : "hold";
|
|
4070
|
+
let reason = result.synthesized_verdict.dissent_summary ?? result.synthesized_verdict.reasoning_excerpt ?? `panel agreed (${result.per_model_verdicts.length} models, engine=${result.engine_version})`;
|
|
4071
|
+
if (hasBlocker && recommendation === "merge") {
|
|
4072
|
+
recommendation = "hold";
|
|
4073
|
+
reason = `Consensus leaned merge, but PR has an open blocker (${pr.blocker}) \u2014 holding (safety overlay). ${reason}`;
|
|
4074
|
+
}
|
|
4075
|
+
const perModel = toEventPerModelVerdicts(result.per_model_verdicts);
|
|
4076
|
+
const synth = toEventSynthesizedVerdict(result.synthesized_verdict);
|
|
4077
|
+
const payload = {
|
|
4078
|
+
recommendation,
|
|
4079
|
+
verdict,
|
|
4080
|
+
confidence: result.synthesized_verdict.confidence,
|
|
4081
|
+
reason,
|
|
4082
|
+
basis: "metadata",
|
|
4083
|
+
pr,
|
|
4084
|
+
per_model_verdicts: perModel,
|
|
4085
|
+
synthesized_verdict: synth,
|
|
4086
|
+
engine_version: result.engine_version,
|
|
4087
|
+
degraded: result.degraded,
|
|
4088
|
+
merged: false
|
|
4089
|
+
};
|
|
4090
|
+
return emit(payload, {
|
|
4091
|
+
consensus_confidence: result.synthesized_verdict.confidence,
|
|
4092
|
+
duration_ms: result.duration_ms,
|
|
4093
|
+
consensus_engine_version: result.engine_version,
|
|
4094
|
+
per_model_verdicts: perModel,
|
|
4095
|
+
synthesized_verdict: synth
|
|
4096
|
+
});
|
|
4097
|
+
}
|
|
4098
|
+
|
|
4099
|
+
// src/tools/session/directive.ts
|
|
4100
|
+
var SESSION_DIRECTIVE_THRESHOLDS = {
|
|
4101
|
+
prepare_handoff_pct: 70,
|
|
4102
|
+
execute_handoff_now_pct: 85
|
|
4103
|
+
};
|
|
4104
|
+
function computeDirective(context_used_pct) {
|
|
4105
|
+
const clamped = Math.max(0, Math.min(100, context_used_pct));
|
|
4106
|
+
if (clamped >= SESSION_DIRECTIVE_THRESHOLDS.execute_handoff_now_pct) {
|
|
4107
|
+
return "execute_handoff_now";
|
|
4108
|
+
}
|
|
4109
|
+
if (clamped >= SESSION_DIRECTIVE_THRESHOLDS.prepare_handoff_pct) {
|
|
4110
|
+
return "prepare_handoff";
|
|
4111
|
+
}
|
|
4112
|
+
return "continue";
|
|
4113
|
+
}
|
|
4114
|
+
function directiveMessage(directive) {
|
|
4115
|
+
switch (directive) {
|
|
4116
|
+
case "continue":
|
|
4117
|
+
return "Context usage healthy. Continue work.";
|
|
4118
|
+
case "prepare_handoff":
|
|
4119
|
+
return `Context usage at or above ${SESSION_DIRECTIVE_THRESHOLDS.prepare_handoff_pct}%. Begin wrapping up the current sub-task and prepare a handoff doc covering current state, next steps, blockers, and verification needed.`;
|
|
4120
|
+
case "execute_handoff_now":
|
|
4121
|
+
return `Context usage at or above ${SESSION_DIRECTIVE_THRESHOLDS.execute_handoff_now_pct}%. Stop work, write the handoff doc using the template at ~/.vo/templates/handoff.md to ~/.vo/handoffs/<session_id>-<ts>.md, then call endSession with terminal_status='handed_off' and the handoff path.`;
|
|
4122
|
+
}
|
|
4123
|
+
}
|
|
4124
|
+
function suggestedHandoffPath(session_id, isoTimestamp) {
|
|
4125
|
+
const safeTs = isoTimestamp.replace(/[:.]/g, "-");
|
|
4126
|
+
return `~/.vo/handoffs/${session_id}-${safeTs}.md`;
|
|
4127
|
+
}
|
|
4128
|
+
|
|
4129
|
+
// src/tools/session/report-session-state.ts
|
|
4130
|
+
var TOOL_NAME19 = "vo_report_session_state";
|
|
4131
|
+
var VALID_AGENT_TYPES = ["claude-code", "codex", "cursor", "continue"];
|
|
4132
|
+
var MAX_GOAL_CHARS = 500;
|
|
4133
|
+
var MAX_RECENT_FILES = 20;
|
|
4134
|
+
var MAX_RECENT_TOOLS = 50;
|
|
4135
|
+
var inputSchema19 = {
|
|
4136
|
+
type: "object",
|
|
4137
|
+
properties: {
|
|
4138
|
+
operator_id: {
|
|
4139
|
+
type: "string",
|
|
4140
|
+
minLength: 1,
|
|
4141
|
+
description: "Stable identifier (UUID expected) of the operator owning this session."
|
|
4142
|
+
},
|
|
4143
|
+
session_id: {
|
|
4144
|
+
type: "string",
|
|
4145
|
+
minLength: 1,
|
|
4146
|
+
description: "Stable identifier (UUID expected) of the session reporting state."
|
|
4147
|
+
},
|
|
4148
|
+
agent_type: {
|
|
4149
|
+
type: "string",
|
|
4150
|
+
enum: [...VALID_AGENT_TYPES],
|
|
4151
|
+
description: "Which host agent runtime is reporting (claude-code | codex | cursor | continue)."
|
|
4152
|
+
},
|
|
4153
|
+
context_used_pct: {
|
|
4154
|
+
type: "number",
|
|
4155
|
+
minimum: 0,
|
|
4156
|
+
maximum: 100,
|
|
4157
|
+
description: "Current context-window utilization, 0-100. Used to compute the directive."
|
|
4158
|
+
},
|
|
4159
|
+
current_goal: {
|
|
4160
|
+
type: "string",
|
|
4161
|
+
maxLength: MAX_GOAL_CHARS,
|
|
4162
|
+
description: "Optional one-line goal statement. Truncated server-side at 500 chars."
|
|
4163
|
+
},
|
|
4164
|
+
recent_files_touched: {
|
|
4165
|
+
type: "array",
|
|
4166
|
+
items: { type: "string" },
|
|
4167
|
+
maxItems: MAX_RECENT_FILES,
|
|
4168
|
+
description: `Optional list of recent file paths touched in the session. Capped at ${MAX_RECENT_FILES} items.`
|
|
4169
|
+
},
|
|
4170
|
+
recent_tool_uses: {
|
|
4171
|
+
type: "array",
|
|
4172
|
+
items: { type: "string" },
|
|
4173
|
+
maxItems: MAX_RECENT_TOOLS,
|
|
4174
|
+
description: `Optional list of recent tool names invoked in the session. Capped at ${MAX_RECENT_TOOLS} items.`
|
|
4175
|
+
}
|
|
4176
|
+
},
|
|
4177
|
+
required: ["operator_id", "session_id", "agent_type", "context_used_pct"],
|
|
4178
|
+
additionalProperties: false
|
|
4179
|
+
};
|
|
4180
|
+
var description19 = "Reports per-session context-window utilization to VO and returns a directive: 'continue' (under 70%), 'prepare_handoff' (70-84%), or 'execute_handoff_now' (\u226585%). Implements V1 launch gate #9 (fleet context lifecycle management) per the official VO roadmap. V1 backend is stub-local \u2014 computes the directive purely from `context_used_pct` against the documented thresholds without a network call. Phase 3 wires this to the deployed vo-control-plane HTTP API; the response shape stays stable across the cutover (`backend_mode` field in the payload tells the caller which mode produced the verdict).";
|
|
4181
|
+
function isStringArray(v, maxItems) {
|
|
4182
|
+
if (!Array.isArray(v)) return false;
|
|
4183
|
+
if (v.length > maxItems) return false;
|
|
4184
|
+
return v.every((item) => typeof item === "string");
|
|
4185
|
+
}
|
|
4186
|
+
function isToolInput19(v) {
|
|
4187
|
+
if (typeof v !== "object" || v === null) return false;
|
|
4188
|
+
const o = v;
|
|
4189
|
+
if (typeof o["operator_id"] !== "string" || o["operator_id"].length === 0) return false;
|
|
4190
|
+
if (typeof o["session_id"] !== "string" || o["session_id"].length === 0) return false;
|
|
4191
|
+
if (typeof o["agent_type"] !== "string") return false;
|
|
4192
|
+
if (!VALID_AGENT_TYPES.includes(o["agent_type"])) return false;
|
|
4193
|
+
if (typeof o["context_used_pct"] !== "number") return false;
|
|
4194
|
+
if (!Number.isFinite(o["context_used_pct"])) return false;
|
|
4195
|
+
if (o["context_used_pct"] < 0 || o["context_used_pct"] > 100) return false;
|
|
4196
|
+
if (o["current_goal"] !== void 0 && typeof o["current_goal"] !== "string") return false;
|
|
4197
|
+
if (o["recent_files_touched"] !== void 0 && !isStringArray(o["recent_files_touched"], MAX_RECENT_FILES)) {
|
|
4198
|
+
return false;
|
|
4199
|
+
}
|
|
4200
|
+
if (o["recent_tool_uses"] !== void 0 && !isStringArray(o["recent_tool_uses"], MAX_RECENT_TOOLS)) {
|
|
4201
|
+
return false;
|
|
4202
|
+
}
|
|
4203
|
+
return true;
|
|
4204
|
+
}
|
|
4205
|
+
async function handleReportSessionState(deps, rawInput, _signal) {
|
|
4206
|
+
if (!isToolInput19(rawInput)) {
|
|
4207
|
+
throw invalidParams(
|
|
4208
|
+
TOOL_NAME19,
|
|
4209
|
+
`invalid input. Required fields: operator_id (non-empty string), session_id (non-empty string), agent_type (one of: ${VALID_AGENT_TYPES.join(" | ")}), context_used_pct (number 0-100). Optional: current_goal (string \u2264${MAX_GOAL_CHARS} chars), recent_files_touched (string[] \u2264${MAX_RECENT_FILES}), recent_tool_uses (string[] \u2264${MAX_RECENT_TOOLS}).`
|
|
4210
|
+
);
|
|
4211
|
+
}
|
|
4212
|
+
const directive = computeDirective(rawInput.context_used_pct);
|
|
4213
|
+
const ts = deps.now().toISOString();
|
|
4214
|
+
const payload = {
|
|
4215
|
+
directive,
|
|
4216
|
+
message: directiveMessage(directive),
|
|
4217
|
+
session_id: rawInput.session_id,
|
|
4218
|
+
operator_id: rawInput.operator_id,
|
|
4219
|
+
agent_type: rawInput.agent_type,
|
|
4220
|
+
context_used_pct: rawInput.context_used_pct,
|
|
4221
|
+
backend_mode: "stub-local",
|
|
4222
|
+
ts,
|
|
4223
|
+
schema_version: 1,
|
|
4224
|
+
...directive === "execute_handoff_now" ? { handoff_path: suggestedHandoffPath(rawInput.session_id, ts) } : {}
|
|
4225
|
+
};
|
|
4226
|
+
return jsonContent(payload);
|
|
4227
|
+
}
|
|
4228
|
+
|
|
4229
|
+
// src/tools/concierge/common-concierge.ts
|
|
4230
|
+
var CONCIERGE_STUB_REASON = "cloud mode not active in this MCP runtime \u2014 set VO_CONTROL_PLANE_URL + VO_CONTROL_PLANE_ADMIN_TOKEN to enable. The server-side /api/v1/admin/concierge/dispatch endpoint IS built + deployed (vo-control-plane #5724/#5734); in cloud mode this tool returns the routed pack README + file index.";
|
|
4231
|
+
var CONCIERGE_GATE_TYPE = "concierge-dispatch";
|
|
4232
|
+
var KNOWN_CONCIERGE_PACKS = [
|
|
4233
|
+
"gcp",
|
|
4234
|
+
"firebase",
|
|
4235
|
+
"aws",
|
|
4236
|
+
"cloudflare",
|
|
4237
|
+
"vercel",
|
|
4238
|
+
"netlify",
|
|
4239
|
+
"tax",
|
|
4240
|
+
"hybrid"
|
|
4241
|
+
];
|
|
4242
|
+
function isKnownConciergePack(value) {
|
|
4243
|
+
return typeof value === "string" && KNOWN_CONCIERGE_PACKS.includes(value);
|
|
4244
|
+
}
|
|
4245
|
+
|
|
4246
|
+
// src/tools/concierge/dispatch.ts
|
|
4247
|
+
var TOOL_NAME20 = "vo_concierge_dispatch";
|
|
4248
|
+
var CALLABLE_NAME10 = "voConciergeDispatch";
|
|
4249
|
+
var ADMIN_PATH10 = "/api/v1/admin/concierge/dispatch";
|
|
4250
|
+
var inputSchema20 = {
|
|
4251
|
+
type: "object",
|
|
4252
|
+
properties: {
|
|
4253
|
+
pack: {
|
|
4254
|
+
type: "string",
|
|
4255
|
+
description: `Explicit knowledge pack name. One of: ${KNOWN_CONCIERGE_PACKS.join(", ")}. When provided, overrides tenant-based routing. Omit to let server route by tenant_id.`,
|
|
4256
|
+
enum: [...KNOWN_CONCIERGE_PACKS]
|
|
4257
|
+
},
|
|
4258
|
+
tenant_id: {
|
|
4259
|
+
type: "string",
|
|
4260
|
+
description: "Tenant identifier. When provided (and pack is omitted), the server looks up Tenant.cloud_provider and dispatches to the matching pack. Ignored when pack is also provided."
|
|
4261
|
+
}
|
|
4262
|
+
},
|
|
4263
|
+
additionalProperties: false
|
|
4264
|
+
};
|
|
4265
|
+
var description20 = "Dispatches a provider-scoped knowledge pack (gcp | firebase | aws | cloudflare | vercel | netlify | tax | hybrid). Cross-vendor MCP equivalent of the /vo-concierge Claude-Code slash command. Route explicitly via `pack`, or via tenant.cloud_provider by passing `tenant_id`. Returns the pack's README (`readme_markdown`) + file index. In cloud mode, dispatches via vo-control-plane and returns `verdict: 'pass'` with the pack/directory data; without cloud config, returns `verdict: 'unimplemented'`.";
|
|
4266
|
+
function isToolInput20(v) {
|
|
4267
|
+
if (typeof v !== "object" || v === null) return false;
|
|
4268
|
+
const obj = v;
|
|
4269
|
+
if (obj.pack !== void 0 && typeof obj.pack !== "string") return false;
|
|
4270
|
+
if (obj.tenant_id !== void 0 && typeof obj.tenant_id !== "string") return false;
|
|
4271
|
+
return true;
|
|
4272
|
+
}
|
|
4273
|
+
async function handleConciergeDispatch(deps, rawInput, _signal) {
|
|
4274
|
+
if (!isToolInput20(rawInput)) {
|
|
4275
|
+
throw invalidParams(
|
|
4276
|
+
TOOL_NAME20,
|
|
4277
|
+
"invalid input. Expected { pack?: string, tenant_id?: string }."
|
|
4278
|
+
);
|
|
4279
|
+
}
|
|
4280
|
+
if (rawInput.pack !== void 0 && rawInput.pack !== "" && !isKnownConciergePack(rawInput.pack)) {
|
|
4281
|
+
throw invalidParams(
|
|
4282
|
+
TOOL_NAME20,
|
|
4283
|
+
`unknown pack: ${JSON.stringify(rawInput.pack)}. Known packs: ${KNOWN_CONCIERGE_PACKS.join(", ")}.`
|
|
4284
|
+
);
|
|
4285
|
+
}
|
|
4286
|
+
const normalizedInput = {};
|
|
4287
|
+
if (rawInput.pack) normalizedInput.pack = rawInput.pack;
|
|
4288
|
+
if (rawInput.tenant_id) normalizedInput.tenant_id = rawInput.tenant_id;
|
|
4289
|
+
const cloudBody = {};
|
|
4290
|
+
if (rawInput.pack) cloudBody.pack = rawInput.pack;
|
|
4291
|
+
if (rawInput.tenant_id) cloudBody.tenantId = rawInput.tenant_id;
|
|
4292
|
+
return buildCloudOrStubResponse({
|
|
4293
|
+
toolName: TOOL_NAME20,
|
|
4294
|
+
callableName: CALLABLE_NAME10,
|
|
4295
|
+
adminPath: ADMIN_PATH10,
|
|
4296
|
+
normalizedInput,
|
|
4297
|
+
cloudBody,
|
|
4298
|
+
// Concierge dispatch returns `{ok, mode, routed_from, pack|packs}`,
|
|
4299
|
+
// NOT the callable-proxy `{ok, callable, result}` shape — take the
|
|
4300
|
+
// raw envelope as response_data instead of mandating `.result`.
|
|
4301
|
+
rawEnvelope: true,
|
|
4302
|
+
gateType: CONCIERGE_GATE_TYPE,
|
|
4303
|
+
stubReason: CONCIERGE_STUB_REASON,
|
|
4304
|
+
// Read-only pack fetch — stays live under VO_ADMIN_CALLABLES_READONLY.
|
|
4305
|
+
readOnly: true,
|
|
4306
|
+
deps
|
|
4307
|
+
});
|
|
4308
|
+
}
|
|
4309
|
+
|
|
4310
|
+
// src/server.ts
|
|
4311
|
+
function buildToolRegistry() {
|
|
4312
|
+
return {
|
|
4313
|
+
[TOOL_NAME]: {
|
|
4314
|
+
definition: {
|
|
4315
|
+
name: TOOL_NAME,
|
|
4316
|
+
description,
|
|
4317
|
+
inputSchema
|
|
4318
|
+
},
|
|
4319
|
+
handler: handleCheckAssertionStrength
|
|
4320
|
+
},
|
|
4321
|
+
[TOOL_NAME2]: {
|
|
4322
|
+
definition: {
|
|
4323
|
+
name: TOOL_NAME2,
|
|
4324
|
+
description: description2,
|
|
4325
|
+
inputSchema: inputSchema2
|
|
4326
|
+
},
|
|
4327
|
+
handler: handleCheckHollowTest
|
|
4328
|
+
},
|
|
4329
|
+
[TOOL_NAME3]: {
|
|
4330
|
+
definition: {
|
|
4331
|
+
name: TOOL_NAME3,
|
|
4332
|
+
description: description3,
|
|
4333
|
+
inputSchema: inputSchema3
|
|
4334
|
+
},
|
|
4335
|
+
handler: handleVerifyAnswer
|
|
4336
|
+
},
|
|
4337
|
+
[TOOL_NAME4]: {
|
|
4338
|
+
definition: {
|
|
4339
|
+
name: TOOL_NAME4,
|
|
4340
|
+
description: description4,
|
|
4341
|
+
inputSchema: inputSchema4
|
|
4342
|
+
},
|
|
4343
|
+
handler: handleConsensusJudgment
|
|
4344
|
+
},
|
|
4345
|
+
[TOOL_NAME5]: {
|
|
4346
|
+
definition: {
|
|
4347
|
+
name: TOOL_NAME5,
|
|
4348
|
+
description: description5,
|
|
4349
|
+
inputSchema: inputSchema5
|
|
4350
|
+
},
|
|
4351
|
+
handler: handleArchitectureReview
|
|
4352
|
+
},
|
|
4353
|
+
[TOOL_NAME7]: {
|
|
4354
|
+
definition: {
|
|
4355
|
+
name: TOOL_NAME7,
|
|
4356
|
+
description: description7,
|
|
4357
|
+
inputSchema: inputSchema7
|
|
4358
|
+
},
|
|
4359
|
+
handler: handleDecomposeDispatch
|
|
4360
|
+
},
|
|
4361
|
+
[TOOL_NAME6]: {
|
|
4362
|
+
definition: {
|
|
4363
|
+
name: TOOL_NAME6,
|
|
4364
|
+
description: description6,
|
|
4365
|
+
inputSchema: inputSchema6
|
|
4366
|
+
},
|
|
4367
|
+
handler: handleCheckRatchets
|
|
4368
|
+
},
|
|
4369
|
+
[TOOL_NAME8]: {
|
|
4370
|
+
definition: {
|
|
4371
|
+
name: TOOL_NAME8,
|
|
4372
|
+
description: description8,
|
|
4373
|
+
inputSchema: inputSchema8
|
|
4374
|
+
},
|
|
4375
|
+
handler: handleTriggerHeal
|
|
4376
|
+
},
|
|
4377
|
+
[TOOL_NAME9]: {
|
|
4378
|
+
definition: {
|
|
4379
|
+
name: TOOL_NAME9,
|
|
4380
|
+
description: description9,
|
|
4381
|
+
inputSchema: inputSchema9
|
|
4382
|
+
},
|
|
4383
|
+
handler: handleFixRetry
|
|
4384
|
+
},
|
|
4385
|
+
[TOOL_NAME10]: {
|
|
4386
|
+
definition: {
|
|
4387
|
+
name: TOOL_NAME10,
|
|
4388
|
+
description: description10,
|
|
4389
|
+
inputSchema: inputSchema10
|
|
4390
|
+
},
|
|
4391
|
+
handler: handleFixClear
|
|
4392
|
+
},
|
|
4393
|
+
[TOOL_NAME11]: {
|
|
4394
|
+
definition: {
|
|
4395
|
+
name: TOOL_NAME11,
|
|
4396
|
+
description: description11,
|
|
4397
|
+
inputSchema: inputSchema11
|
|
4398
|
+
},
|
|
4399
|
+
handler: handleStopWorkflow
|
|
4400
|
+
},
|
|
4401
|
+
[TOOL_NAME12]: {
|
|
4402
|
+
definition: {
|
|
4403
|
+
name: TOOL_NAME12,
|
|
4404
|
+
description: description12,
|
|
4405
|
+
inputSchema: inputSchema12
|
|
4406
|
+
},
|
|
4407
|
+
handler: handleGetWorkflowRuns
|
|
4408
|
+
},
|
|
4409
|
+
[TOOL_NAME13]: {
|
|
4410
|
+
definition: {
|
|
4411
|
+
name: TOOL_NAME13,
|
|
4412
|
+
description: description13,
|
|
4413
|
+
inputSchema: inputSchema13
|
|
4414
|
+
},
|
|
4415
|
+
handler: handleListPendingPRs
|
|
4416
|
+
},
|
|
4417
|
+
[TOOL_NAME14]: {
|
|
4418
|
+
definition: {
|
|
4419
|
+
name: TOOL_NAME14,
|
|
4420
|
+
description: description14,
|
|
4421
|
+
inputSchema: inputSchema14
|
|
4422
|
+
},
|
|
4423
|
+
handler: handleMergePR
|
|
4424
|
+
},
|
|
4425
|
+
[TOOL_NAME15]: {
|
|
4426
|
+
definition: {
|
|
4427
|
+
name: TOOL_NAME15,
|
|
4428
|
+
description: description15,
|
|
4429
|
+
inputSchema: inputSchema15
|
|
4430
|
+
},
|
|
4431
|
+
handler: handleRejectPR
|
|
4432
|
+
},
|
|
4433
|
+
[TOOL_NAME16]: {
|
|
4434
|
+
definition: {
|
|
4435
|
+
name: TOOL_NAME16,
|
|
4436
|
+
description: description16,
|
|
4437
|
+
inputSchema: inputSchema16
|
|
4438
|
+
},
|
|
4439
|
+
handler: handleApproveAllFixes
|
|
4440
|
+
},
|
|
4441
|
+
[TOOL_NAME17]: {
|
|
4442
|
+
definition: {
|
|
4443
|
+
name: TOOL_NAME17,
|
|
4444
|
+
description: description17,
|
|
4445
|
+
inputSchema: inputSchema17
|
|
4446
|
+
},
|
|
4447
|
+
handler: handleRejectAndRetry
|
|
4448
|
+
},
|
|
4449
|
+
[TOOL_NAME18]: {
|
|
4450
|
+
definition: {
|
|
4451
|
+
name: TOOL_NAME18,
|
|
4452
|
+
description: description18,
|
|
4453
|
+
inputSchema: inputSchema18
|
|
4454
|
+
},
|
|
4455
|
+
handler: handleReviewMerge
|
|
4456
|
+
},
|
|
4457
|
+
[TOOL_NAME19]: {
|
|
4458
|
+
definition: {
|
|
4459
|
+
name: TOOL_NAME19,
|
|
4460
|
+
description: description19,
|
|
4461
|
+
inputSchema: inputSchema19
|
|
4462
|
+
},
|
|
4463
|
+
handler: handleReportSessionState
|
|
4464
|
+
},
|
|
4465
|
+
[TOOL_NAME20]: {
|
|
4466
|
+
definition: {
|
|
4467
|
+
name: TOOL_NAME20,
|
|
4468
|
+
description: description20,
|
|
4469
|
+
inputSchema: inputSchema20
|
|
4470
|
+
},
|
|
4471
|
+
handler: handleConciergeDispatch
|
|
4472
|
+
}
|
|
4473
|
+
};
|
|
4474
|
+
}
|
|
4475
|
+
function createServer(options) {
|
|
4476
|
+
const sessionId = options.sessionId ?? randomUUID2();
|
|
4477
|
+
const mode = createLocalMode();
|
|
4478
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
4479
|
+
const server = new Server(
|
|
4480
|
+
{
|
|
4481
|
+
name: "vo-mcp",
|
|
4482
|
+
version: "0.1.0"
|
|
4483
|
+
},
|
|
4484
|
+
{
|
|
4485
|
+
capabilities: {
|
|
4486
|
+
tools: {}
|
|
4487
|
+
}
|
|
4488
|
+
}
|
|
4489
|
+
);
|
|
4490
|
+
const registry = buildToolRegistry();
|
|
4491
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
4492
|
+
return {
|
|
4493
|
+
tools: Object.values(registry).map((t) => t.definition)
|
|
4494
|
+
};
|
|
4495
|
+
});
|
|
4496
|
+
server.setRequestHandler(CallToolRequestSchema, async (req, extra) => {
|
|
4497
|
+
const toolName = req.params.name;
|
|
4498
|
+
const entry = registry[toolName];
|
|
4499
|
+
if (!entry) {
|
|
4500
|
+
throw methodNotFound(toolName, Object.keys(registry));
|
|
4501
|
+
}
|
|
4502
|
+
const clientInfo = server.getClientVersion();
|
|
4503
|
+
const session = {
|
|
4504
|
+
sessionId,
|
|
4505
|
+
clientId: clientInfo?.name ?? "unknown",
|
|
4506
|
+
mode: mode.mode,
|
|
4507
|
+
tenantId: mode.tenantId,
|
|
4508
|
+
operatorId: mode.operatorId
|
|
4509
|
+
};
|
|
4510
|
+
const deps = {
|
|
4511
|
+
session,
|
|
4512
|
+
cache: options.cache,
|
|
4513
|
+
events: options.events,
|
|
4514
|
+
ratchets: options.ratchets,
|
|
4515
|
+
consensus: options.consensus,
|
|
4516
|
+
now,
|
|
4517
|
+
adminCallables: options.adminCallables ?? null
|
|
4518
|
+
};
|
|
4519
|
+
return entry.handler(deps, req.params.arguments ?? {}, extra?.signal);
|
|
4520
|
+
});
|
|
4521
|
+
return server;
|
|
4522
|
+
}
|
|
4523
|
+
function listToolNames() {
|
|
4524
|
+
return Object.keys(buildToolRegistry());
|
|
4525
|
+
}
|
|
4526
|
+
|
|
4527
|
+
// src/cache/sqlite-cache.ts
|
|
4528
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
4529
|
+
import { chmodSync as chmodSync3, mkdirSync as mkdirSync3 } from "node:fs";
|
|
4530
|
+
import { dirname as dirname5 } from "node:path";
|
|
4531
|
+
import { DatabaseSync } from "node:sqlite";
|
|
4532
|
+
|
|
4533
|
+
// src/cache/canonicalize.ts
|
|
4534
|
+
var DEFAULT_VOLATILE_KEYS = /* @__PURE__ */ new Set([
|
|
4535
|
+
"ts",
|
|
4536
|
+
"timestamp",
|
|
4537
|
+
"session_id",
|
|
4538
|
+
"sessionId",
|
|
4539
|
+
"request_id",
|
|
4540
|
+
"requestId",
|
|
4541
|
+
"nonce"
|
|
4542
|
+
]);
|
|
4543
|
+
function canonicalize(value, options = {}) {
|
|
4544
|
+
const excluded = /* @__PURE__ */ new Set([
|
|
4545
|
+
...DEFAULT_VOLATILE_KEYS,
|
|
4546
|
+
...options.excludeKeys ?? /* @__PURE__ */ new Set()
|
|
4547
|
+
]);
|
|
4548
|
+
const normalized = normalize(value, excluded);
|
|
4549
|
+
return JSON.stringify(normalized);
|
|
4550
|
+
}
|
|
4551
|
+
function normalize(value, excluded) {
|
|
4552
|
+
if (value === null || value === void 0) return null;
|
|
4553
|
+
if (typeof value === "string") return normalizeString(value);
|
|
4554
|
+
if (typeof value === "number" || typeof value === "boolean") return value;
|
|
4555
|
+
if (Array.isArray(value)) return value.map((item) => normalize(item, excluded));
|
|
4556
|
+
if (typeof value === "object") {
|
|
4557
|
+
const out = {};
|
|
4558
|
+
const obj = value;
|
|
4559
|
+
const keys = Object.keys(obj).filter((k) => !excluded.has(k)).sort();
|
|
4560
|
+
for (const key of keys) {
|
|
4561
|
+
out[key] = normalize(obj[key], excluded);
|
|
4562
|
+
}
|
|
4563
|
+
return out;
|
|
4564
|
+
}
|
|
4565
|
+
return String(value);
|
|
4566
|
+
}
|
|
4567
|
+
function normalizeString(s) {
|
|
4568
|
+
return s.replace(/\r\n/g, "\n").replace(/\r/g, "\n").trim();
|
|
4569
|
+
}
|
|
4570
|
+
|
|
4571
|
+
// src/cache/sqlite-cache.ts
|
|
4572
|
+
function createSqliteCache(options) {
|
|
4573
|
+
const fileBacked = options.dbPath !== ":memory:";
|
|
4574
|
+
if (fileBacked) {
|
|
4575
|
+
mkdirSync3(dirname5(options.dbPath), { recursive: true, mode: 448 });
|
|
4576
|
+
}
|
|
4577
|
+
const versionNamespace = options.cacheVersionNamespace ?? "";
|
|
4578
|
+
const db = new DatabaseSync(options.dbPath);
|
|
4579
|
+
db.exec("PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000; PRAGMA synchronous=NORMAL;");
|
|
4580
|
+
db.exec(`
|
|
4581
|
+
CREATE TABLE IF NOT EXISTS cache (
|
|
4582
|
+
key TEXT PRIMARY KEY,
|
|
4583
|
+
value TEXT NOT NULL,
|
|
4584
|
+
cached_at INTEGER NOT NULL,
|
|
4585
|
+
expires_at INTEGER
|
|
4586
|
+
);
|
|
4587
|
+
`);
|
|
4588
|
+
if (fileBacked) {
|
|
4589
|
+
try {
|
|
4590
|
+
chmodSync3(options.dbPath, 384);
|
|
4591
|
+
} catch {
|
|
4592
|
+
}
|
|
4593
|
+
}
|
|
4594
|
+
const insertStmt = db.prepare(
|
|
4595
|
+
"INSERT OR REPLACE INTO cache (key, value, cached_at, expires_at) VALUES (?, ?, ?, ?)"
|
|
4596
|
+
);
|
|
4597
|
+
const selectStmt = db.prepare(
|
|
4598
|
+
"SELECT key, value, cached_at, expires_at FROM cache WHERE key = ?"
|
|
4599
|
+
);
|
|
4600
|
+
const deleteStmt = db.prepare("DELETE FROM cache WHERE key = ?");
|
|
4601
|
+
const clearAllStmt = db.prepare("DELETE FROM cache");
|
|
4602
|
+
const countStmt = db.prepare("SELECT COUNT(*) AS c FROM cache");
|
|
4603
|
+
return {
|
|
4604
|
+
keyFor(toolName, input, opts) {
|
|
4605
|
+
const canonical = canonicalize(input, opts);
|
|
4606
|
+
const hash = createHash3("sha256");
|
|
4607
|
+
if (versionNamespace.length > 0) {
|
|
4608
|
+
hash.update(versionNamespace);
|
|
4609
|
+
hash.update("|");
|
|
4610
|
+
}
|
|
4611
|
+
hash.update(toolName);
|
|
4612
|
+
hash.update(" ");
|
|
4613
|
+
hash.update(canonical);
|
|
4614
|
+
return hash.digest("hex");
|
|
4615
|
+
},
|
|
4616
|
+
get(key) {
|
|
4617
|
+
const row = selectStmt.get(key);
|
|
4618
|
+
if (!row) return null;
|
|
4619
|
+
if (row.expires_at !== null && row.expires_at < Date.now()) {
|
|
4620
|
+
deleteStmt.run(key);
|
|
4621
|
+
return null;
|
|
4622
|
+
}
|
|
4623
|
+
return {
|
|
4624
|
+
key: row.key,
|
|
4625
|
+
value: JSON.parse(row.value),
|
|
4626
|
+
cachedAt: row.cached_at
|
|
4627
|
+
};
|
|
4628
|
+
},
|
|
4629
|
+
set(key, value, ttlMs) {
|
|
4630
|
+
const now = Date.now();
|
|
4631
|
+
const expiresAt = typeof ttlMs === "number" ? now + ttlMs : null;
|
|
4632
|
+
insertStmt.run(key, JSON.stringify(value), now, expiresAt);
|
|
4633
|
+
},
|
|
4634
|
+
delete(key) {
|
|
4635
|
+
const result = deleteStmt.run(key);
|
|
4636
|
+
return Number(result.changes) > 0;
|
|
4637
|
+
},
|
|
4638
|
+
clear() {
|
|
4639
|
+
const result = clearAllStmt.run();
|
|
4640
|
+
return Number(result.changes);
|
|
4641
|
+
},
|
|
4642
|
+
size() {
|
|
4643
|
+
const r = countStmt.get();
|
|
4644
|
+
return r?.c ?? 0;
|
|
4645
|
+
},
|
|
4646
|
+
close() {
|
|
4647
|
+
db.close();
|
|
4648
|
+
}
|
|
4649
|
+
};
|
|
4650
|
+
}
|
|
4651
|
+
|
|
4652
|
+
// src/ratchets/stub-client.ts
|
|
4653
|
+
var HOLLOW_PATTERNS = [
|
|
4654
|
+
{
|
|
4655
|
+
regex: /expect\([^)]*\)\.toBeDefined\(\)/g,
|
|
4656
|
+
code: "hollow.toBeDefined",
|
|
4657
|
+
severity: "warn",
|
|
4658
|
+
message: "toBeDefined() only proves the call returned something; assert a value."
|
|
4659
|
+
},
|
|
4660
|
+
{
|
|
4661
|
+
regex: /expect\((true|false|1|0|'[^']*'|"[^"]*")\)\.toBe\(\1\)/g,
|
|
4662
|
+
code: "hollow.tautology",
|
|
4663
|
+
severity: "error",
|
|
4664
|
+
message: "Tautological assertion: literal compared to itself."
|
|
4665
|
+
},
|
|
4666
|
+
{
|
|
4667
|
+
regex: /expect\([^)]*\)\.toBeTruthy\(\)|expect\([^)]*\)\.toBeFalsy\(\)/g,
|
|
4668
|
+
code: "hollow.toBeTruthy",
|
|
4669
|
+
severity: "warn",
|
|
4670
|
+
message: "toBeTruthy/toBeFalsy hide what value you expected; prefer toBe(...) / toEqual(...)."
|
|
4671
|
+
}
|
|
4672
|
+
];
|
|
4673
|
+
var STRONG_PATTERNS = [
|
|
4674
|
+
/\.toBe\(/g,
|
|
4675
|
+
/\.toEqual\(/g,
|
|
4676
|
+
/\.toMatchObject\(/g,
|
|
4677
|
+
/\.toContain\(/g,
|
|
4678
|
+
/\.toHaveBeenCalledWith\(/g,
|
|
4679
|
+
/\.toStrictEqual\(/g
|
|
4680
|
+
];
|
|
4681
|
+
var MAX_SCORE = 100;
|
|
4682
|
+
var WEAK_FLOOR = 30;
|
|
4683
|
+
var HOLLOW_PENALTY = 25;
|
|
4684
|
+
var HOLLOW_TAUTOLOGY_PENALTY = 50;
|
|
4685
|
+
var STRONG_REWARD_PER = 8;
|
|
4686
|
+
function createStubRatchetClient() {
|
|
4687
|
+
return {
|
|
4688
|
+
async checkAssertionStrength(req) {
|
|
4689
|
+
const findings = [];
|
|
4690
|
+
let score = 50;
|
|
4691
|
+
let hollowHits = 0;
|
|
4692
|
+
let tautologyHits = 0;
|
|
4693
|
+
for (const pat of HOLLOW_PATTERNS) {
|
|
4694
|
+
pat.regex.lastIndex = 0;
|
|
4695
|
+
let m;
|
|
4696
|
+
while ((m = pat.regex.exec(req.source)) !== null) {
|
|
4697
|
+
findings.push({
|
|
4698
|
+
line_excerpt: clip(m[0], 80),
|
|
4699
|
+
severity: pat.severity,
|
|
4700
|
+
code: pat.code,
|
|
4701
|
+
message: pat.message
|
|
4702
|
+
});
|
|
4703
|
+
if (pat.code === "hollow.tautology") tautologyHits += 1;
|
|
4704
|
+
else hollowHits += 1;
|
|
4705
|
+
}
|
|
4706
|
+
}
|
|
4707
|
+
let strongHits = 0;
|
|
4708
|
+
for (const pat of STRONG_PATTERNS) {
|
|
4709
|
+
pat.lastIndex = 0;
|
|
4710
|
+
const matches = req.source.match(pat);
|
|
4711
|
+
if (matches) strongHits += matches.length;
|
|
4712
|
+
}
|
|
4713
|
+
score -= hollowHits * HOLLOW_PENALTY;
|
|
4714
|
+
score -= tautologyHits * HOLLOW_TAUTOLOGY_PENALTY;
|
|
4715
|
+
score += strongHits * STRONG_REWARD_PER;
|
|
4716
|
+
score = Math.max(0, Math.min(MAX_SCORE, score));
|
|
4717
|
+
const verdict = tautologyHits > 0 || hollowHits > 0 && strongHits === 0 ? "hollow" : score < WEAK_FLOOR ? "weak" : "strong";
|
|
4718
|
+
const summary = buildSummary2({
|
|
4719
|
+
verdict,
|
|
4720
|
+
score,
|
|
4721
|
+
strongHits,
|
|
4722
|
+
hollowHits,
|
|
4723
|
+
tautologyHits,
|
|
4724
|
+
filePath: req.filePath
|
|
4725
|
+
});
|
|
4726
|
+
return { score, maxScore: MAX_SCORE, verdict, findings, summary };
|
|
4727
|
+
}
|
|
4728
|
+
};
|
|
4729
|
+
}
|
|
4730
|
+
function clip(s, n) {
|
|
4731
|
+
return s.length <= n ? s : s.slice(0, n) + "\u2026";
|
|
4732
|
+
}
|
|
4733
|
+
function buildSummary2(args) {
|
|
4734
|
+
const fileTag = args.filePath ? `${args.filePath}: ` : "";
|
|
4735
|
+
return `${fileTag}verdict=${args.verdict} score=${args.score}/${MAX_SCORE} (strong=${args.strongHits} hollow=${args.hollowHits} tautology=${args.tautologyHits}) [stub-ratchet \u2014 real thresholds load from @algosuite/ratchets-generic in Phase 2]`;
|
|
4736
|
+
}
|
|
4737
|
+
|
|
4738
|
+
// src/consensus/null-client.ts
|
|
4739
|
+
var NULL_CLIENT_DEFAULT_REASON = "consensus-engine-package-pending";
|
|
4740
|
+
var ENGINE_UNAVAILABLE_REASONS = {
|
|
4741
|
+
/** Fewer than 2 provider API keys present in env — can't form a panel. */
|
|
4742
|
+
CREDENTIALS_MISSING: "consensus-credentials-missing",
|
|
4743
|
+
/** `@algosuite/consensus-engine` module not resolvable / installed. */
|
|
4744
|
+
ENGINE_PACKAGE_NOT_INSTALLED: "consensus-engine-package-not-installed",
|
|
4745
|
+
/** `functions-shared` module not resolvable in the host's node_modules. */
|
|
4746
|
+
FUNCTIONS_SHARED_NOT_INSTALLED: "consensus-functions-shared-not-installed",
|
|
4747
|
+
/** Engine + functions-shared loaded but adapter construction failed for all providers. */
|
|
4748
|
+
PANEL_CONSTRUCTION_FAILED: "consensus-panel-construction-failed"
|
|
4749
|
+
};
|
|
4750
|
+
function createNullConsensusEngineClient(reason = NULL_CLIENT_DEFAULT_REASON) {
|
|
4751
|
+
return {
|
|
4752
|
+
async run() {
|
|
4753
|
+
return { ok: false, reason };
|
|
4754
|
+
}
|
|
4755
|
+
};
|
|
4756
|
+
}
|
|
4757
|
+
|
|
4758
|
+
// src/consensus/engine-client.ts
|
|
4759
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
4760
|
+
async function loadEngineModule() {
|
|
4761
|
+
try {
|
|
4762
|
+
const moduleName = ["@algosuite", "consensus-engine"].join("/");
|
|
4763
|
+
const mod = await import(moduleName);
|
|
4764
|
+
if (mod !== null && typeof mod === "object" && "runConsensus" in mod && typeof mod.runConsensus === "function") {
|
|
4765
|
+
return mod;
|
|
4766
|
+
}
|
|
4767
|
+
return null;
|
|
4768
|
+
} catch {
|
|
4769
|
+
return null;
|
|
4770
|
+
}
|
|
4771
|
+
}
|
|
4772
|
+
function createEngineConsensusClient(options) {
|
|
4773
|
+
const cachedEngine = options.engineModule;
|
|
4774
|
+
let loaded = null;
|
|
4775
|
+
async function getEngine() {
|
|
4776
|
+
if (cachedEngine !== void 0) return cachedEngine;
|
|
4777
|
+
if (loaded === null) loaded = loadEngineModule();
|
|
4778
|
+
return loaded;
|
|
4779
|
+
}
|
|
4780
|
+
return {
|
|
4781
|
+
async run(request) {
|
|
4782
|
+
const signal = request.signal;
|
|
4783
|
+
if (signal?.aborted) {
|
|
4784
|
+
return { ok: false, reason: "cancelled" };
|
|
4785
|
+
}
|
|
4786
|
+
const engine = await getEngine();
|
|
4787
|
+
if (engine === null) {
|
|
4788
|
+
return { ok: false, reason: "consensus-engine-package-pending" };
|
|
4789
|
+
}
|
|
4790
|
+
if (options.panel.length < 2) {
|
|
4791
|
+
return { ok: false, reason: "panel-too-small" };
|
|
4792
|
+
}
|
|
4793
|
+
try {
|
|
4794
|
+
const engineRequest = {
|
|
4795
|
+
gate_type: request.gate_type,
|
|
4796
|
+
prompt: request.prompt,
|
|
4797
|
+
...request.system_prompt !== void 0 ? { system_prompt: request.system_prompt } : {}
|
|
4798
|
+
};
|
|
4799
|
+
const panel = signal === void 0 ? options.panel : options.panel.map((adapter) => ({
|
|
4800
|
+
...adapter,
|
|
4801
|
+
call: (args) => adapter.call({ ...args, abort_signal: signal })
|
|
4802
|
+
}));
|
|
4803
|
+
const engineOptions = {
|
|
4804
|
+
panel,
|
|
4805
|
+
...options.per_model_timeout_ms !== void 0 ? { per_model_timeout_ms: options.per_model_timeout_ms } : {}
|
|
4806
|
+
};
|
|
4807
|
+
const result = signal === void 0 ? await engine.runConsensus(engineRequest, engineOptions) : await Promise.race([
|
|
4808
|
+
engine.runConsensus(engineRequest, engineOptions),
|
|
4809
|
+
new Promise((_, reject) => {
|
|
4810
|
+
const onAbort = () => {
|
|
4811
|
+
reject(new Error("__VO_MCP_CANCELLED__"));
|
|
4812
|
+
};
|
|
4813
|
+
if (signal.aborted) onAbort();
|
|
4814
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
4815
|
+
})
|
|
4816
|
+
]);
|
|
4817
|
+
if (!result.ok || result.response === void 0) {
|
|
4818
|
+
return {
|
|
4819
|
+
ok: false,
|
|
4820
|
+
reason: result.error?.reason ?? "engine-returned-not-ok"
|
|
4821
|
+
};
|
|
4822
|
+
}
|
|
4823
|
+
return {
|
|
4824
|
+
ok: true,
|
|
4825
|
+
synthesized_verdict: result.response.synthesized_verdict,
|
|
4826
|
+
per_model_verdicts: result.response.per_model_verdicts,
|
|
4827
|
+
degraded: result.response.degraded,
|
|
4828
|
+
duration_ms: result.response.duration_ms,
|
|
4829
|
+
engine_version: result.response.engine_version,
|
|
4830
|
+
// Phase 2 Lane D-1 — forward escalation signal when present.
|
|
4831
|
+
...result.response.escalation_required !== void 0 ? { escalation_required: result.response.escalation_required } : {},
|
|
4832
|
+
...result.response.escalation_reason !== void 0 ? { escalation_reason: result.response.escalation_reason } : {}
|
|
4833
|
+
};
|
|
4834
|
+
} catch (err) {
|
|
4835
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4836
|
+
if (message === "__VO_MCP_CANCELLED__" || signal?.aborted || err instanceof Error && err.name === "AbortError") {
|
|
4837
|
+
return { ok: false, reason: "cancelled" };
|
|
4838
|
+
}
|
|
4839
|
+
return { ok: false, reason: `engine-threw: ${message}` };
|
|
4840
|
+
}
|
|
4841
|
+
}
|
|
4842
|
+
};
|
|
4843
|
+
}
|
|
4844
|
+
var DEFAULT_MODELS = {
|
|
4845
|
+
// These ids match the strategic-roadmap §4 `newsStandard` / `newsDeep` panel
|
|
4846
|
+
// intent — current production model ids. Per handoff §C-3 these MUST come
|
|
4847
|
+
// from `CONSENSUS_PANELS` in `functions-shared/shared-model-resolvers.ts`
|
|
4848
|
+
// for V1; placeholder defaults here keep Phase 2 Lane A non-blocking.
|
|
4849
|
+
anthropic: "claude-opus-4-7",
|
|
4850
|
+
openai: "gpt-5",
|
|
4851
|
+
// gemini-2.5-FLASH (not -pro): flash accepts the default thinkingBudget=0 from
|
|
4852
|
+
// callGeminiWithMetrics; 2.5-pro REJECTS budget 0 ("only works in thinking mode").
|
|
4853
|
+
// Flash is also ~10x cheaper. 2026-06-02.
|
|
4854
|
+
google: "gemini-2.5-flash",
|
|
4855
|
+
deepseek: "deepseek-chat"
|
|
4856
|
+
};
|
|
4857
|
+
function probeProviders(env = process.env) {
|
|
4858
|
+
const out = [];
|
|
4859
|
+
if ((env["ANTHROPIC_API_KEY"] ?? "").trim().length > 0) out.push("anthropic");
|
|
4860
|
+
if ((env["OPENAI_API_KEY"] ?? "").trim().length > 0) out.push("openai");
|
|
4861
|
+
if ((env["GOOGLE_API_KEY"] ?? "").trim().length > 0) out.push("google");
|
|
4862
|
+
if ((env["DEEPSEEK_API_KEY"] ?? "").trim().length > 0) out.push("deepseek");
|
|
4863
|
+
return out;
|
|
4864
|
+
}
|
|
4865
|
+
async function loadFactoryAndCallers(injectedEngine, injectedShared) {
|
|
4866
|
+
let engineMod = injectedEngine;
|
|
4867
|
+
if (engineMod === void 0) {
|
|
4868
|
+
try {
|
|
4869
|
+
engineMod = await import(["@algosuite", "consensus-engine"].join("/"));
|
|
4870
|
+
} catch {
|
|
4871
|
+
return { ok: false, reason: ENGINE_UNAVAILABLE_REASONS.ENGINE_PACKAGE_NOT_INSTALLED };
|
|
4872
|
+
}
|
|
4873
|
+
}
|
|
4874
|
+
if (engineMod === null || typeof engineMod !== "object" || typeof engineMod.createAdapter !== "function" || typeof engineMod.runConsensus !== "function") {
|
|
4875
|
+
return { ok: false, reason: ENGINE_UNAVAILABLE_REASONS.ENGINE_PACKAGE_NOT_INSTALLED };
|
|
4876
|
+
}
|
|
4877
|
+
let sharedMod = injectedShared;
|
|
4878
|
+
if (sharedMod === void 0) {
|
|
4879
|
+
try {
|
|
4880
|
+
sharedMod = await import(["functions", "shared"].join("-"));
|
|
4881
|
+
} catch {
|
|
4882
|
+
return { ok: false, reason: ENGINE_UNAVAILABLE_REASONS.FUNCTIONS_SHARED_NOT_INSTALLED };
|
|
4883
|
+
}
|
|
4884
|
+
}
|
|
4885
|
+
if (sharedMod === null || typeof sharedMod !== "object" || typeof sharedMod.callAnthropicWithMetrics !== "function" || typeof sharedMod.callOpenAIWithMetrics !== "function" || typeof sharedMod.callGeminiWithMetrics !== "function") {
|
|
4886
|
+
return { ok: false, reason: ENGINE_UNAVAILABLE_REASONS.FUNCTIONS_SHARED_NOT_INSTALLED };
|
|
4887
|
+
}
|
|
4888
|
+
return {
|
|
4889
|
+
ok: true,
|
|
4890
|
+
engine: engineMod,
|
|
4891
|
+
shared: sharedMod
|
|
4892
|
+
};
|
|
4893
|
+
}
|
|
4894
|
+
async function tryCreateEngineConsensusClientFromEnvAsync(options = {}) {
|
|
4895
|
+
const env = options.envSource ?? process.env;
|
|
4896
|
+
const providers = probeProviders(env);
|
|
4897
|
+
if (providers.length < 2) {
|
|
4898
|
+
return createNullConsensusEngineClient(ENGINE_UNAVAILABLE_REASONS.CREDENTIALS_MISSING);
|
|
4899
|
+
}
|
|
4900
|
+
const loaded = await loadFactoryAndCallers(options.engineModule, options.functionsSharedModule);
|
|
4901
|
+
if (!loaded.ok) {
|
|
4902
|
+
return createNullConsensusEngineClient(loaded.reason);
|
|
4903
|
+
}
|
|
4904
|
+
const callerByProvider = {
|
|
4905
|
+
anthropic: loaded.shared.callAnthropicWithMetrics,
|
|
4906
|
+
openai: loaded.shared.callOpenAIWithMetrics,
|
|
4907
|
+
google: loaded.shared.callGeminiWithMetrics,
|
|
4908
|
+
deepseek: loaded.shared.callDeepSeekWithMetrics
|
|
4909
|
+
};
|
|
4910
|
+
const modelByProvider = {
|
|
4911
|
+
anthropic: options.models?.anthropic ?? DEFAULT_MODELS.anthropic,
|
|
4912
|
+
openai: options.models?.openai ?? DEFAULT_MODELS.openai,
|
|
4913
|
+
google: options.models?.google ?? DEFAULT_MODELS.google,
|
|
4914
|
+
deepseek: options.models?.deepseek ?? DEFAULT_MODELS.deepseek
|
|
4915
|
+
};
|
|
4916
|
+
const panel = [];
|
|
4917
|
+
for (const p of providers) {
|
|
4918
|
+
try {
|
|
4919
|
+
const adapter = loaded.engine.createAdapter(p, {
|
|
4920
|
+
model: modelByProvider[p],
|
|
4921
|
+
caller: callerByProvider[p],
|
|
4922
|
+
envSource: env
|
|
4923
|
+
});
|
|
4924
|
+
panel.push(adapter);
|
|
4925
|
+
} catch {
|
|
4926
|
+
}
|
|
4927
|
+
}
|
|
4928
|
+
if (panel.length < 2) {
|
|
4929
|
+
return createNullConsensusEngineClient(ENGINE_UNAVAILABLE_REASONS.PANEL_CONSTRUCTION_FAILED);
|
|
4930
|
+
}
|
|
4931
|
+
const clientOptions = {
|
|
4932
|
+
panel,
|
|
4933
|
+
engineModule: loaded.engine,
|
|
4934
|
+
...options.per_model_timeout_ms !== void 0 ? { per_model_timeout_ms: options.per_model_timeout_ms } : {}
|
|
4935
|
+
};
|
|
4936
|
+
return createEngineConsensusClient(clientOptions);
|
|
4937
|
+
}
|
|
4938
|
+
function tryCreateEngineConsensusClientFromEnv(options = {}) {
|
|
4939
|
+
const env = options.envSource ?? process.env;
|
|
4940
|
+
if (probeProviders(env).length < 2) {
|
|
4941
|
+
return createNullConsensusEngineClient(ENGINE_UNAVAILABLE_REASONS.CREDENTIALS_MISSING);
|
|
4942
|
+
}
|
|
4943
|
+
let resolved = null;
|
|
4944
|
+
return {
|
|
4945
|
+
async run(request) {
|
|
4946
|
+
if (resolved === null) {
|
|
4947
|
+
resolved = tryCreateEngineConsensusClientFromEnvAsync(options);
|
|
4948
|
+
}
|
|
4949
|
+
const real = await resolved;
|
|
4950
|
+
return real.run(request);
|
|
4951
|
+
}
|
|
4952
|
+
};
|
|
4953
|
+
}
|
|
4954
|
+
export {
|
|
4955
|
+
canonicalize,
|
|
4956
|
+
createEngineConsensusClient,
|
|
4957
|
+
createFileEventsWriter,
|
|
4958
|
+
createMemoryEventsWriter,
|
|
4959
|
+
createNullConsensusEngineClient,
|
|
4960
|
+
createServer,
|
|
4961
|
+
createSqliteCache,
|
|
4962
|
+
createStubRatchetClient,
|
|
4963
|
+
defaultEventsPath,
|
|
4964
|
+
listToolNames,
|
|
4965
|
+
sanitizeExcerpt,
|
|
4966
|
+
tryCreateEngineConsensusClientFromEnv
|
|
4967
|
+
};
|
|
4968
|
+
//# sourceMappingURL=index.js.map
|