@roodriigoooo/pi-scrutiny 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.
@@ -0,0 +1,407 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
4
+ import type { Council, PanelMember, PanelMode, ScrutinyConfig, ScrutinyConfigSource, ScrutinyParams, ScrutinySurface, ThinkingLevel, VerifyCheckSpec } from "./types.js";
5
+
6
+ const DEFAULT_MAX_PANEL_MODELS = 4;
7
+ const DEFAULT_PANEL_TIMEOUT_MS = 180_000;
8
+ const DEFAULT_JUDGE_TIMEOUT_MS = 60_000;
9
+ const DEFAULT_VERIFY_TIMEOUT_MS = 120_000;
10
+ const DEFAULT_PANEL_OUTPUT_CHARS = 24_000;
11
+ const DEFAULT_JUDGE_OUTPUT_CHARS = 24_000;
12
+ const DEFAULT_DIFF_CHARS = 16_000;
13
+ const CONFIG_DIR_NAME = ".pi";
14
+
15
+ const DEFAULT_VERIFY_CHECKS: VerifyCheckSpec[] = [
16
+ { name: "typecheck", command: "npm", args: ["run", "check"], timeoutMs: 60_000 },
17
+ { name: "tests", command: "npm", args: ["test"], timeoutMs: 120_000 },
18
+ { name: "lint", command: "npm", args: ["run", "lint"], timeoutMs: 60_000 },
19
+ ];
20
+
21
+ export const SCRUTINY_SURFACES: ScrutinySurface[] = ["consult", "hypotheses", "criteria", "repo-map", "risks", "verify"];
22
+
23
+ export const SURFACE_DEFAULTS: Record<ScrutinySurface, { panelCount: number; panelMode?: PanelMode; judgeMode: "auto" | "off" | "on"; includeGitDiff: boolean; verify: boolean }> = {
24
+ consult: { panelCount: 2, panelMode: "replicate", judgeMode: "auto", includeGitDiff: false, verify: false },
25
+ hypotheses: { panelCount: 2, panelMode: "replicate", judgeMode: "off", includeGitDiff: true, verify: false },
26
+ criteria: { panelCount: 2, panelMode: "replicate", judgeMode: "off", includeGitDiff: true, verify: false },
27
+ "repo-map": { panelCount: 2, panelMode: "roles", judgeMode: "off", includeGitDiff: true, verify: false },
28
+ risks: { panelCount: 2, panelMode: "roles", judgeMode: "off", includeGitDiff: true, verify: true },
29
+ verify: { panelCount: 0, judgeMode: "off", includeGitDiff: true, verify: true },
30
+ };
31
+
32
+ export type ScrutinyConfigOptions = { cwd?: string; projectTrusted?: boolean };
33
+
34
+ type ConfigPatch = Partial<Omit<ScrutinyConfig, "configSources">>;
35
+
36
+ export function userConfigPath(): string {
37
+ return path.join(getAgentDir(), "scrutiny.json");
38
+ }
39
+
40
+ export function projectConfigPath(cwd: string): string {
41
+ return path.join(cwd, CONFIG_DIR_NAME, "scrutiny.json");
42
+ }
43
+
44
+ export function exampleConfigJson(): string {
45
+ return `${JSON.stringify(
46
+ {
47
+ panel: [
48
+ { model: "openai-codex/gpt-5.4-mini", thinking: "low" },
49
+ { model: "opencode-go/kimi-k2.7-code", thinking: "off" },
50
+ ],
51
+ judge: "openai-codex/gpt-5.4-mini",
52
+ maxPanelModels: 4,
53
+ includeGitDiff: true,
54
+ verifyChecks: [{ name: "typecheck", command: "npm", args: ["run", "check"], timeoutMs: 60000 }],
55
+ panels: {
56
+ "code-duo": {
57
+ surface: "risks",
58
+ members: [
59
+ { model: "openai-codex/gpt-5.4-mini", lens: "concurrency", thinking: "low" },
60
+ { model: "opencode-go/kimi-k2.7-code", lens: "reactive-chain", thinking: "off" },
61
+ ],
62
+ verify: true,
63
+ judgeMode: "off",
64
+ },
65
+ },
66
+ },
67
+ null,
68
+ 2,
69
+ )}\n`;
70
+ }
71
+
72
+ export function readScrutinyConfig(options: ScrutinyConfigOptions = {}): ScrutinyConfig {
73
+ let config = baseConfig();
74
+ const configSources: ScrutinyConfigSource[] = [];
75
+
76
+ for (const source of configFileSources(options)) {
77
+ if (source.status === "skipped" || source.status === "missing") {
78
+ configSources.push(source);
79
+ continue;
80
+ }
81
+ try {
82
+ const raw = fs.readFileSync(source.path!, "utf8");
83
+ const patch = parseConfigObject(JSON.parse(raw));
84
+ config = mergeConfig(config, patch);
85
+ configSources.push({ ...source, status: "loaded" });
86
+ } catch (error) {
87
+ configSources.push({ ...source, status: "error", reason: error instanceof Error ? error.message : String(error) });
88
+ }
89
+ }
90
+
91
+ const envPatch = readEnvPatch();
92
+ if (Object.keys(envPatch).length > 0) {
93
+ config = mergeConfig(config, envPatch);
94
+ configSources.push({ scope: "env", status: "loaded", reason: "PI_SCRUTINY_*" });
95
+ }
96
+
97
+ return { ...config, configSources };
98
+ }
99
+
100
+ export function readEnvConfig(): ScrutinyConfig {
101
+ const envPatch = readEnvPatch();
102
+ const config = mergeConfig(baseConfig(), envPatch);
103
+ const configSources: ScrutinyConfigSource[] = Object.keys(envPatch).length > 0 ? [{ scope: "env", status: "loaded", reason: "PI_SCRUTINY_*" }] : [];
104
+ return { ...config, configSources };
105
+ }
106
+
107
+ export function resolvePanel(input: { panel?: string[]; panelMembers?: PanelMember[]; maxPanelModels?: number }, config: ScrutinyConfig): PanelMember[] {
108
+ const members = input.panelMembers?.length ? input.panelMembers : input.panel?.length ? input.panel.map((model) => ({ model })) : config.panel;
109
+ const unique = new Map<string, PanelMember>();
110
+ for (const member of members) {
111
+ const model = member.model.trim();
112
+ if (model) unique.set(model, { ...member, model });
113
+ }
114
+ return [...unique.values()].slice(0, input.maxPanelModels ?? config.maxPanelModels);
115
+ }
116
+
117
+ export function resolveJudge(input: { judge?: string }, config: ScrutinyConfig, panel: PanelMember[]): string | undefined {
118
+ return input.judge?.trim() || config.judge || panel[0]?.model;
119
+ }
120
+
121
+ export function resolveTools(input: { tools?: string[] }, config: ScrutinyConfig): string[] {
122
+ return (input.tools && input.tools.length > 0 ? input.tools : config.tools).map((tool) => tool.trim()).filter(Boolean);
123
+ }
124
+
125
+ export function loadCouncils(options: ScrutinyConfigOptions = {}): Council[] {
126
+ return readScrutinyConfig(options).councils;
127
+ }
128
+
129
+ export function findCouncil(name: string, options: ScrutinyConfigOptions = {}): Council | undefined {
130
+ return loadCouncils(options).find((c) => c.name === name);
131
+ }
132
+
133
+ export function councilToParams(council: Council, prompt: string): ScrutinyParams {
134
+ return {
135
+ prompt,
136
+ surface: council.surface,
137
+ panelMembers: council.panelists,
138
+ judge: council.judge,
139
+ judgeMode: council.judgeMode,
140
+ includeGitDiff: council.includeGitDiff,
141
+ verify: council.verify,
142
+ };
143
+ }
144
+
145
+ function baseConfig(): ScrutinyConfig {
146
+ return {
147
+ panel: [],
148
+ judge: undefined,
149
+ maxPanelModels: DEFAULT_MAX_PANEL_MODELS,
150
+ maxPanelOutputChars: DEFAULT_PANEL_OUTPUT_CHARS,
151
+ maxJudgeOutputChars: DEFAULT_JUDGE_OUTPUT_CHARS,
152
+ panelTimeoutMs: DEFAULT_PANEL_TIMEOUT_MS,
153
+ judgeTimeoutMs: DEFAULT_JUDGE_TIMEOUT_MS,
154
+ verifyTimeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
155
+ includeGitDiff: true,
156
+ gitDiffCharLimit: DEFAULT_DIFF_CHARS,
157
+ tools: [],
158
+ verifyChecks: DEFAULT_VERIFY_CHECKS,
159
+ councils: [],
160
+ configSources: [],
161
+ };
162
+ }
163
+
164
+ function configFileSources(options: ScrutinyConfigOptions): ScrutinyConfigSource[] {
165
+ const sources: ScrutinyConfigSource[] = [];
166
+ const globalPath = userConfigPath();
167
+ sources.push(fs.existsSync(globalPath) ? { scope: "global", path: globalPath, status: "loaded" } : { scope: "global", path: globalPath, status: "missing" });
168
+
169
+ if (options.cwd) {
170
+ const projectPath = projectConfigPath(options.cwd);
171
+ if (!fs.existsSync(projectPath)) sources.push({ scope: "project", path: projectPath, status: "missing" });
172
+ else if (!options.projectTrusted) sources.push({ scope: "project", path: projectPath, status: "skipped", reason: "project not trusted" });
173
+ else sources.push({ scope: "project", path: projectPath, status: "loaded" });
174
+ }
175
+ return sources;
176
+ }
177
+
178
+ function readEnvPatch(): ConfigPatch {
179
+ const patch: ConfigPatch = {};
180
+ if ("PI_SCRUTINY_PANEL" in process.env) patch.panel = parseCsv(process.env.PI_SCRUTINY_PANEL).map((model) => ({ model }));
181
+ if ("PI_SCRUTINY_JUDGE" in process.env) patch.judge = emptyToUndefined(process.env.PI_SCRUTINY_JUDGE);
182
+ if ("PI_SCRUTINY_MAX_PANEL_MODELS" in process.env) patch.maxPanelModels = parseIntEnv("PI_SCRUTINY_MAX_PANEL_MODELS", DEFAULT_MAX_PANEL_MODELS, 1, 8);
183
+ if ("PI_SCRUTINY_MAX_PANEL_OUTPUT_CHARS" in process.env) patch.maxPanelOutputChars = parseIntEnv("PI_SCRUTINY_MAX_PANEL_OUTPUT_CHARS", DEFAULT_PANEL_OUTPUT_CHARS, 2_000, 200_000);
184
+ if ("PI_SCRUTINY_MAX_JUDGE_OUTPUT_CHARS" in process.env) patch.maxJudgeOutputChars = parseIntEnv("PI_SCRUTINY_MAX_JUDGE_OUTPUT_CHARS", DEFAULT_JUDGE_OUTPUT_CHARS, 2_000, 200_000);
185
+ if ("PI_SCRUTINY_PANEL_TIMEOUT_MS" in process.env) patch.panelTimeoutMs = parseIntEnv("PI_SCRUTINY_PANEL_TIMEOUT_MS", DEFAULT_PANEL_TIMEOUT_MS, 5_000, 30 * 60_000);
186
+ if ("PI_SCRUTINY_JUDGE_TIMEOUT_MS" in process.env) patch.judgeTimeoutMs = parseIntEnv("PI_SCRUTINY_JUDGE_TIMEOUT_MS", DEFAULT_JUDGE_TIMEOUT_MS, 5_000, 30 * 60_000);
187
+ if ("PI_SCRUTINY_VERIFY_TIMEOUT_MS" in process.env) patch.verifyTimeoutMs = parseIntEnv("PI_SCRUTINY_VERIFY_TIMEOUT_MS", DEFAULT_VERIFY_TIMEOUT_MS, 5_000, 30 * 60_000);
188
+ if ("PI_SCRUTINY_INCLUDE_GIT_DIFF" in process.env) patch.includeGitDiff = parseBoolEnv("PI_SCRUTINY_INCLUDE_GIT_DIFF", true);
189
+ if ("PI_SCRUTINY_GIT_DIFF_CHARS" in process.env) patch.gitDiffCharLimit = parseIntEnv("PI_SCRUTINY_GIT_DIFF_CHARS", DEFAULT_DIFF_CHARS, 0, 200_000);
190
+ if ("PI_SCRUTINY_TOOLS" in process.env) patch.tools = parseCsv(process.env.PI_SCRUTINY_TOOLS);
191
+ if ("PI_SCRUTINY_VERIFY_CHECKS" in process.env) patch.verifyChecks = parseVerifyChecksJson(process.env.PI_SCRUTINY_VERIFY_CHECKS) ?? DEFAULT_VERIFY_CHECKS;
192
+ if ("PI_SCRUTINY_COUNCILS" in process.env) patch.councils = parseCouncilsJson(process.env.PI_SCRUTINY_COUNCILS) ?? [];
193
+ if ("PI_SCRUTINY_PANELS" in process.env) patch.councils = parseCouncilsJson(process.env.PI_SCRUTINY_PANELS) ?? [];
194
+ return patch;
195
+ }
196
+
197
+ function parseConfigObject(value: unknown): ConfigPatch {
198
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
199
+ const input = value as Record<string, unknown>;
200
+ const patch: ConfigPatch = {};
201
+ const panel = parsePanelMembers(input.panel);
202
+ if (panel) patch.panel = panel;
203
+ const judge = parseString(input.judge);
204
+ if (judge !== undefined || "judge" in input) patch.judge = judge;
205
+ const maxPanelModels = parseNumber(input.maxPanelModels, 1, 8);
206
+ if (maxPanelModels !== undefined) patch.maxPanelModels = maxPanelModels;
207
+ const maxPanelOutputChars = parseNumber(input.maxPanelOutputChars, 2_000, 200_000);
208
+ if (maxPanelOutputChars !== undefined) patch.maxPanelOutputChars = maxPanelOutputChars;
209
+ const maxJudgeOutputChars = parseNumber(input.maxJudgeOutputChars, 2_000, 200_000);
210
+ if (maxJudgeOutputChars !== undefined) patch.maxJudgeOutputChars = maxJudgeOutputChars;
211
+ const panelTimeoutMs = parseNumber(input.panelTimeoutMs, 5_000, 30 * 60_000);
212
+ if (panelTimeoutMs !== undefined) patch.panelTimeoutMs = panelTimeoutMs;
213
+ const judgeTimeoutMs = parseNumber(input.judgeTimeoutMs, 5_000, 30 * 60_000);
214
+ if (judgeTimeoutMs !== undefined) patch.judgeTimeoutMs = judgeTimeoutMs;
215
+ const verifyTimeoutMs = parseNumber(input.verifyTimeoutMs, 5_000, 30 * 60_000);
216
+ if (verifyTimeoutMs !== undefined) patch.verifyTimeoutMs = verifyTimeoutMs;
217
+ const includeGitDiff = parseBoolValue(input.includeGitDiff);
218
+ if (includeGitDiff !== undefined) patch.includeGitDiff = includeGitDiff;
219
+ const gitDiffCharLimit = parseNumber(input.gitDiffCharLimit ?? input.gitDiffChars, 0, 200_000);
220
+ if (gitDiffCharLimit !== undefined) patch.gitDiffCharLimit = gitDiffCharLimit;
221
+ const tools = parseStringList(input.tools);
222
+ if (tools) patch.tools = tools;
223
+ const verifyChecks = parseVerifyChecksValue(input.verifyChecks);
224
+ if (verifyChecks) patch.verifyChecks = verifyChecks;
225
+ const councils = parseCouncilsValue(input.panels ?? input.councils);
226
+ if (councils) patch.councils = councils;
227
+ return patch;
228
+ }
229
+
230
+ function mergeConfig(config: ScrutinyConfig, patch: ConfigPatch): ScrutinyConfig {
231
+ const next = { ...config };
232
+ if ("panel" in patch && patch.panel) next.panel = patch.panel;
233
+ if ("judge" in patch) next.judge = patch.judge;
234
+ if ("maxPanelModels" in patch && patch.maxPanelModels !== undefined) next.maxPanelModels = patch.maxPanelModels;
235
+ if ("maxPanelOutputChars" in patch && patch.maxPanelOutputChars !== undefined) next.maxPanelOutputChars = patch.maxPanelOutputChars;
236
+ if ("maxJudgeOutputChars" in patch && patch.maxJudgeOutputChars !== undefined) next.maxJudgeOutputChars = patch.maxJudgeOutputChars;
237
+ if ("panelTimeoutMs" in patch && patch.panelTimeoutMs !== undefined) next.panelTimeoutMs = patch.panelTimeoutMs;
238
+ if ("judgeTimeoutMs" in patch && patch.judgeTimeoutMs !== undefined) next.judgeTimeoutMs = patch.judgeTimeoutMs;
239
+ if ("verifyTimeoutMs" in patch && patch.verifyTimeoutMs !== undefined) next.verifyTimeoutMs = patch.verifyTimeoutMs;
240
+ if ("includeGitDiff" in patch && patch.includeGitDiff !== undefined) next.includeGitDiff = patch.includeGitDiff;
241
+ if ("gitDiffCharLimit" in patch && patch.gitDiffCharLimit !== undefined) next.gitDiffCharLimit = patch.gitDiffCharLimit;
242
+ if ("tools" in patch && patch.tools) next.tools = patch.tools;
243
+ if ("verifyChecks" in patch && patch.verifyChecks) next.verifyChecks = patch.verifyChecks;
244
+ if ("councils" in patch && patch.councils) next.councils = patch.councils;
245
+ return next;
246
+ }
247
+
248
+ function parseCouncilsJson(value: string | undefined): Council[] | undefined {
249
+ const trimmed = value?.trim();
250
+ if (!trimmed) return [];
251
+ try {
252
+ return parseCouncilsValue(JSON.parse(trimmed));
253
+ } catch {
254
+ return undefined;
255
+ }
256
+ }
257
+
258
+ function parseCouncilsValue(value: unknown): Council[] | undefined {
259
+ if (!value) return undefined;
260
+ if (Array.isArray(value)) return value.map((item) => parseCouncil(item)).filter((item): item is Council => Boolean(item));
261
+ if (typeof value === "object") {
262
+ return Object.entries(value as Record<string, unknown>)
263
+ .map(([name, item]) => parseCouncil({ ...(item && typeof item === "object" ? (item as Record<string, unknown>) : {}), name }))
264
+ .filter((item): item is Council => Boolean(item));
265
+ }
266
+ return undefined;
267
+ }
268
+
269
+ function parseCouncil(value: unknown): Council | undefined {
270
+ if (!value || typeof value !== "object") return undefined;
271
+ const input = value as Record<string, unknown>;
272
+ const name = parseString(input.name);
273
+ const surface = parseSurface(input.surface) ?? "consult";
274
+ if (!name) return undefined;
275
+ const thinking = parseThinkingLevel(input.thinking);
276
+ const panelists = parsePanelMembers(input.members ?? input.panelists ?? input.panel)
277
+ ?.map((member) => ({ ...member, thinking: member.thinking ?? thinking })) ?? [];
278
+ const judge = parseString(input.judge);
279
+ const judgeMode = parseJudgeMode(input.judgeMode ?? input.judgePolicy);
280
+ const includeGitDiff = parseBoolValue(input.includeGitDiff);
281
+ const verify = parseVerifyPolicy(input.verify ?? input.verifyPolicy);
282
+ return { name, surface, panelists, thinking, judge, judgeMode, includeGitDiff, verify };
283
+ }
284
+
285
+ function parsePanelMembers(value: unknown): PanelMember[] | undefined {
286
+ if (typeof value === "string") return parseCsv(value).map((model) => ({ model }));
287
+ if (!Array.isArray(value)) return undefined;
288
+ const members = value
289
+ .map((item) => {
290
+ if (typeof item === "string") return { model: item.trim() };
291
+ if (!item || typeof item !== "object") return undefined;
292
+ const input = item as Record<string, unknown>;
293
+ const model = parseString(input.model);
294
+ if (!model) return undefined;
295
+ const member: PanelMember = { model };
296
+ const lens = parseString(input.lens);
297
+ const thinking = parseThinkingLevel(input.thinking);
298
+ if (lens) member.lens = lens;
299
+ if (thinking) member.thinking = thinking;
300
+ return member;
301
+ })
302
+ .filter((item): item is PanelMember => Boolean(item));
303
+ return members.length ? members : undefined;
304
+ }
305
+
306
+ function parseVerifyChecksJson(value: string | undefined): VerifyCheckSpec[] | undefined {
307
+ const trimmed = value?.trim();
308
+ if (!trimmed) return undefined;
309
+ try {
310
+ return parseVerifyChecksValue(JSON.parse(trimmed));
311
+ } catch {
312
+ return undefined;
313
+ }
314
+ }
315
+
316
+ function parseVerifyChecksValue(value: unknown): VerifyCheckSpec[] | undefined {
317
+ if (!Array.isArray(value)) return undefined;
318
+ const checks = value
319
+ .map((item) => {
320
+ if (!item || typeof item !== "object") return undefined;
321
+ const input = item as Record<string, unknown>;
322
+ const name = parseString(input.name);
323
+ const command = parseString(input.command);
324
+ if (!name || !command) return undefined;
325
+ const args = parseStringList(input.args);
326
+ const timeoutMs = parseNumber(input.timeoutMs, 1_000, 30 * 60_000);
327
+ const check: VerifyCheckSpec = { name, command };
328
+ if (args) check.args = args;
329
+ if (timeoutMs !== undefined) check.timeoutMs = timeoutMs;
330
+ return check;
331
+ })
332
+ .filter((item): item is VerifyCheckSpec => Boolean(item));
333
+ return checks.length ? checks : undefined;
334
+ }
335
+
336
+ function parseCsv(value: string | undefined): string[] {
337
+ return (value ?? "")
338
+ .split(",")
339
+ .map((part) => part.trim())
340
+ .filter(Boolean);
341
+ }
342
+
343
+ function parseStringList(value: unknown): string[] | undefined {
344
+ if (typeof value === "string") return parseCsv(value);
345
+ if (!Array.isArray(value)) return undefined;
346
+ return value.map(String).map((part) => part.trim()).filter(Boolean);
347
+ }
348
+
349
+ function parseString(value: unknown): string | undefined {
350
+ if (typeof value !== "string") return undefined;
351
+ const trimmed = value.trim();
352
+ return trimmed ? trimmed : undefined;
353
+ }
354
+
355
+ function emptyToUndefined(value: string | undefined): string | undefined {
356
+ const trimmed = value?.trim();
357
+ return trimmed ? trimmed : undefined;
358
+ }
359
+
360
+ function parseBoolValue(value: unknown): boolean | undefined {
361
+ if (typeof value === "boolean") return value;
362
+ if (typeof value !== "string") return undefined;
363
+ const normalized = value.trim().toLowerCase();
364
+ if (["1", "true", "yes", "on"].includes(normalized)) return true;
365
+ if (["0", "false", "no", "off"].includes(normalized)) return false;
366
+ return undefined;
367
+ }
368
+
369
+ function parseBoolEnv(name: string, fallback: boolean): boolean {
370
+ return parseBoolValue(process.env[name]) ?? fallback;
371
+ }
372
+
373
+ function parseNumber(value: unknown, min: number, max: number): number | undefined {
374
+ const n = typeof value === "number" ? value : typeof value === "string" ? Number.parseInt(value, 10) : Number.NaN;
375
+ if (!Number.isFinite(n)) return undefined;
376
+ return Math.max(min, Math.min(max, Math.floor(n)));
377
+ }
378
+
379
+ function parseIntEnv(name: string, fallback: number, min: number, max: number): number {
380
+ return parseNumber(process.env[name], min, max) ?? fallback;
381
+ }
382
+
383
+ function parseSurface(value: unknown): ScrutinySurface | undefined {
384
+ const surface = parseString(value) as ScrutinySurface | undefined;
385
+ return surface && SCRUTINY_SURFACES.includes(surface) ? surface : undefined;
386
+ }
387
+
388
+ function parseJudgeMode(value: unknown): Council["judgeMode"] | undefined {
389
+ const mode = parseString(value);
390
+ if (mode === "auto" || mode === "off" || mode === "on") return mode;
391
+ return undefined;
392
+ }
393
+
394
+ function parseThinkingLevel(value: unknown): ThinkingLevel | undefined {
395
+ const level = parseString(value);
396
+ if (level === "off" || level === "minimal" || level === "low" || level === "medium" || level === "high" || level === "xhigh") return level;
397
+ return undefined;
398
+ }
399
+
400
+ function parseVerifyPolicy(value: unknown): boolean | undefined {
401
+ const bool = parseBoolValue(value);
402
+ if (bool !== undefined) return bool;
403
+ const policy = parseString(value);
404
+ if (policy === "on") return true;
405
+ if (policy === "off") return false;
406
+ return undefined;
407
+ }