@oscharko-dev/keiko-verification 0.2.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,401 @@
1
+ // The verification orchestrator (ADR-0007 D2–D5). Runs each plan step sequentially through the
2
+ // UNCHANGED #6 runCommand, applying per-command resource limits, honest appliedLimits, memory
3
+ // monitoring via a SpawnFn wrapper + ResourceMonitor seam (never modifying src/tools), cross-step
4
+ // cancellation, and a redacted output digest. Error handling lives only at this IO boundary; the
5
+ // classification it feeds is pure.
6
+ import { redact } from "@oscharko-dev/keiko-security";
7
+ import { DEFAULT_COMMAND_RULES, DEFAULT_SANDBOX_POLICY, runCommand, } from "@oscharko-dev/keiko-tools";
8
+ import { nodeSpawnFn } from "@oscharko-dev/keiko-tools/internal/exec";
9
+ import { nodeWorkspaceFs } from "@oscharko-dev/keiko-workspace/internal/fs";
10
+ import { classifyOutcome } from "./classify.js";
11
+ import { classifyScripts } from "./detect.js";
12
+ import { buildAppliedLimits } from "./limits.js";
13
+ import { nodeResourceMonitor } from "./monitor.js";
14
+ // Verification runs deterministic repository gates selected by Keiko, not arbitrary model-issued
15
+ // run_command calls. Keep the model-facing defaults read-only while allowing the verification
16
+ // orchestrator to invoke npm scripts and framework-targeted npx runs through the same #6 boundary.
17
+ export const VERIFICATION_COMMAND_RULES = Object.freeze([
18
+ {
19
+ executable: "npm",
20
+ allowedSubcommands: Object.freeze(["test", "run"]),
21
+ denyFlags: Object.freeze(["-c", "--call"]),
22
+ },
23
+ {
24
+ executable: "npx",
25
+ allowedSubcommands: Object.freeze(["vitest", "jest"]),
26
+ denyFlags: Object.freeze(["-c", "--call"]),
27
+ },
28
+ ...DEFAULT_COMMAND_RULES,
29
+ ]);
30
+ const ALL_STATUSES = [
31
+ "passed",
32
+ "failed",
33
+ "skipped",
34
+ "denied",
35
+ "timed-out",
36
+ "cancelled",
37
+ "resource-exceeded",
38
+ ];
39
+ // Maps a step's resource limits and the run's resolved network policy onto a #6 SandboxPolicy:
40
+ // wall-time and output-size are enforced by runCommand. Memory is NOT a SandboxPolicy field — it is
41
+ // handled by the SpawnFn-wrapper monitor, so it does not appear here. The network policy is resolved by
42
+ // `resolveStepNetwork` from the run's enforcement mode and probed backend availability (ADR-0043); for
43
+ // an enforced "none" run, runCommand wraps the spawn through keiko-sandbox and records the attestation,
44
+ // which `buildAppliedLimits` reports honestly. The default mode resolves to "inherit", so existing
45
+ // callers are unaffected.
46
+ function policyForStep(limits, network) {
47
+ return {
48
+ ...DEFAULT_SANDBOX_POLICY,
49
+ maxOutputBytes: limits.maxOutputBytes,
50
+ defaultTimeoutMs: limits.wallTimeMs,
51
+ network,
52
+ };
53
+ }
54
+ // Pure resolution of a step's effective network policy from the run's enforcement mode and whether an
55
+ // enforcing backend is available. The default mode ("inherit") and any step that does not declare
56
+ // `network:"none"` always run with "inherit" — byte-identical to the historical orchestrator. Only an
57
+ // explicit enforce-* mode on a `network:"none"` step requests enforcement; when no backend is available
58
+ // it degrades honestly or fails closed per the mode.
59
+ export function resolveStepNetwork(limits, mode, available) {
60
+ if (mode === "inherit") {
61
+ return { kind: "run", network: "inherit" };
62
+ }
63
+ if (limits.network !== "none") {
64
+ return { kind: "run", network: limits.network };
65
+ }
66
+ if (available) {
67
+ return { kind: "run", network: "none" };
68
+ }
69
+ return mode === "enforce-or-degrade"
70
+ ? { kind: "run", network: "inherit" }
71
+ : { kind: "fail-closed" };
72
+ }
73
+ // Data-minimal output metadata. #6 already redacts/caps each stream, but regulated CLI/SDK
74
+ // summaries should not echo arbitrary repository logs or customer data by default.
75
+ function outputDigest(result) {
76
+ if (result === undefined) {
77
+ return "";
78
+ }
79
+ const combined = `${result.stdout}${result.stderr}`;
80
+ if (combined.length === 0) {
81
+ return "";
82
+ }
83
+ if (result.truncated) {
84
+ return "command output exceeded the configured output-size limit and was omitted";
85
+ }
86
+ const bytes = Buffer.byteLength(combined, "utf8");
87
+ return `command output captured (${String(bytes)} bytes) and omitted from summary`;
88
+ }
89
+ // Derives which single dimension tripped, so exactly one appliedLimits row is breached:true.
90
+ function breachedDimension(status, abortReason, result) {
91
+ if (abortReason === "memory") {
92
+ return "memory";
93
+ }
94
+ if (status === "timed-out") {
95
+ return "wall-time";
96
+ }
97
+ if (status === "resource-exceeded" && result?.truncated === true) {
98
+ return "output-size";
99
+ }
100
+ return undefined;
101
+ }
102
+ function deniedResult(step, reason) {
103
+ return {
104
+ kind: step.kind,
105
+ scriptName: step.scriptName,
106
+ command: step.command,
107
+ args: step.args,
108
+ status: "denied",
109
+ exitCode: null,
110
+ signal: null,
111
+ durationMs: 0,
112
+ truncated: false,
113
+ redacted: true,
114
+ outputSummary: "",
115
+ appliedLimits: buildAppliedLimits(step.limits, undefined),
116
+ detail: redact(reason),
117
+ };
118
+ }
119
+ function isGeneratedSkipShape(step) {
120
+ return (step.kind !== "targeted-test" &&
121
+ step.skipReason !== undefined &&
122
+ step.scriptName === undefined &&
123
+ step.command === "npm" &&
124
+ step.args.length === 2 &&
125
+ step.args[0] === "run" &&
126
+ step.args[1] === step.kind);
127
+ }
128
+ function hasWindowsDrivePrefix(value) {
129
+ return value.length >= 2 && value[1] === ":";
130
+ }
131
+ function isGeneratedTargetPath(value) {
132
+ if (value.length === 0 ||
133
+ value.startsWith("-") ||
134
+ value.startsWith("/") ||
135
+ value.includes("\u0000") ||
136
+ hasWindowsDrivePrefix(value)) {
137
+ return false;
138
+ }
139
+ return value
140
+ .split("\\")
141
+ .join("/")
142
+ .split("/")
143
+ .every((segment) => segment.length > 0 && segment !== "." && segment !== "..");
144
+ }
145
+ function scriptNameMatchesKind(step) {
146
+ if (step.kind === "targeted-test" || step.scriptName === undefined) {
147
+ return false;
148
+ }
149
+ return classifyScripts({ [step.scriptName]: "" })[step.kind] === step.scriptName;
150
+ }
151
+ function isValidTargetedStep(step) {
152
+ if (step.scriptName !== undefined || step.command !== "npx" || step.args.length < 2) {
153
+ return false;
154
+ }
155
+ if (step.args[0] === "vitest") {
156
+ return (step.args[1] === "run" &&
157
+ step.args.length >= 3 &&
158
+ step.args.slice(2).every(isGeneratedTargetPath));
159
+ }
160
+ if (step.args[0] === "jest") {
161
+ return step.args.length >= 2 && step.args.slice(1).every(isGeneratedTargetPath);
162
+ }
163
+ return false;
164
+ }
165
+ function isValidTestStep(step) {
166
+ if (step.kind === "test") {
167
+ if (step.scriptName === "test") {
168
+ return step.args.length === 1 && step.args[0] === "test";
169
+ }
170
+ return (scriptNameMatchesKind(step) &&
171
+ step.args.length === 2 &&
172
+ step.args[0] === "run" &&
173
+ step.args[1] === step.scriptName);
174
+ }
175
+ return false;
176
+ }
177
+ function isValidScriptStep(step) {
178
+ if (step.command !== "npm") {
179
+ return false;
180
+ }
181
+ if (step.kind === "test") {
182
+ return isValidTestStep(step);
183
+ }
184
+ if (!scriptNameMatchesKind(step)) {
185
+ return false;
186
+ }
187
+ return step.args.length === 2 && step.args[0] === "run" && step.args[1] === step.scriptName;
188
+ }
189
+ function isValidVerificationStep(step) {
190
+ if (isGeneratedSkipShape(step)) {
191
+ return true;
192
+ }
193
+ return step.kind === "targeted-test" ? isValidTargetedStep(step) : isValidScriptStep(step);
194
+ }
195
+ // Runs one command step through #6, wrapping the base SpawnFn with the memory monitor and owning
196
+ // the AbortController. The monitor's unwatch runs in `finally` on EVERY settle path (resolve,
197
+ // reject, denied-before-spawn where stop is never set, or a throwing await), so the sampling
198
+ // interval can never leak (ADR-0007 D3).
199
+ async function runStep(step, deps, baseSpawn, monitor, network) {
200
+ const now = deps.now ?? Date.now;
201
+ const startedAt = now();
202
+ let abortReason;
203
+ const ac = new AbortController();
204
+ const onHarnessAbort = () => {
205
+ abortReason ??= "harness";
206
+ ac.abort();
207
+ };
208
+ deps.signal?.addEventListener("abort", onHarnessAbort, { once: true });
209
+ let stop;
210
+ const spawn = (cmd, args, opts) => {
211
+ const child = baseSpawn(cmd, args, opts);
212
+ stop = monitor.watch(child.pid, step.limits.maxMemoryBytes, () => {
213
+ abortReason ??= "memory";
214
+ ac.abort();
215
+ });
216
+ return child;
217
+ };
218
+ try {
219
+ const result = await runCommand({
220
+ command: step.command,
221
+ args: step.args,
222
+ cwd: undefined,
223
+ timeoutMs: step.limits.wallTimeMs,
224
+ signal: ac.signal,
225
+ }, buildRunDeps(deps, step, spawn, network));
226
+ return { result, error: undefined, abortReason, durationMs: result.durationMs };
227
+ }
228
+ catch (error) {
229
+ return { result: undefined, error, abortReason, durationMs: now() - startedAt };
230
+ }
231
+ finally {
232
+ stop?.();
233
+ deps.signal?.removeEventListener("abort", onHarnessAbort);
234
+ }
235
+ }
236
+ function buildRunDeps(deps, step, spawn, network) {
237
+ return {
238
+ workspace: deps.workspace,
239
+ policy: policyForStep(step.limits, network),
240
+ commandRules: VERIFICATION_COMMAND_RULES,
241
+ spawn,
242
+ processEnv: deps.processEnv ?? process.env,
243
+ now: deps.now ?? Date.now,
244
+ fs: deps.fs ?? nodeWorkspaceFs,
245
+ };
246
+ }
247
+ function skippedResult(step) {
248
+ return {
249
+ kind: step.kind,
250
+ scriptName: step.scriptName,
251
+ command: step.command,
252
+ args: step.args,
253
+ status: "skipped",
254
+ exitCode: null,
255
+ signal: null,
256
+ durationMs: 0,
257
+ truncated: false,
258
+ redacted: true,
259
+ outputSummary: "",
260
+ appliedLimits: buildAppliedLimits(step.limits, undefined),
261
+ detail: redact(step.skipReason ?? "skipped"),
262
+ };
263
+ }
264
+ function cancelledResult(step) {
265
+ return {
266
+ kind: step.kind,
267
+ scriptName: step.scriptName,
268
+ command: step.command,
269
+ args: step.args,
270
+ status: "cancelled",
271
+ exitCode: null,
272
+ signal: null,
273
+ durationMs: 0,
274
+ truncated: false,
275
+ redacted: true,
276
+ outputSummary: "",
277
+ appliedLimits: buildAppliedLimits(step.limits, undefined),
278
+ detail: "cancelled before execution",
279
+ };
280
+ }
281
+ // Honest network-enforcement reporting (ADR-0043 D4): the attestation is present only when this run
282
+ // requested network:"none" and keiko-sandbox wrapped the spawn; an inherited-network run carries no
283
+ // attestation, so this is false and the appliedLimits network row stays documented-not-enforced.
284
+ function networkEnforcedOf(result) {
285
+ return result?.attestation?.networkEnforced ?? false;
286
+ }
287
+ function toResult(step, run) {
288
+ const status = classifyOutcome({
289
+ skipped: false,
290
+ result: run.result,
291
+ error: run.error,
292
+ abortReason: run.abortReason,
293
+ });
294
+ const breached = breachedDimension(status, run.abortReason, run.result);
295
+ const networkEnforced = networkEnforcedOf(run.result);
296
+ return {
297
+ kind: step.kind,
298
+ scriptName: step.scriptName,
299
+ command: step.command,
300
+ args: step.args,
301
+ status,
302
+ exitCode: run.result?.exitCode ?? null,
303
+ signal: run.result?.signal ?? null,
304
+ durationMs: run.result?.durationMs ?? run.durationMs,
305
+ truncated: run.result?.truncated ?? false,
306
+ redacted: true,
307
+ outputSummary: outputDigest(run.result),
308
+ appliedLimits: buildAppliedLimits(step.limits, breached, networkEnforced),
309
+ detail: detailFor(status, run),
310
+ };
311
+ }
312
+ function detailFor(status, run) {
313
+ if (run.abortReason === "memory") {
314
+ return "memory ceiling exceeded";
315
+ }
316
+ // For denied/failed paths the rejection message (already a redacted Error from #6 for denied)
317
+ // is re-redacted here as defence in depth before it reaches the report.
318
+ if ((status === "denied" || status === "failed") && run.error instanceof Error) {
319
+ return redact(run.error.message);
320
+ }
321
+ return undefined;
322
+ }
323
+ function overallStatus(results, cancelled) {
324
+ if (cancelled) {
325
+ return "cancelled";
326
+ }
327
+ const allOk = results.every((r) => r.status === "passed" || r.status === "skipped");
328
+ return allOk ? "passed" : "failed";
329
+ }
330
+ function countByStatus(results) {
331
+ const counts = Object.fromEntries(ALL_STATUSES.map((s) => [s, 0]));
332
+ for (const r of results) {
333
+ counts[r.status] += 1;
334
+ }
335
+ return counts;
336
+ }
337
+ function finishReport(workspaceRoot, results, cancelled, startedAtMs, now) {
338
+ return {
339
+ workspaceRoot,
340
+ results,
341
+ overallStatus: overallStatus(results, cancelled),
342
+ startedAtMs,
343
+ durationMs: now() - startedAtMs,
344
+ counts: countByStatus(results),
345
+ };
346
+ }
347
+ function rootMismatchReport(plan, workspaceRoot, startedAtMs, now) {
348
+ const results = plan.steps.map((step) => deniedResult(step, "verification plan rejected: workspace root mismatch"));
349
+ return finishReport(workspaceRoot, results, false, startedAtMs, now);
350
+ }
351
+ function preExecutionResult(step, cancelled, signal) {
352
+ if (!isValidVerificationStep(step)) {
353
+ return {
354
+ result: deniedResult(step, "verification plan rejected: unsupported step shape"),
355
+ cancelled,
356
+ };
357
+ }
358
+ if (cancelled) {
359
+ return { result: cancelledResult(step), cancelled };
360
+ }
361
+ if (step.skipReason !== undefined) {
362
+ return { result: skippedResult(step), cancelled };
363
+ }
364
+ if (signal?.aborted === true) {
365
+ return { result: cancelledResult(step), cancelled: true };
366
+ }
367
+ return undefined;
368
+ }
369
+ async function runPlanSteps(plan, deps, baseSpawn, monitor) {
370
+ const results = [];
371
+ const mode = deps.networkEnforcement ?? "inherit";
372
+ const available = deps.enforcedNetworkAvailable ?? false;
373
+ let cancelled = false;
374
+ for (const step of plan.steps) {
375
+ const early = preExecutionResult(step, cancelled, deps.signal);
376
+ if (early !== undefined) {
377
+ results.push(early.result);
378
+ cancelled = early.cancelled;
379
+ continue;
380
+ }
381
+ const resolution = resolveStepNetwork(step.limits, mode, available);
382
+ if (resolution.kind === "fail-closed") {
383
+ results.push(deniedResult(step, "network egress isolation required but no enforcing sandbox backend is available on this host; refusing to execute untrusted code"));
384
+ continue;
385
+ }
386
+ const result = toResult(step, await runStep(step, deps, baseSpawn, monitor, resolution.network));
387
+ results.push(result);
388
+ cancelled ||= result.status === "cancelled";
389
+ }
390
+ return { results, cancelled };
391
+ }
392
+ export async function runVerification(plan, deps) {
393
+ const now = deps.now ?? Date.now;
394
+ const startedAtMs = now();
395
+ const workspaceRoot = deps.workspace.root;
396
+ if (plan.workspaceRoot !== workspaceRoot) {
397
+ return rootMismatchReport(plan, workspaceRoot, startedAtMs, now);
398
+ }
399
+ const { results, cancelled } = await runPlanSteps(plan, deps, deps.spawn ?? nodeSpawnFn, deps.monitor ?? nodeResourceMonitor);
400
+ return finishReport(workspaceRoot, results, cancelled, startedAtMs, now);
401
+ }
package/dist/plan.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ import { type WorkspaceFs, type WorkspaceInfo } from "@oscharko-dev/keiko-workspace";
2
+ import { type ScriptCatalog, type VerificationKind, type VerificationPlan, type VerificationResourceLimits, type VerificationStep } from "./types.js";
3
+ export interface PlanOptions {
4
+ readonly only?: readonly VerificationKind[] | undefined;
5
+ readonly limits?: Partial<VerificationResourceLimits> | undefined;
6
+ readonly changedFiles?: readonly string[] | undefined;
7
+ }
8
+ export declare function resolveTargetedTests(workspace: WorkspaceInfo, changedFiles: readonly string[], fs?: WorkspaceFs, limits?: VerificationResourceLimits): readonly VerificationStep[];
9
+ export declare function planDirectTargetedTests(workspace: WorkspaceInfo, testFiles: readonly string[], fs?: WorkspaceFs, limits?: VerificationResourceLimits): readonly VerificationStep[];
10
+ export declare function buildVerificationPlan(workspace: WorkspaceInfo, catalog: ScriptCatalog, options?: PlanOptions, fs?: WorkspaceFs): VerificationPlan;
11
+ //# sourceMappingURL=plan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plan.d.ts","sourceRoot":"","sources":["../src/plan.ts"],"names":[],"mappings":"AAMA,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,aAAa,EACnB,MAAM,+BAA+B,CAAC;AAEvC,OAAO,EAEL,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,0BAA0B,EAC/B,KAAK,gBAAgB,EACtB,MAAM,YAAY,CAAC;AAUpB,MAAM,WAAW,WAAW;IAE1B,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,gBAAgB,EAAE,GAAG,SAAS,CAAC;IAExD,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,0BAA0B,CAAC,GAAG,SAAS,CAAC;IAElE,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;CACvD;AA6FD,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,aAAa,EACxB,YAAY,EAAE,SAAS,MAAM,EAAE,EAC/B,EAAE,GAAE,WAA6B,EACjC,MAAM,GAAE,0BAAwD,GAC/D,SAAS,gBAAgB,EAAE,CAyB7B;AAQD,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,aAAa,EACxB,SAAS,EAAE,SAAS,MAAM,EAAE,EAC5B,EAAE,GAAE,WAA6B,EACjC,MAAM,GAAE,0BAAwD,GAC/D,SAAS,gBAAgB,EAAE,CAuB7B;AAED,wBAAgB,qBAAqB,CACnC,SAAS,EAAE,aAAa,EACxB,OAAO,EAAE,aAAa,EACtB,OAAO,GAAE,WAAgB,EACzB,EAAE,GAAE,WAA6B,GAChC,gBAAgB,CAYlB"}
package/dist/plan.js ADDED
@@ -0,0 +1,155 @@
1
+ // Plan construction: turns a detected ScriptCatalog into an ordered VerificationPlan, and resolves
2
+ // targeted-test steps from a changed-file set. All path handling goes through the workspace
3
+ // boundary (resolveWithinWorkspace + WorkspaceFs.exists); no raw node:fs. Candidate test-path
4
+ // derivation uses plain string ops (no regex), so there is no ReDoS surface.
5
+ import { basename, dirname, extname, join, relative } from "node:path";
6
+ import { resolveWithinWorkspace, } from "@oscharko-dev/keiko-workspace";
7
+ import { nodeWorkspaceFs } from "@oscharko-dev/keiko-workspace/internal/fs";
8
+ import { DEFAULT_VERIFICATION_LIMITS, } from "./types.js";
9
+ // The script-backed kinds, in run order. `targeted-test` is synthesised, not script-backed.
10
+ const SCRIPT_KINDS = [
11
+ "typecheck",
12
+ "lint",
13
+ "test",
14
+ "build",
15
+ ];
16
+ function resolveLimits(overrides) {
17
+ return { ...DEFAULT_VERIFICATION_LIMITS, ...overrides };
18
+ }
19
+ function scriptStep(kind, scriptName, limits) {
20
+ if (scriptName === undefined) {
21
+ return {
22
+ kind,
23
+ scriptName: undefined,
24
+ command: "npm",
25
+ args: ["run", kind],
26
+ limits,
27
+ skipReason: `no ${kind} script detected in package.json`,
28
+ };
29
+ }
30
+ // `npm test` has a dedicated subcommand; every other kind runs via `npm run <script>`.
31
+ const args = scriptName === "test" ? ["test"] : ["run", scriptName];
32
+ return { kind, scriptName, command: "npm", args, limits };
33
+ }
34
+ function wants(only, kind) {
35
+ return only === undefined || only.includes(kind);
36
+ }
37
+ // Derives candidate test paths for a changed source file: a sibling `X.test.ts`/`X.spec.ts`
38
+ // (and .tsx/.js/.jsx variants), and the same basename mirrored under each configured testDir.
39
+ function candidateTestPaths(workspace, file) {
40
+ const ext = extname(file);
41
+ if (ext === "") {
42
+ return [];
43
+ }
44
+ const dir = dirname(file);
45
+ const stem = basename(file, ext);
46
+ const suffixes = [".test", ".spec"];
47
+ const siblings = suffixes.map((s) => join(dir, `${stem}${s}${ext}`));
48
+ const sourceSubdir = sourceRelativeDir(workspace, file);
49
+ const mirrored = workspace.testDirs.flatMap((testDir) => suffixes.map((s) => join(testDir, sourceSubdir, `${stem}${s}${ext}`)));
50
+ return [...siblings, ...mirrored];
51
+ }
52
+ function sourceRelativeDir(workspace, file) {
53
+ const dir = dirname(file);
54
+ for (const sourceDir of workspace.sourceDirs) {
55
+ const rel = relative(sourceDir, dir);
56
+ if (rel === "") {
57
+ return "";
58
+ }
59
+ if (!rel.startsWith("..") && !rel.startsWith("/") && !rel.includes(":")) {
60
+ return rel;
61
+ }
62
+ }
63
+ return dir === "." ? "" : dir;
64
+ }
65
+ function existsInWorkspace(workspace, fs, relPath) {
66
+ try {
67
+ const abs = resolveWithinWorkspace(workspace.root, relPath);
68
+ return fs.exists(abs);
69
+ }
70
+ catch {
71
+ // A path that escapes the workspace is simply not a resolvable target; skip it.
72
+ return false;
73
+ }
74
+ }
75
+ // Builds the framework-appropriate invocation that runs ONLY the given test files. vitest and jest
76
+ // both accept positional file paths. Returns undefined for an unknown framework, so no targeted
77
+ // step is added rather than guessing. The `npx <runner> ...` shape passes the #6 allowlist.
78
+ function targetedInvocation(workspace, files) {
79
+ if (workspace.testFramework === "vitest") {
80
+ return { command: "npx", args: ["vitest", "run", ...files] };
81
+ }
82
+ if (workspace.testFramework === "jest") {
83
+ return { command: "npx", args: ["jest", ...files] };
84
+ }
85
+ return undefined;
86
+ }
87
+ export function resolveTargetedTests(workspace, changedFiles, fs = nodeWorkspaceFs, limits = DEFAULT_VERIFICATION_LIMITS) {
88
+ const resolved = [];
89
+ for (const file of changedFiles) {
90
+ for (const candidate of candidateTestPaths(workspace, file)) {
91
+ if (existsInWorkspace(workspace, fs, candidate) && !resolved.includes(candidate)) {
92
+ resolved.push(candidate);
93
+ }
94
+ }
95
+ }
96
+ if (resolved.length === 0) {
97
+ return [];
98
+ }
99
+ const invocation = targetedInvocation(workspace, resolved);
100
+ if (invocation === undefined) {
101
+ return [];
102
+ }
103
+ return [
104
+ {
105
+ kind: "targeted-test",
106
+ scriptName: undefined,
107
+ command: invocation.command,
108
+ args: invocation.args,
109
+ limits,
110
+ },
111
+ ];
112
+ }
113
+ // Builds a single targeted-test step that runs EXACTLY the given test files (Issue #1204 post-apply
114
+ // verification), rather than deriving sibling test paths from changed source files. Each path is
115
+ // validated to be a workspace-contained, existing file before it is included; an unknown test framework
116
+ // or an empty resolved set yields no step (the caller then reports verification as skipped/not-run).
117
+ // Used to re-confirm a just-applied generated test in place, isolated by the orchestrator's enforced
118
+ // network policy.
119
+ export function planDirectTargetedTests(workspace, testFiles, fs = nodeWorkspaceFs, limits = DEFAULT_VERIFICATION_LIMITS) {
120
+ const resolved = [];
121
+ for (const file of testFiles) {
122
+ if (existsInWorkspace(workspace, fs, file) && !resolved.includes(file)) {
123
+ resolved.push(file);
124
+ }
125
+ }
126
+ if (resolved.length === 0) {
127
+ return [];
128
+ }
129
+ const invocation = targetedInvocation(workspace, resolved);
130
+ if (invocation === undefined) {
131
+ return [];
132
+ }
133
+ return [
134
+ {
135
+ kind: "targeted-test",
136
+ scriptName: undefined,
137
+ command: invocation.command,
138
+ args: invocation.args,
139
+ limits,
140
+ },
141
+ ];
142
+ }
143
+ export function buildVerificationPlan(workspace, catalog, options = {}, fs = nodeWorkspaceFs) {
144
+ const limits = resolveLimits(options.limits);
145
+ const steps = [];
146
+ for (const kind of SCRIPT_KINDS) {
147
+ if (wants(options.only, kind)) {
148
+ steps.push(scriptStep(kind, catalog.mapping[kind], limits));
149
+ }
150
+ }
151
+ if (wants(options.only, "targeted-test") && options.changedFiles !== undefined) {
152
+ steps.push(...resolveTargetedTests(workspace, options.changedFiles, fs, limits));
153
+ }
154
+ return { workspaceRoot: workspace.root, steps };
155
+ }
@@ -0,0 +1,7 @@
1
+ import type { VerificationResultSummary, VerificationSummary, AuditResultEntry, VerificationAuditSummary } from "@oscharko-dev/keiko-contracts";
2
+ export type { VerificationResultSummary, VerificationSummary, AuditResultEntry, VerificationAuditSummary, };
3
+ import type { VerificationReport } from "./types.js";
4
+ export declare function buildVerificationSummary(report: VerificationReport): VerificationSummary;
5
+ export declare function summarizeForAudit(report: VerificationReport): VerificationAuditSummary;
6
+ export declare function renderMarkdownSummary(report: VerificationReport): string;
7
+ //# sourceMappingURL=summary.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"summary.d.ts","sourceRoot":"","sources":["../src/summary.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EACV,yBAAyB,EACzB,mBAAmB,EACnB,gBAAgB,EAChB,wBAAwB,EACzB,MAAM,+BAA+B,CAAC;AACvC,YAAY,EACV,yBAAyB,EACzB,mBAAmB,EACnB,gBAAgB,EAChB,wBAAwB,GACzB,CAAC;AAGF,OAAO,KAAK,EAAE,kBAAkB,EAA0C,MAAM,YAAY,CAAC;AAE7F,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,kBAAkB,GAAG,mBAAmB,CAoBxF;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,kBAAkB,GAAG,wBAAwB,CAiBtF;AAoBD,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,CAMxE"}
@@ -0,0 +1,67 @@
1
+ // The render surfaces over a VerificationReport (ADR-0007). buildVerificationSummary is the
2
+ // structured CLI/SDK view (keeps the redacted output digest); summarizeForAudit is the audit-ledger
3
+ // projection that EXCLUDES raw output text (mirroring ADR-0005 audit excerpt-exclusion), keeping
4
+ // only status/exit/duration/appliedLimits/counts; renderMarkdownSummary is a PR/issue table. Every
5
+ // composed string is run through redact() so nothing a summary emits can leak a secret. Pure — no IO.
6
+ import { redact } from "@oscharko-dev/keiko-security";
7
+ export function buildVerificationSummary(report) {
8
+ return {
9
+ workspaceRoot: report.workspaceRoot,
10
+ overallStatus: report.overallStatus,
11
+ durationMs: report.durationMs,
12
+ counts: report.counts,
13
+ results: report.results.map((r) => ({
14
+ kind: r.kind,
15
+ scriptName: r.scriptName,
16
+ command: redact(`${r.command} ${r.args.join(" ")}`.trim()),
17
+ status: r.status,
18
+ exitCode: r.exitCode,
19
+ durationMs: r.durationMs,
20
+ truncated: r.truncated,
21
+ // r.outputSummary is already redacted at the orchestrator; re-redact as defence in depth.
22
+ outputSummary: redact(r.outputSummary),
23
+ appliedLimits: r.appliedLimits,
24
+ detail: r.detail === undefined ? undefined : redact(r.detail),
25
+ })),
26
+ };
27
+ }
28
+ export function summarizeForAudit(report) {
29
+ return {
30
+ workspaceRoot: report.workspaceRoot,
31
+ overallStatus: report.overallStatus,
32
+ durationMs: report.durationMs,
33
+ counts: report.counts,
34
+ results: report.results.map((r) => ({
35
+ kind: r.kind,
36
+ scriptName: r.scriptName,
37
+ command: redact(`${r.command} ${r.args.join(" ")}`.trim()),
38
+ status: r.status,
39
+ exitCode: r.exitCode,
40
+ durationMs: r.durationMs,
41
+ truncated: r.truncated,
42
+ appliedLimits: r.appliedLimits,
43
+ })),
44
+ };
45
+ }
46
+ function statusMark(status) {
47
+ if (status === "passed") {
48
+ return "pass";
49
+ }
50
+ if (status === "skipped") {
51
+ return "skip";
52
+ }
53
+ return "FAIL";
54
+ }
55
+ function markdownRow(result) {
56
+ const cmd = redact(`${result.command} ${result.args.join(" ")}`.trim());
57
+ const detail = result.detail === undefined ? "" : redact(result.detail);
58
+ const exit = result.exitCode === null ? "—" : String(result.exitCode);
59
+ return `| ${result.kind} | ${result.status} | ${exit} | ${String(result.durationMs)} | \`${cmd}\` | ${detail} |`;
60
+ }
61
+ // A PR/issue Markdown table. Every cell that can carry command-derived text is redacted.
62
+ export function renderMarkdownSummary(report) {
63
+ const header = `### Verification: ${statusMark(report.overallStatus)} (${report.overallStatus})`;
64
+ const tableHead = "| Kind | Status | Exit | ms | Command | Detail |\n| --- | --- | --- | --- | --- | --- |";
65
+ const rows = report.results.map(markdownRow).join("\n");
66
+ return `${header}\n\n${tableHead}\n${rows}\n`;
67
+ }
@@ -0,0 +1,3 @@
1
+ export type { VerificationKind, VerificationStatus, ResourceDimension, ResourceLimitDecision, VerificationResourceLimits, VerificationStep, VerificationPlan, VerificationResult, VerificationReport, ScriptCatalog, ScriptMapping, } from "@oscharko-dev/keiko-contracts";
2
+ export { DEFAULT_VERIFICATION_LIMITS } from "@oscharko-dev/keiko-contracts";
3
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAIA,YAAY,EACV,gBAAgB,EAChB,kBAAkB,EAClB,iBAAiB,EACjB,qBAAqB,EACrB,0BAA0B,EAC1B,gBAAgB,EAChB,gBAAgB,EAChB,kBAAkB,EAClB,kBAAkB,EAClB,aAAa,EACb,aAAa,GACd,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,2BAA2B,EAAE,MAAM,+BAA+B,CAAC"}
package/dist/types.js ADDED
@@ -0,0 +1,4 @@
1
+ // Re-export shim: verification contract types and the frozen DEFAULT_VERIFICATION_LIMITS table
2
+ // live in @oscharko-dev/keiko-contracts (issue #158). `verbatimModuleSyntax` is on, so type-only
3
+ // names use `export type` and value-emitting frozen tables use `export`.
4
+ export { DEFAULT_VERIFICATION_LIMITS } from "@oscharko-dev/keiko-contracts";