@sverka/sdk 0.0.0 → 0.1.2

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/dist/index.mjs ADDED
@@ -0,0 +1,2216 @@
1
+ import { existsSync, mkdtempSync, readFileSync, realpathSync } from "node:fs";
2
+ import path, { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { spawn } from "node:child_process";
5
+ import { AgentStep, ComponentStep, CompositionError, CoreError, Pipeline, PipelineCallStep, PlanningError, ShellStep, canonicalStringify, computeGraphId, computePlanId, computePlanId as computePlanId$1, computeRunPlanId, expandPipelineCalls, matrix, parallel, pipeline, run, validateGraph, validatePlan, validatePlan as validatePlan$1, when, workflow, workflow as workflow$1 } from "@sverka/workflow";
6
+ import { CheckError, DEFAULT_POLICY, DEFAULT_POLICY as DEFAULT_POLICY$1, computeFingerprint, createBaseline, createBuiltinResolver, createBuiltinResolver as createBuiltinResolver$1, createPolicy, createPolicy as createPolicy$1, evaluatePolicy, evaluatePolicy as evaluatePolicy$1, extractFindings, extractFindings as extractFindings$1, filterOnlyNew, filterOnlyNew as filterOnlyNew$1, loadBaseline, loadBaseline as loadBaseline$1, normalizeSarif, saveBaseline, updateBaseline } from "@sverka/verification";
7
+ import { pathToFileURL } from "node:url";
8
+ import { rm } from "node:fs/promises";
9
+ import { tmpdir } from "node:os";
10
+ //#region src/planner/internal/git-cli.ts
11
+ /**
12
+ * Resolve the absolute path to the `git` executable by searching `PATH`
13
+ * at this trusted parent level. The spawned child receives a minimal
14
+ * environment without `PATH`, mitigating PATH-injection hotspots.
15
+ */
16
+ function resolveGitBinary() {
17
+ const pathEnv = process.env.PATH ?? "";
18
+ for (const dir of pathEnv.split(path.delimiter)) {
19
+ if (!dir) continue;
20
+ const candidate = path.join(dir, "git");
21
+ if (existsSync(candidate)) return candidate;
22
+ }
23
+ return "git";
24
+ }
25
+ const GIT_BINARY = resolveGitBinary();
26
+ /**
27
+ * Default `GitCli` implementation backed by `node:child_process.spawn`.
28
+ */
29
+ function createGitCli() {
30
+ return { run(args, cwd) {
31
+ return new Promise((resolvePromise, reject) => {
32
+ const env = {
33
+ HOME: process.env.HOME,
34
+ GIT_TERMINAL_PROMPT: "0"
35
+ };
36
+ const child = spawn(GIT_BINARY, [...args], {
37
+ cwd,
38
+ env,
39
+ stdio: [
40
+ "ignore",
41
+ "pipe",
42
+ "pipe"
43
+ ]
44
+ });
45
+ let stdout = "";
46
+ let stderr = "";
47
+ child.stdout?.on("data", (data) => {
48
+ stdout += data.toString();
49
+ });
50
+ child.stderr?.on("data", (data) => {
51
+ stderr += data.toString();
52
+ });
53
+ child.on("error", (err) => {
54
+ reject(err);
55
+ });
56
+ child.on("close", (code) => {
57
+ if (code === 0) resolvePromise(stdout);
58
+ else reject(new Error(`git ${args.join(" ")} exited ${code ?? -1}`, stderr.trim() ? { cause: stderr.trim() } : void 0));
59
+ });
60
+ });
61
+ } };
62
+ }
63
+ //#endregion
64
+ //#region src/planner/detect.ts
65
+ const MANIFEST_FILES = /* @__PURE__ */ new Set([
66
+ "package.json",
67
+ "pyproject.toml",
68
+ "Cargo.toml",
69
+ "go.mod",
70
+ "pom.xml",
71
+ "build.gradle",
72
+ "composer.json"
73
+ ]);
74
+ const LOCKFILE_MAP = {
75
+ "bun.lock": "bun",
76
+ "bun.lockb": "bun",
77
+ "package-lock.json": "npm",
78
+ "yarn.lock": "yarn",
79
+ "pnpm-lock.yaml": "pnpm",
80
+ "poetry.lock": "poetry",
81
+ "Cargo.lock": "cargo",
82
+ "go.sum": "go"
83
+ };
84
+ const MONOREPO_MARKER_FILES = {
85
+ "nx.json": "nx",
86
+ "turbo.json": "turborepo",
87
+ "lerna.json": "lerna",
88
+ "pnpm-workspace.yaml": "pnpm-workspace"
89
+ };
90
+ /**
91
+ * Detect local signals from a file list. Pure: no I/O.
92
+ */
93
+ function detectSignals(files) {
94
+ const signals = [];
95
+ for (const file of files) {
96
+ const base = basename$1(file);
97
+ if (MANIFEST_FILES.has(base)) {
98
+ signals.push({
99
+ type: "manifest",
100
+ path: file,
101
+ detail: null,
102
+ confidence: 1
103
+ });
104
+ continue;
105
+ }
106
+ if (Object.hasOwn(LOCKFILE_MAP, base)) {
107
+ signals.push({
108
+ type: "lockfile",
109
+ path: file,
110
+ detail: null,
111
+ confidence: 1
112
+ });
113
+ continue;
114
+ }
115
+ if (base === "Dockerfile" || base.endsWith(".Dockerfile")) {
116
+ signals.push({
117
+ type: "dockerfile",
118
+ path: file,
119
+ detail: null,
120
+ confidence: 1
121
+ });
122
+ continue;
123
+ }
124
+ if (base === "docker-compose.yml" || base === "docker-compose.yaml") {
125
+ signals.push({
126
+ type: "docker-compose",
127
+ path: file,
128
+ detail: null,
129
+ confidence: 1
130
+ });
131
+ continue;
132
+ }
133
+ if (isCiDefinition(file, base)) {
134
+ signals.push({
135
+ type: "ci-definition",
136
+ path: file,
137
+ detail: null,
138
+ confidence: 1
139
+ });
140
+ continue;
141
+ }
142
+ if (Object.hasOwn(MONOREPO_MARKER_FILES, base)) signals.push({
143
+ type: "monorepo-marker",
144
+ path: file,
145
+ detail: null,
146
+ confidence: 1
147
+ });
148
+ }
149
+ return signals;
150
+ }
151
+ function isCiDefinition(file, base) {
152
+ if (base === ".gitlab-ci.yml" || base === "azure-pipelines.yml" || base === "Jenkinsfile") return true;
153
+ if (file.startsWith(".github/workflows/") && (base.endsWith(".yml") || base.endsWith(".yaml"))) return true;
154
+ if (file.startsWith(".circleci/")) return true;
155
+ return false;
156
+ }
157
+ const EXTENSION_MAP = {
158
+ ".ts": "TypeScript",
159
+ ".tsx": "TypeScript",
160
+ ".js": "JavaScript",
161
+ ".jsx": "JavaScript",
162
+ ".mjs": "JavaScript",
163
+ ".cjs": "JavaScript",
164
+ ".py": "Python",
165
+ ".rs": "Rust",
166
+ ".go": "Go",
167
+ ".java": "Java",
168
+ ".kt": "Kotlin",
169
+ ".rb": "Ruby",
170
+ ".php": "PHP",
171
+ ".cs": "C#",
172
+ ".cpp": "C++",
173
+ ".cc": "C++",
174
+ ".cxx": "C++",
175
+ ".c": "C",
176
+ ".h": "C",
177
+ ".hpp": "C++",
178
+ ".swift": "Swift",
179
+ ".scala": "Scala",
180
+ ".sh": "Shell",
181
+ ".bash": "Shell"
182
+ };
183
+ function detectLanguages(files) {
184
+ const counts = /* @__PURE__ */ new Map();
185
+ for (const file of files) {
186
+ const ext = extname(file);
187
+ const lang = EXTENSION_MAP[ext];
188
+ if (!lang) continue;
189
+ const entry = counts.get(lang);
190
+ if (entry) entry.push(ext);
191
+ else counts.set(lang, [ext]);
192
+ }
193
+ const out = [];
194
+ for (const [name, exts] of counts) {
195
+ const fileCount = exts.length;
196
+ const confidence = Math.min(1, fileCount / 10);
197
+ out.push({
198
+ name,
199
+ confidence,
200
+ evidence: [...new Set(exts)],
201
+ fileCount
202
+ });
203
+ }
204
+ out.sort((a, b) => b.fileCount - a.fileCount || a.name.localeCompare(b.name));
205
+ return out;
206
+ }
207
+ const MANIFEST_PACKAGE_MANAGER = {
208
+ "go.mod": "go",
209
+ "Cargo.toml": "cargo"
210
+ };
211
+ function detectPackageManagers(signals, rootPkgJson) {
212
+ const byName = /* @__PURE__ */ new Map();
213
+ collectLockfilePackageManagers(signals, byName);
214
+ collectManifestPackageManagers(signals, byName);
215
+ applyPackageManagerField(rootPkgJson, byName);
216
+ return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
217
+ }
218
+ /** Populate `byName` from lockfile signals. */
219
+ function collectLockfilePackageManagers(signals, byName) {
220
+ for (const sig of signals) {
221
+ if (sig.type !== "lockfile") continue;
222
+ const base = basename$1(sig.path);
223
+ const name = LOCKFILE_MAP[base];
224
+ if (!name) {
225
+ const other = byName.get("other");
226
+ if (other) other.evidence.push(sig.path);
227
+ else byName.set("other", {
228
+ name: "other",
229
+ version: null,
230
+ lockfile: sig.path,
231
+ evidence: [sig.path]
232
+ });
233
+ continue;
234
+ }
235
+ const existing = byName.get(name);
236
+ if (existing) {
237
+ if (!existing.evidence.includes(sig.path)) existing.evidence.push(sig.path);
238
+ } else byName.set(name, {
239
+ name,
240
+ version: null,
241
+ lockfile: sig.path,
242
+ evidence: [sig.path]
243
+ });
244
+ }
245
+ }
246
+ /** Populate `byName` from manifest signals for ecosystems without a lockfile or when the manifest is the primary signal. */
247
+ function collectManifestPackageManagers(signals, byName) {
248
+ for (const sig of signals) {
249
+ if (sig.type !== "manifest") continue;
250
+ const base = basename$1(sig.path);
251
+ const name = MANIFEST_PACKAGE_MANAGER[base];
252
+ if (!name || byName.has(name)) continue;
253
+ byName.set(name, {
254
+ name,
255
+ version: null,
256
+ lockfile: null,
257
+ evidence: [sig.path]
258
+ });
259
+ }
260
+ }
261
+ /** Apply the `packageManager` field override from the root package.json. */
262
+ function applyPackageManagerField(rootPkgJson, byName) {
263
+ if (!rootPkgJson) return;
264
+ const pm = rootPkgJson["packageManager"];
265
+ if (typeof pm !== "string") return;
266
+ const { tool, version } = parsePackageManager(pm);
267
+ if (!tool) return;
268
+ const existing = byName.get(tool);
269
+ if (existing) {
270
+ existing.version = version;
271
+ if (!existing.evidence.includes("package.json#packageManager")) existing.evidence.push("package.json#packageManager");
272
+ } else byName.set(tool, {
273
+ name: tool,
274
+ version,
275
+ lockfile: null,
276
+ evidence: ["package.json#packageManager"]
277
+ });
278
+ }
279
+ function parsePackageManager(pm) {
280
+ const atIdx = pm.indexOf("@");
281
+ if (atIdx <= 0) return {
282
+ tool: null,
283
+ version: null
284
+ };
285
+ const toolName = pm.slice(0, atIdx);
286
+ const version = pm.slice(atIdx + 1) || null;
287
+ if (![
288
+ "npm",
289
+ "yarn",
290
+ "pnpm",
291
+ "bun",
292
+ "pip",
293
+ "poetry",
294
+ "uv",
295
+ "pipenv",
296
+ "cargo",
297
+ "go",
298
+ "maven",
299
+ "gradle",
300
+ "composer",
301
+ "other"
302
+ ].includes(toolName)) return {
303
+ tool: null,
304
+ version
305
+ };
306
+ return {
307
+ tool: toolName,
308
+ version
309
+ };
310
+ }
311
+ function detectMonorepo(signals, rootPkgJson) {
312
+ const globs = rootPkgJson ? extractWorkspaceGlobs(rootPkgJson["workspaces"]) : [];
313
+ const workspaces = resolveWorkspaceDirs(signals, globs);
314
+ for (const sig of signals) {
315
+ if (sig.type !== "monorepo-marker") continue;
316
+ const base = basename$1(sig.path);
317
+ const tool = MONOREPO_MARKER_FILES[base];
318
+ if (tool) return {
319
+ tool,
320
+ workspaces,
321
+ evidence: globs.length > 0 ? [sig.path, "package.json#workspaces"] : [sig.path]
322
+ };
323
+ }
324
+ if (globs.length > 0 && rootPkgJson) return {
325
+ tool: isBunWorkspace(rootPkgJson) ? "bun-workspace" : "custom",
326
+ workspaces,
327
+ evidence: ["package.json#workspaces"]
328
+ };
329
+ return null;
330
+ }
331
+ /**
332
+ * Resolve workspace globs to concrete workspace dirs using manifest signals.
333
+ * A dir is a workspace when `<dir>/package.json` appears as a manifest and
334
+ * `<dir>` matches a workspace glob (`*` = one path segment, `**` = any depth).
335
+ */
336
+ function resolveWorkspaceDirs(signals, globs) {
337
+ const positive = globs.filter((g) => !g.startsWith("!"));
338
+ const negative = globs.filter((g) => g.startsWith("!")).map((g) => g.slice(1));
339
+ const dirs = /* @__PURE__ */ new Set();
340
+ for (const sig of signals) {
341
+ if (sig.type !== "manifest" || !sig.path.endsWith("/package.json")) continue;
342
+ const dir = sig.path.slice(0, sig.path.length - 13);
343
+ if (positive.some((g) => matchesWorkspaceGlob(dir, g)) && !negative.some((g) => matchesWorkspaceGlob(dir, g))) dirs.add(dir);
344
+ }
345
+ return [...dirs].sort((a, b) => a.localeCompare(b));
346
+ }
347
+ function matchesWorkspaceGlob(dir, glob) {
348
+ const d = dir.split("/");
349
+ const g = glob.split("/");
350
+ const memo = /* @__PURE__ */ new Map();
351
+ const match = (di, gi) => {
352
+ const key = `${di}:${gi}`;
353
+ const cached = memo.get(key);
354
+ if (cached !== void 0) return cached;
355
+ let result;
356
+ if (gi === g.length) result = di === d.length;
357
+ else if (g[gi] === "**") {
358
+ result = false;
359
+ for (let k = di; k <= d.length && !result; k++) result = match(k, gi + 1);
360
+ } else result = di < d.length && matchesGlobSegment(d[di], g[gi]) && match(di + 1, gi + 1);
361
+ memo.set(key, result);
362
+ return result;
363
+ };
364
+ return match(0, 0);
365
+ }
366
+ function matchesGlobSegment(segment, pattern) {
367
+ let s = 0;
368
+ let p = 0;
369
+ let starP = -1;
370
+ let starS = -1;
371
+ while (s < segment.length) if (p < pattern.length && (pattern[p] === "?" || pattern[p] === segment[s])) {
372
+ s++;
373
+ p++;
374
+ } else if (p < pattern.length && pattern[p] === "*") {
375
+ starP = p++;
376
+ starS = s;
377
+ } else if (starP !== -1) {
378
+ p = starP + 1;
379
+ s = ++starS;
380
+ } else return false;
381
+ while (p < pattern.length && pattern[p] === "*") p++;
382
+ return p === pattern.length;
383
+ }
384
+ function extractWorkspaceGlobs(ws) {
385
+ if (Array.isArray(ws)) return ws.filter((g) => typeof g === "string");
386
+ if (typeof ws === "object" && ws !== null) {
387
+ const packages = ws["packages"];
388
+ if (Array.isArray(packages)) return packages.filter((g) => typeof g === "string");
389
+ }
390
+ return [];
391
+ }
392
+ function isBunWorkspace(pkgJson) {
393
+ const pm = pkgJson["packageManager"];
394
+ return typeof pm === "string" && pm.startsWith("bun");
395
+ }
396
+ function basename$1(path) {
397
+ const idx = path.lastIndexOf("/");
398
+ return idx >= 0 ? path.slice(idx + 1) : path;
399
+ }
400
+ function extname(path) {
401
+ const base = basename$1(path);
402
+ const idx = base.lastIndexOf(".");
403
+ return idx >= 0 ? base.slice(idx) : "";
404
+ }
405
+ //#endregion
406
+ //#region src/planner/explain.ts
407
+ const SIGNAL_TYPES = [
408
+ "manifest",
409
+ "lockfile",
410
+ "dockerfile",
411
+ "docker-compose",
412
+ "ci-definition",
413
+ "monorepo-marker",
414
+ "git-metadata"
415
+ ];
416
+ /**
417
+ * Build a `DiscoveryExplanation` from the collected signals. Pure.
418
+ */
419
+ function buildExplanation(signals, opts) {
420
+ const counts = {};
421
+ for (const t of SIGNAL_TYPES) counts[t] = 0;
422
+ for (const sig of signals) counts[sig.type] = (counts[sig.type] ?? 0) + 1;
423
+ const parts = [];
424
+ const total = signals.length;
425
+ parts.push(`${total} signal${total === 1 ? "" : "s"}`);
426
+ const nonzero = SIGNAL_TYPES.filter((t) => counts[t] > 0);
427
+ if (nonzero.length) parts.push(`(${formatSignalCounts(nonzero, counts)})`);
428
+ if (opts.dirty) parts.push("working tree dirty");
429
+ else parts.push("working tree clean");
430
+ if (!opts.hasBaseRef) parts.push("no baseRef provided");
431
+ return {
432
+ summary: parts.join(" "),
433
+ signalCounts: counts
434
+ };
435
+ }
436
+ /** Format non-zero signal counts as "type=count, type=count". */
437
+ function formatSignalCounts(types, counts) {
438
+ return types.map((t) => t + "=" + counts[t]).join(", ");
439
+ }
440
+ //#endregion
441
+ //#region src/planner/errors.ts
442
+ /**
443
+ * Base error class for the planner package. All discovery failures throw a
444
+ * `DiscoveryError` with one of the four `DiscoveryErrorCode` values.
445
+ */
446
+ var DiscoveryError = class extends Error {
447
+ code;
448
+ cause;
449
+ constructor(message, code, cause) {
450
+ super(message);
451
+ this.name = "DiscoveryError";
452
+ this.code = code;
453
+ this.cause = cause;
454
+ }
455
+ };
456
+ /**
457
+ * Planner error for Run Plan binding failures. Spec 13 — Error handling.
458
+ */
459
+ var PlannerError = class extends Error {
460
+ code;
461
+ cause;
462
+ constructor(message, code, cause) {
463
+ super(message);
464
+ this.name = "PlannerError";
465
+ this.code = code;
466
+ this.cause = cause;
467
+ }
468
+ };
469
+ //#endregion
470
+ //#region src/planner/bind.ts
471
+ const INPUT_TYPES = /* @__PURE__ */ new Set([
472
+ "string",
473
+ "number",
474
+ "boolean"
475
+ ]);
476
+ /**
477
+ * Bind a Definition Graph's Entry + user inputs into a concrete RunPlan
478
+ * that the native engine can execute.
479
+ */
480
+ function bindRunPlan(options) {
481
+ const { graph, entryId, inputs } = options;
482
+ validateGraphShape(graph);
483
+ if (graph.project.pipelines.length === 0) throw new PlannerError("graph has no pipelines", "INVALID_GRAPH");
484
+ const { pipeline, entry } = findEntry(graph, entryId);
485
+ validateRoots(pipeline, entry);
486
+ validatePipeline(graph, pipeline);
487
+ const reachableSteps = computeReachableSteps(pipeline.steps, entry.roots);
488
+ const expandedSteps = expandPipelineCalls(graph, reachableSteps);
489
+ const boundInputs = bindInputs(pipeline.inputs, inputs);
490
+ const graphId = computeGraphId(graph);
491
+ const boundEntry = {
492
+ id: entry.id,
493
+ trigger: entry.trigger
494
+ };
495
+ return {
496
+ apiVersion: "sverka.dev/v1run",
497
+ id: computeRunPlanId({
498
+ apiVersion: "sverka.dev/v1run",
499
+ graphId,
500
+ entry: boundEntry,
501
+ inputs: boundInputs,
502
+ steps: expandedSteps
503
+ }),
504
+ graphId,
505
+ entry: boundEntry,
506
+ inputs: boundInputs,
507
+ steps: expandedSteps,
508
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
509
+ };
510
+ }
511
+ /**
512
+ * Validate that the runtime value has the shape of a DefinitionGraph before
513
+ * any field is dereferenced. Malformed values become PlannerError(INVALID_GRAPH)
514
+ * instead of TypeError.
515
+ */
516
+ function validateGraphShape(value) {
517
+ const graph = value;
518
+ if (typeof graph !== "object" || graph === null) throw new PlannerError("graph must be an object", "INVALID_GRAPH");
519
+ const project = graph.project;
520
+ if (typeof project !== "object" || project === null) throw new PlannerError("graph.project must be an object", "INVALID_GRAPH");
521
+ const pipelines = project.pipelines;
522
+ if (!Array.isArray(pipelines)) throw new PlannerError("graph.project.pipelines must be an array", "INVALID_GRAPH");
523
+ for (let i = 0; i < pipelines.length; i++) {
524
+ const pipeline = pipelines[i];
525
+ validatePipelineShape(pipeline, i);
526
+ }
527
+ }
528
+ function validatePipelineShape(value, index) {
529
+ const pipeline = value;
530
+ if (typeof pipeline !== "object" || pipeline === null) throw new PlannerError(`graph.project.pipelines[${index}] must be an object`, "INVALID_GRAPH");
531
+ validatePipelineFields(pipeline, index);
532
+ validatePipelineChildren(pipeline, index);
533
+ }
534
+ function validatePipelineFields(pipeline, index) {
535
+ if (typeof pipeline.id !== "string") throw new PlannerError(`graph.project.pipelines[${index}].id must be a string`, "INVALID_GRAPH");
536
+ if (!Array.isArray(pipeline.entries)) throw new PlannerError(`graph.project.pipelines[${index}].entries must be an array`, "INVALID_GRAPH");
537
+ if (!Array.isArray(pipeline.steps)) throw new PlannerError(`graph.project.pipelines[${index}].steps must be an array`, "INVALID_GRAPH");
538
+ if (typeof pipeline.inputs !== "object" || pipeline.inputs === null || Array.isArray(pipeline.inputs)) throw new PlannerError(`graph.project.pipelines[${index}].inputs must be an object`, "INVALID_GRAPH");
539
+ }
540
+ function validatePipelineChildren(pipeline, index) {
541
+ const inputs = pipeline.inputs;
542
+ for (const [name, descriptor] of Object.entries(inputs)) validateInputDescriptor(descriptor, index, name);
543
+ const entries = pipeline.entries;
544
+ if (!Array.isArray(entries)) return;
545
+ const steps = pipeline.steps;
546
+ if (!Array.isArray(steps)) return;
547
+ for (let j = 0; j < entries.length; j++) validateEntryShape(entries[j], index, j);
548
+ for (let j = 0; j < steps.length; j++) validateStepShape(steps[j], index, j);
549
+ }
550
+ function validateInputDescriptor(value, pipelineIndex, name) {
551
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new PlannerError(`graph.project.pipelines[${pipelineIndex}].inputs.${name} must be an object`, "INVALID_GRAPH");
552
+ const type = value.type;
553
+ if (type !== void 0 && !INPUT_TYPES.has(type)) throw new PlannerError(`graph.project.pipelines[${pipelineIndex}].inputs.${name} has invalid type`, "INVALID_GRAPH");
554
+ }
555
+ function validateEntryShape(value, pipelineIndex, index) {
556
+ const entry = value;
557
+ if (typeof entry !== "object" || entry === null) throw new PlannerError(`graph.project.pipelines[${pipelineIndex}].entries[${index}] must be an object`, "INVALID_GRAPH");
558
+ if (typeof entry.id !== "string") throw new PlannerError(`graph.project.pipelines[${pipelineIndex}].entries[${index}].id must be a string`, "INVALID_GRAPH");
559
+ if (!Array.isArray(entry.roots)) throw new PlannerError(`graph.project.pipelines[${pipelineIndex}].entries[${index}].roots must be an array`, "INVALID_GRAPH");
560
+ }
561
+ function validateStepShape(value, pipelineIndex, index) {
562
+ const step = value;
563
+ if (typeof step !== "object" || step === null) throw new PlannerError(`graph.project.pipelines[${pipelineIndex}].steps[${index}] must be an object`, "INVALID_GRAPH");
564
+ if (typeof step.id !== "string") throw new PlannerError(`graph.project.pipelines[${pipelineIndex}].steps[${index}].id must be a string`, "INVALID_GRAPH");
565
+ if (!Array.isArray(step.dependencies)) throw new PlannerError(`graph.project.pipelines[${pipelineIndex}].steps[${index}].dependencies must be an array`, "INVALID_GRAPH");
566
+ }
567
+ /**
568
+ * Compute the transitive closure of reachable steps from the given roots.
569
+ * A step is reachable if it is connected to a root by following
570
+ * `dependencies[].producer` edges backward (i.e. the root's prerequisites).
571
+ * Returns steps in the original graph order, filtered to reachable steps.
572
+ */
573
+ function computeReachableSteps(steps, roots) {
574
+ const producerMap = buildProducerMap(steps);
575
+ const reachable = /* @__PURE__ */ new Set();
576
+ const queue = [];
577
+ for (const id of roots) if (!reachable.has(id)) {
578
+ reachable.add(id);
579
+ queue.push(id);
580
+ }
581
+ let head = 0;
582
+ while (head < queue.length) {
583
+ const id = queue[head];
584
+ head++;
585
+ for (const producer of producerMap.get(id) ?? []) if (!reachable.has(producer)) {
586
+ reachable.add(producer);
587
+ queue.push(producer);
588
+ }
589
+ }
590
+ return steps.filter((s) => reachable.has(s.id));
591
+ }
592
+ /**
593
+ * Build a map from each step id to the list of producer step ids it depends on.
594
+ */
595
+ function buildProducerMap(steps) {
596
+ const map = /* @__PURE__ */ new Map();
597
+ for (const step of steps) {
598
+ const producers = [];
599
+ map.set(step.id, producers);
600
+ for (const dep of step.dependencies) producers.push(dep.producer);
601
+ }
602
+ return map;
603
+ }
604
+ function findEntry(graph, entryId) {
605
+ for (const pipeline of graph.project.pipelines) {
606
+ const entry = pipeline.entries.find((e) => e.id === entryId);
607
+ if (entry) return {
608
+ pipeline,
609
+ entry
610
+ };
611
+ }
612
+ throw new PlannerError(`entry "${entryId}" not found`, "ENTRY_NOT_FOUND");
613
+ }
614
+ function validateRoots(pipeline, entry) {
615
+ const stepIds = new Set(pipeline.steps.map((s) => s.id));
616
+ for (const root of entry.roots) if (!stepIds.has(root)) throw new PlannerError(`entry root "${root}" not found in pipeline steps`, "ROOT_NOT_FOUND");
617
+ }
618
+ function validatePipeline(graph, pipeline) {
619
+ try {
620
+ validateGraph({ project: {
621
+ id: graph.project.id,
622
+ pipelines: [pipeline]
623
+ } });
624
+ } catch (err) {
625
+ throw new PlannerError(err instanceof Error ? err.message : String(err), "INVALID_GRAPH", err);
626
+ }
627
+ }
628
+ function bindInputs(pipelineInputs, userInputs) {
629
+ const result = {};
630
+ for (const [name, input] of Object.entries(pipelineInputs)) {
631
+ const type = input.type;
632
+ validateInputType(name, type);
633
+ if (input.secret) continue;
634
+ const userValue = userInputs?.[name];
635
+ if (userValue !== void 0) {
636
+ assertInputValue(userValue, type, name);
637
+ result[name] = userValue;
638
+ continue;
639
+ }
640
+ if (input.default !== void 0) {
641
+ assertInputValue(input.default, type, name);
642
+ result[name] = input.default;
643
+ continue;
644
+ }
645
+ if (input.required ?? true) throw new PlannerError(`required input "${name}" has no value and no default`, "MISSING_INPUT");
646
+ }
647
+ return result;
648
+ }
649
+ function validateInputType(name, type) {
650
+ if (type === void 0 || !INPUT_TYPES.has(type)) throw new PlannerError(`input "${name}" has no valid declared type`, "INVALID_GRAPH");
651
+ }
652
+ function assertInputValue(value, type, name) {
653
+ if (type === "array") {
654
+ if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) throw new PlannerError(`input "${name}" value ${JSON.stringify(value)} does not match declared type "array" (expected string array)`, "INVALID_INPUT");
655
+ return;
656
+ }
657
+ if (typeof value !== (type === "choice" ? "string" : type)) throw new PlannerError(`input "${name}" value ${JSON.stringify(value)} does not match declared type "${type}"`, "INVALID_INPUT");
658
+ }
659
+ //#endregion
660
+ //#region src/planner/planner.ts
661
+ function createPlanner() {
662
+ return new PlannerImpl();
663
+ }
664
+ const DEFAULT_MAX_DEPTH = 10;
665
+ var PlannerImpl = class {
666
+ git;
667
+ constructor() {
668
+ this.git = createGitCli();
669
+ }
670
+ async discover(options) {
671
+ const root = options.root;
672
+ if (!existsSync(root)) throw new DiscoveryError(`root directory not found: ${root}`, "ROOT_NOT_FOUND");
673
+ await assertGitAvailable(this.git, root);
674
+ const toplevel = await resolveToplevel(this.git, root);
675
+ const { tracked, untracked, porcelain } = await collectGitFiles(this.git, toplevel);
676
+ const allFiles = [.../* @__PURE__ */ new Set([...tracked, ...untracked])];
677
+ const scoped = options.scopeToRoot === true ? scopeFilesToRoot(allFiles, toplevel, root) : null;
678
+ const projectRoot = scoped?.dir ?? toplevel;
679
+ const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
680
+ const filtered = applyMaxDepth(scoped?.files ?? allFiles, maxDepth);
681
+ const signals = detectSignals(filtered);
682
+ const languages = detectLanguages(filtered);
683
+ const rootPkgJson = readRootPackageJson(projectRoot);
684
+ const packageManagers = detectPackageManagers(signals, rootPkgJson);
685
+ const monorepo = detectMonorepo(signals, rootPkgJson);
686
+ const hasContainerBuild = signals.some((s) => s.type === "dockerfile" || s.type === "docker-compose");
687
+ const hasCiDefinition = signals.some((s) => s.type === "ci-definition");
688
+ const commit = await resolveHeadCommit(this.git, toplevel);
689
+ const dirty = porcelain.length > 0;
690
+ const changedFiles = await collectChangedFiles(this.git, toplevel, options.baseRef);
691
+ const gitSignal = {
692
+ type: "git-metadata",
693
+ path: ".git",
694
+ detail: commit ? `HEAD=${commit.slice(0, 12)}` : "no commits",
695
+ confidence: 1
696
+ };
697
+ const allSignals = [...signals, gitSignal];
698
+ return {
699
+ root: projectRoot,
700
+ commit,
701
+ dirty,
702
+ changedFiles,
703
+ languages,
704
+ packageManagers,
705
+ hasContainerBuild,
706
+ hasCiDefinition,
707
+ monorepo,
708
+ localSignals: allSignals,
709
+ explanation: buildExplanation(allSignals, {
710
+ hasBaseRef: Boolean(options.baseRef),
711
+ dirty
712
+ })
713
+ };
714
+ }
715
+ async plan(context) {
716
+ return synthesizePlan(context);
717
+ }
718
+ bindRunPlan(options) {
719
+ return bindRunPlan(options);
720
+ }
721
+ };
722
+ function applyMaxDepth(files, maxDepth) {
723
+ return files.filter((f) => f.split("/").length <= maxDepth);
724
+ }
725
+ /**
726
+ * Restrict repo-relative `files` to the subtree under `root`, re-rooting
727
+ * paths relative to `root`. Returns null when `root` is not inside the
728
+ * repository (should not happen — toplevel is resolved from `root`).
729
+ */
730
+ function scopeFilesToRoot(files, toplevel, root) {
731
+ const dir = realpathSync(resolve(root));
732
+ const prefix = relative(toplevel, dir).split(sep).join("/");
733
+ if (prefix.startsWith("..") || isAbsolute(prefix)) return null;
734
+ if (prefix === "") return {
735
+ dir,
736
+ files: [...files]
737
+ };
738
+ return {
739
+ dir,
740
+ files: files.filter((f) => f.startsWith(prefix + "/")).map((f) => f.slice(prefix.length + 1))
741
+ };
742
+ }
743
+ /** Throw `GIT_UNAVAILABLE` if git is not on PATH. */
744
+ async function assertGitAvailable(git, root) {
745
+ try {
746
+ await git.run(["--version"], root);
747
+ } catch (err) {
748
+ throw new DiscoveryError("git is not installed or not on PATH", "GIT_UNAVAILABLE", err);
749
+ }
750
+ }
751
+ /** Resolve the repository toplevel, throwing `GIT_NOT_A_REPO` on failure. */
752
+ async function resolveToplevel(git, root) {
753
+ try {
754
+ return (await git.run(["rev-parse", "--show-toplevel"], root)).trim();
755
+ } catch (err) {
756
+ throw new DiscoveryError(`not a git repository: ${root}`, "GIT_NOT_A_REPO", err);
757
+ }
758
+ }
759
+ /** Collect tracked files, untracked files, and porcelain lines from git. */
760
+ async function collectGitFiles(git, toplevel) {
761
+ let trackedRaw;
762
+ let porcelainRaw;
763
+ try {
764
+ trackedRaw = await git.run(["ls-files"], toplevel);
765
+ porcelainRaw = await git.run([
766
+ "status",
767
+ "--porcelain",
768
+ "-uall"
769
+ ], toplevel);
770
+ } catch (err) {
771
+ throw new DiscoveryError("filesystem traversal failed", "TRAVERSAL_FAILED", err);
772
+ }
773
+ const tracked = trackedRaw.split("\n").filter(Boolean);
774
+ const porcelain = porcelainRaw.split("\n").filter(Boolean);
775
+ return {
776
+ tracked,
777
+ untracked: extractUntracked(porcelain),
778
+ porcelain
779
+ };
780
+ }
781
+ /** Extract untracked file paths from porcelain status lines. */
782
+ function extractUntracked(porcelain) {
783
+ const untracked = [];
784
+ for (const line of porcelain) {
785
+ const status = line.slice(0, 2);
786
+ const path = line.slice(3).trim();
787
+ if (status.includes("??")) untracked.push(path);
788
+ }
789
+ return untracked;
790
+ }
791
+ /** Resolve the HEAD commit SHA, returning "" on failure. */
792
+ async function resolveHeadCommit(git, toplevel) {
793
+ try {
794
+ return (await git.run(["rev-parse", "HEAD"], toplevel)).trim();
795
+ } catch {
796
+ return "";
797
+ }
798
+ }
799
+ const REF_RE = /^[A-Za-z0-9_./~^\-@{}]+$/;
800
+ function validateBaseRef(baseRef) {
801
+ if (baseRef.startsWith("-") || !REF_RE.test(baseRef)) throw new DiscoveryError(`invalid baseRef: ${baseRef}`, "INVALID_BASE_REF");
802
+ }
803
+ /** Collect changed files against `baseRef` when provided. */
804
+ async function collectChangedFiles(git, toplevel, baseRef) {
805
+ if (!baseRef) return [];
806
+ validateBaseRef(baseRef);
807
+ try {
808
+ return parseDiffNameStatus(await git.run([
809
+ "diff",
810
+ "--name-status",
811
+ `${baseRef}...HEAD`,
812
+ "--"
813
+ ], toplevel));
814
+ } catch (err) {
815
+ throw new DiscoveryError(`failed to diff against baseRef "${baseRef}"`, "TRAVERSAL_FAILED", err);
816
+ }
817
+ }
818
+ function readRootPackageJson(root) {
819
+ try {
820
+ const raw = readFileSync(`${root}/package.json`, "utf8");
821
+ return JSON.parse(raw);
822
+ } catch {
823
+ return null;
824
+ }
825
+ }
826
+ function parseDiffNameStatus(diff) {
827
+ const out = [];
828
+ for (const line of diff.split("\n").filter(Boolean)) {
829
+ const parts = line.split(" ");
830
+ const code = parts[0] ?? "";
831
+ const path = code.startsWith("R") ? parts[2] ?? "" : parts[1] ?? "";
832
+ if (!path) continue;
833
+ out.push({
834
+ path,
835
+ status: diffStatusCode(code)
836
+ });
837
+ }
838
+ return out;
839
+ }
840
+ /** Map a git diff --name-status code to a `ChangedFile` status. */
841
+ function diffStatusCode(code) {
842
+ if (code.startsWith("A")) return "added";
843
+ if (code.startsWith("M")) return "modified";
844
+ if (code.startsWith("D")) return "deleted";
845
+ if (code.startsWith("R")) return "renamed";
846
+ return "modified";
847
+ }
848
+ const PLAN_DRIVERS = [
849
+ {
850
+ checkId: "typecheck",
851
+ reason: "Node project defaults",
852
+ languages: ["TypeScript", "JavaScript"],
853
+ packageManagers: [
854
+ "npm",
855
+ "yarn",
856
+ "pnpm",
857
+ "bun"
858
+ ]
859
+ },
860
+ {
861
+ checkId: "lint",
862
+ reason: "Node project defaults",
863
+ languages: ["TypeScript", "JavaScript"],
864
+ packageManagers: [
865
+ "npm",
866
+ "yarn",
867
+ "pnpm",
868
+ "bun"
869
+ ]
870
+ },
871
+ {
872
+ checkId: "test",
873
+ reason: "Node project defaults",
874
+ languages: ["TypeScript", "JavaScript"],
875
+ packageManagers: [
876
+ "npm",
877
+ "yarn",
878
+ "pnpm",
879
+ "bun"
880
+ ]
881
+ },
882
+ {
883
+ checkId: "lint",
884
+ reason: "Python project defaults",
885
+ languages: ["Python"],
886
+ packageManagers: [
887
+ "pip",
888
+ "poetry",
889
+ "uv",
890
+ "pipenv"
891
+ ]
892
+ },
893
+ {
894
+ checkId: "test",
895
+ reason: "Python project defaults",
896
+ languages: ["Python"],
897
+ packageManagers: [
898
+ "pip",
899
+ "poetry",
900
+ "uv",
901
+ "pipenv"
902
+ ]
903
+ },
904
+ {
905
+ checkId: "fmt-check",
906
+ reason: "Rust project defaults",
907
+ languages: ["Rust"],
908
+ packageManagers: ["cargo"]
909
+ },
910
+ {
911
+ checkId: "clippy",
912
+ reason: "Rust project defaults",
913
+ languages: ["Rust"],
914
+ packageManagers: ["cargo"]
915
+ },
916
+ {
917
+ checkId: "test",
918
+ reason: "Rust project defaults",
919
+ languages: ["Rust"],
920
+ packageManagers: ["cargo"]
921
+ },
922
+ {
923
+ checkId: "vet",
924
+ reason: "Go project defaults",
925
+ languages: ["Go"],
926
+ packageManagers: ["go"]
927
+ },
928
+ {
929
+ checkId: "test",
930
+ reason: "Go project defaults",
931
+ languages: ["Go"],
932
+ packageManagers: ["go"]
933
+ }
934
+ ];
935
+ function synthesizePlan(context) {
936
+ const langNames = context.languages.map((l) => l.name);
937
+ const pmNames = context.packageManagers.map((p) => p.name);
938
+ const signalRef = resolveSignalRef(context.localSignals);
939
+ const checks = collectDefaultChecks(PLAN_DRIVERS, langNames, pmNames, signalRef);
940
+ return {
941
+ context,
942
+ checks,
943
+ workflowPath: null,
944
+ notes: buildPlanNotes(checks, langNames, pmNames)
945
+ };
946
+ }
947
+ /** Resolve the signal reference for the first manifest/lockfile signal. */
948
+ function resolveSignalRef(signals) {
949
+ const manifestSignal = signals.find((s) => s.type === "manifest" || s.type === "lockfile") ?? null;
950
+ return manifestSignal ? `${manifestSignal.type}:${manifestSignal.path}` : null;
951
+ }
952
+ /** Collect deduplicated default checks that match the detected languages/package managers. */
953
+ function collectDefaultChecks(drivers, langNames, pmNames, signalRef) {
954
+ const checks = [];
955
+ const seen = /* @__PURE__ */ new Set();
956
+ for (const driver of drivers) {
957
+ const langMatch = driver.languages.some((l) => langNames.includes(l));
958
+ const pmMatch = driver.packageManagers.some((p) => pmNames.includes(p));
959
+ if (!langMatch || !pmMatch) continue;
960
+ const key = `${driver.checkId}:${driver.reason}`;
961
+ if (seen.has(key)) continue;
962
+ seen.add(key);
963
+ const id = makeCheckId(driver.checkId, driver.reason);
964
+ checks.push({
965
+ id,
966
+ checkId: driver.checkId,
967
+ reason: driver.reason,
968
+ signalRef,
969
+ priority: 2
970
+ });
971
+ }
972
+ return checks;
973
+ }
974
+ /** Build a stable proposed-check id from its checkId and reason. */
975
+ function makeCheckId(checkId, reason) {
976
+ return "prop-" + createHash("sha256").update(checkId + reason).digest("hex").slice(0, 16);
977
+ }
978
+ /** Build human-readable notes describing the selected checks. */
979
+ function buildPlanNotes(checks, langNames, pmNames) {
980
+ if (checks.length === 0) return ["No default checks applied: no recognized languages or package managers detected."];
981
+ const drivers = [];
982
+ if (langNames.length) drivers.push(`languages=[${langNames.join(",")}]`);
983
+ if (pmNames.length) drivers.push(`packageManagers=[${pmNames.join(",")}]`);
984
+ return [`Selected ${checks.length} default checks from ${drivers.join(" ")}.`];
985
+ }
986
+ //#endregion
987
+ //#region src/planner/matrix.ts
988
+ /**
989
+ * Expand all matrix steps in a step list into concrete step instances.
990
+ * Non-matrix steps pass through unchanged.
991
+ *
992
+ * - Each matrix step becomes N steps, one per combination (cross-product of
993
+ * dimensions, minus exclude, plus include).
994
+ * - Each expanded step gets `matrixValues` (the concrete bindings) and
995
+ * `matrix: undefined` (the spec is consumed).
996
+ * - Expanded step IDs: `<originalId>[key=val,key=val]` (keys sorted alphabetically).
997
+ * - Dependencies are rewired: a dependency on a matrix step maps to ALL
998
+ * expanded instances (all-to-all, no correlation matching in M1).
999
+ */
1000
+ function expandMatrixSteps(steps) {
1001
+ const expansionMap = /* @__PURE__ */ new Map();
1002
+ let hasMatrix = false;
1003
+ for (const step of steps) if (step.matrix) {
1004
+ hasMatrix = true;
1005
+ expansionMap.set(step.id, expandStep(step));
1006
+ } else expansionMap.set(step.id, [step]);
1007
+ if (!hasMatrix) return steps;
1008
+ const result = [];
1009
+ for (const step of steps) {
1010
+ const expanded = expansionMap.get(step.id);
1011
+ for (const expStep of expanded) result.push(rewireDependencies(expStep, expansionMap));
1012
+ }
1013
+ return result;
1014
+ }
1015
+ function expandStep(step) {
1016
+ const spec = step.matrix;
1017
+ if (spec.maxParallel !== void 0) {
1018
+ if (!Number.isInteger(spec.maxParallel) || spec.maxParallel < 1) throw new PlannerError(`matrix on step '${step.id}' has invalid maxParallel: must be a positive integer (got ${spec.maxParallel})`, "INVALID_MATRIX");
1019
+ }
1020
+ const combinations = computeCombinations(spec);
1021
+ if (combinations.length === 0) throw new PlannerError(`matrix on step '${step.id}' produced no combinations (check exclude rules)`, "INVALID_MATRIX");
1022
+ return combinations.map((combo) => {
1023
+ const { matrix: _consumed, ...rest } = step;
1024
+ return {
1025
+ ...rest,
1026
+ id: formatExpandedId(step.id, combo),
1027
+ matrixValues: combo,
1028
+ dependencies: step.dependencies,
1029
+ ...spec.failFast !== void 0 ? { matrixFailFast: spec.failFast } : {},
1030
+ ...spec.maxParallel !== void 0 ? { matrixMaxParallel: spec.maxParallel } : {}
1031
+ };
1032
+ });
1033
+ }
1034
+ function computeCombinations(spec) {
1035
+ const keys = Object.keys(spec.dimensions);
1036
+ if (keys.length === 0) return (spec.include ?? []).map((e) => ({ ...e }));
1037
+ return mergeIncludes(applyExcludes(crossProduct(spec.dimensions, keys), spec.exclude ?? []), spec.include ?? []);
1038
+ }
1039
+ /**
1040
+ * Compute the cross-product of all matrix dimensions.
1041
+ * Throws INVALID_MATRIX if any dimension has no values.
1042
+ */
1043
+ function crossProduct(dimensions, keys) {
1044
+ let combinations = [{}];
1045
+ for (const key of keys) {
1046
+ const values = dimensions[key];
1047
+ if (!values || values.length === 0) throw new PlannerError(`matrix dimension '${key}' has no values`, "INVALID_MATRIX");
1048
+ const next = [];
1049
+ for (const combo of combinations) for (const v of values) next.push({
1050
+ ...combo,
1051
+ [key]: v
1052
+ });
1053
+ combinations = next;
1054
+ }
1055
+ return combinations;
1056
+ }
1057
+ /** Filter out combinations matching any exclude rule (partial match). */
1058
+ function applyExcludes(combinations, exclude) {
1059
+ if (exclude.length === 0) return [...combinations];
1060
+ return combinations.filter((c) => !matchesAnyRule(c, exclude));
1061
+ }
1062
+ /** Merge include entries, deduplicating against existing combinations. */
1063
+ function mergeIncludes(combinations, include) {
1064
+ const includeEntries = include.map((e) => ({ ...e }));
1065
+ const existingKeys = new Set(combinations.map(comboKey));
1066
+ const uniqueIncludes = includeEntries.filter((e) => !existingKeys.has(comboKey(e)));
1067
+ return [...combinations, ...uniqueIncludes];
1068
+ }
1069
+ function matchesAnyRule(combo, rules) {
1070
+ for (const rule of rules) {
1071
+ const entries = Object.entries(rule);
1072
+ if (entries.length === 0) continue;
1073
+ if (entries.every(([k, v]) => combo[k] === v)) return true;
1074
+ }
1075
+ return false;
1076
+ }
1077
+ /** Produce a deterministic string key for a combination (sorted key=value pairs). */
1078
+ function comboKey(combo) {
1079
+ return Object.keys(combo).sort((a, b) => a.localeCompare(b)).map((k) => `${k}=${String(combo[k])}`).join(",");
1080
+ }
1081
+ function formatExpandedId(id, combo) {
1082
+ return `${id}[${Object.keys(combo).sort((a, b) => a.localeCompare(b)).map((k) => `${k}=${combo[k]}`).join(",")}]`;
1083
+ }
1084
+ function rewireDependencies(step, expansionMap) {
1085
+ const hasDeps = step.dependencies.length > 0;
1086
+ const hasInputs = step.inputs.length > 0;
1087
+ if (!hasDeps && !hasInputs) return step;
1088
+ const newDeps = hasDeps ? rewireDeps(step.dependencies, expansionMap) : step.dependencies;
1089
+ const newInputs = hasInputs ? rewireInputs(step.inputs, expansionMap) : step.inputs;
1090
+ return {
1091
+ ...step,
1092
+ dependencies: newDeps,
1093
+ inputs: newInputs
1094
+ };
1095
+ }
1096
+ function rewireDeps(deps, expansionMap) {
1097
+ const newDeps = [];
1098
+ const seen = /* @__PURE__ */ new Set();
1099
+ for (const dep of deps) {
1100
+ const expanded = expansionMap.get(dep.producer);
1101
+ if (!expanded) {
1102
+ addDep(newDeps, seen, dep);
1103
+ continue;
1104
+ }
1105
+ for (const expStep of expanded) addDep(newDeps, seen, {
1106
+ ...dep,
1107
+ producer: expStep.id
1108
+ });
1109
+ }
1110
+ return newDeps;
1111
+ }
1112
+ function rewireInputs(inputs, expansionMap) {
1113
+ const newInputs = [];
1114
+ const seen = /* @__PURE__ */ new Set();
1115
+ for (const ref of inputs) {
1116
+ if (ref.kind !== "step") {
1117
+ newInputs.push(ref);
1118
+ continue;
1119
+ }
1120
+ const expanded = expansionMap.get(ref.step);
1121
+ if (!expanded) {
1122
+ newInputs.push(ref);
1123
+ continue;
1124
+ }
1125
+ for (const expStep of expanded) {
1126
+ const rewired = {
1127
+ ...ref,
1128
+ step: expStep.id
1129
+ };
1130
+ const key = `${rewired.step}:${rewired.output}`;
1131
+ if (seen.has(key)) continue;
1132
+ seen.add(key);
1133
+ newInputs.push(rewired);
1134
+ }
1135
+ }
1136
+ return newInputs;
1137
+ }
1138
+ function addDep(deps, seen, dep) {
1139
+ const key = dep.kind === "control" ? `control:${dep.producer}` : `${dep.kind}:${dep.producer}:${dep.output}`;
1140
+ if (seen.has(key)) return;
1141
+ seen.add(key);
1142
+ deps.push(dep);
1143
+ }
1144
+ //#endregion
1145
+ //#region src/errors.ts
1146
+ var SdkError = class extends Error {
1147
+ cause;
1148
+ code;
1149
+ constructor(message, code, cause) {
1150
+ super(message);
1151
+ this.name = "SdkError";
1152
+ this.code = code;
1153
+ if (cause !== void 0) this.cause = cause;
1154
+ }
1155
+ };
1156
+ //#endregion
1157
+ //#region src/compat/config.ts
1158
+ const CONFIG_NAMES = ["sverka.config.ts", "sverka.config.js"];
1159
+ const MAX_PARENTS = 5;
1160
+ /**
1161
+ * Search upward from `root` for sverka.config.ts (then .js). Returns the
1162
+ * path or null. Searches up to 5 parent directories.
1163
+ */
1164
+ async function findConfig(root) {
1165
+ let dir = resolve(root);
1166
+ for (let i = 0; i <= MAX_PARENTS; i++) {
1167
+ for (const name of CONFIG_NAMES) {
1168
+ const candidate = resolve(dir, name);
1169
+ if (existsSync(candidate)) return candidate;
1170
+ }
1171
+ const parent = dirname(dir);
1172
+ if (parent === dir) break;
1173
+ dir = parent;
1174
+ }
1175
+ return null;
1176
+ }
1177
+ /**
1178
+ * Dynamically import a sverka.config.ts file and return its default
1179
+ * export as a WorkflowDefinition. Validates the shape.
1180
+ *
1181
+ * Under Bun, .ts files import natively. Under Node, use the .js fallback
1182
+ * (findConfig searches .ts first, then .js).
1183
+ */
1184
+ async function loadWorkflow(configPath, root) {
1185
+ const resolved = isAbsolute(configPath) ? resolve(configPath) : resolve(root ?? process.cwd(), configPath);
1186
+ if (!existsSync(resolved)) throw new SdkError(`config file not found: ${configPath}`, "CONFIG_NOT_FOUND");
1187
+ if (root !== void 0) {
1188
+ const rel = relative(resolve(root), resolved);
1189
+ if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) throw new SdkError(`config path escapes root: ${configPath}`, "CONFIG_PATH_ESCAPE");
1190
+ }
1191
+ let mod;
1192
+ try {
1193
+ mod = await import(pathToFileURL(resolved).href);
1194
+ } catch (e) {
1195
+ throw new SdkError(`failed to load config: ${basename(configPath)}`, "CONFIG_LOAD_FAILED", e);
1196
+ }
1197
+ const def = mod.default;
1198
+ validateDefaultExport(def);
1199
+ validateWorkflowShape(def.workflow);
1200
+ return def;
1201
+ }
1202
+ function validateDefaultExport(def) {
1203
+ if (!isPlainObject(def)) throw new SdkError("config default export must be an object", "CONFIG_INVALID");
1204
+ if (typeof def.name !== "string" || def.name.length === 0) throw new SdkError("config default export must have a non-empty 'name' string", "CONFIG_INVALID");
1205
+ if (!isPlainObject(def.workflow)) throw new SdkError("config default export must have a 'workflow' object", "CONFIG_INVALID");
1206
+ }
1207
+ function validateWorkflowShape(wf) {
1208
+ const isWorkflow = Array.isArray(wf.roots) && typeof wf.plan === "function";
1209
+ const isOperation = typeof wf.kind === "string" && isPlainObject(wf.spec);
1210
+ if (!isWorkflow && !isOperation) throw new SdkError("config 'workflow' must be a Workflow (with 'roots' and 'plan') or an Operation (with 'kind' and 'spec')", "CONFIG_INVALID");
1211
+ }
1212
+ function isPlainObject(v) {
1213
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1214
+ }
1215
+ //#endregion
1216
+ //#region src/compat/convert.ts
1217
+ const SVERKA_VERSION = (() => {
1218
+ let raw = "";
1219
+ try {
1220
+ raw = readFileSync(new URL("../package.json", import.meta.url), "utf-8");
1221
+ } catch {
1222
+ try {
1223
+ raw = readFileSync(new URL("../../package.json", import.meta.url), "utf-8");
1224
+ } catch {
1225
+ return "0.0.0";
1226
+ }
1227
+ }
1228
+ const parsed = JSON.parse(raw);
1229
+ if (typeof parsed === "object" && parsed !== null && "version" in parsed && typeof parsed.version === "string") return parsed.version;
1230
+ return "0.0.0";
1231
+ })();
1232
+ /**
1233
+ * Convert an array of OperationSpecs into a validated IR Plan with all
1234
+ * defaults filled per the spec's conversion table.
1235
+ */
1236
+ function convertToPlan(operations, opts) {
1237
+ const planOps = operations.map((spec) => convertOperation(spec, opts));
1238
+ const sourceContextHash = computeSourceContextHash(opts.context);
1239
+ const metadata = {
1240
+ sverkaVersion: SVERKA_VERSION,
1241
+ generatedBy: "manual"
1242
+ };
1243
+ const body = {
1244
+ apiVersion: "sverka.dev/v1",
1245
+ name: opts.name,
1246
+ sourceContextHash,
1247
+ operations: planOps,
1248
+ metadata
1249
+ };
1250
+ const id = computePlanId$1(body);
1251
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
1252
+ return {
1253
+ ...body,
1254
+ id,
1255
+ createdAt
1256
+ };
1257
+ }
1258
+ function convertOperation(spec, opts) {
1259
+ if (!spec.id || spec.id.length === 0) throw new SdkError(`operation has empty id (kind=${spec.kind}, name=${spec.name})`, "EXECUTION_FAILED");
1260
+ return buildPlanOperation(spec, buildExecutor(spec, opts), buildResources(spec), buildRetry(spec), (spec.artifacts ?? []).map(convertArtifact), spec.cache ? convertCache(spec.cache) : void 0);
1261
+ }
1262
+ function buildExecutor(spec, opts) {
1263
+ return {
1264
+ type: opts.executor,
1265
+ ...spec.image !== void 0 ? { image: spec.image } : {},
1266
+ ...spec.imageDigest !== void 0 ? { imageDigest: spec.imageDigest } : {}
1267
+ };
1268
+ }
1269
+ function buildResources(spec) {
1270
+ return {
1271
+ cpu: spec.cpuLimit ?? "1",
1272
+ memory: spec.memoryLimit ?? "512Mi"
1273
+ };
1274
+ }
1275
+ function buildRetry(spec) {
1276
+ const retries = spec.retries ?? 0;
1277
+ return {
1278
+ maxAttempts: Math.max(1, retries + 1),
1279
+ backoffSeconds: 0,
1280
+ retryOn: ["failure", "timeout"]
1281
+ };
1282
+ }
1283
+ function buildPlanOperation(spec, executor, resources, retry, artifacts, cache) {
1284
+ return {
1285
+ id: spec.id,
1286
+ kind: spec.kind,
1287
+ name: spec.name,
1288
+ ...optionalFields(spec),
1289
+ ...resolveDefaults(spec),
1290
+ executor,
1291
+ resources,
1292
+ ...cache !== void 0 ? { cache } : {},
1293
+ artifacts,
1294
+ retry
1295
+ };
1296
+ }
1297
+ function valueOrDefault(value, defaultValue) {
1298
+ return value === void 0 ? defaultValue : value;
1299
+ }
1300
+ /** Resolve fields with default values to reduce buildPlanOperation complexity. */
1301
+ function resolveDefaults(spec) {
1302
+ return {
1303
+ dependsOn: valueOrDefault(spec.dependsOn, []),
1304
+ network: valueOrDefault(spec.network, "deny"),
1305
+ credentials: valueOrDefault(spec.credentials, []),
1306
+ timeoutSeconds: valueOrDefault(spec.timeoutSeconds, 300),
1307
+ continueOnError: valueOrDefault(spec.continueOnError, false)
1308
+ };
1309
+ }
1310
+ function optionalFields(spec) {
1311
+ return {
1312
+ ...spec.description !== void 0 ? { description: spec.description } : {},
1313
+ ...spec.command !== void 0 ? { command: spec.command } : {},
1314
+ ...spec.args !== void 0 ? { args: spec.args } : {},
1315
+ ...spec.env !== void 0 ? { env: spec.env } : {},
1316
+ ...spec.workingDir !== void 0 ? { workingDir: spec.workingDir } : {},
1317
+ ...spec.condition !== void 0 ? { condition: spec.condition } : {}
1318
+ };
1319
+ }
1320
+ function convertArtifact(a) {
1321
+ return {
1322
+ path: a.path,
1323
+ ...a.name !== void 0 ? { name: a.name } : {},
1324
+ retain: a.retain ?? false
1325
+ };
1326
+ }
1327
+ function convertCache(c) {
1328
+ return {
1329
+ inputs: c.inputs,
1330
+ outputs: c.outputs ?? [],
1331
+ key: c.key ?? computeCacheKey(c.inputs)
1332
+ };
1333
+ }
1334
+ function computeCacheKey(inputs) {
1335
+ return createHash("sha256").update(inputs.join(",")).digest("hex").slice(0, 32);
1336
+ }
1337
+ function computeSourceContextHash(context) {
1338
+ const changedFiles = context ? [...context.changedFiles.map((f) => f.path)].sort((a, b) => a.localeCompare(b)) : [];
1339
+ const value = {
1340
+ commit: context?.commit ?? "",
1341
+ dirty: context?.dirty ?? false,
1342
+ changedFiles
1343
+ };
1344
+ return createHash("sha256").update(canonicalStringify(value)).digest("hex");
1345
+ }
1346
+ //#endregion
1347
+ //#region src/compat/internal/plan-runtime.ts
1348
+ /**
1349
+ * A plan-mode Runtime that records operations without side effects.
1350
+ * Used by the SDK to evaluate a workflow graph into an OperationSpec[]
1351
+ * without executing any commands.
1352
+ */
1353
+ var PlanRuntime = class {
1354
+ mode = "plan";
1355
+ recorded = [];
1356
+ start;
1357
+ constructor() {
1358
+ this.start = Date.now();
1359
+ }
1360
+ async evaluate(operation) {
1361
+ this.recorded.push(operation);
1362
+ return {
1363
+ operationId: operation.id,
1364
+ status: "planned",
1365
+ durationMs: 0
1366
+ };
1367
+ }
1368
+ async finalize() {
1369
+ return {
1370
+ mode: "plan",
1371
+ operations: this.recorded,
1372
+ outcomes: [],
1373
+ durationMs: Date.now() - this.start
1374
+ };
1375
+ }
1376
+ };
1377
+ //#endregion
1378
+ //#region src/sverka.ts
1379
+ /**
1380
+ * Create a Sverka instance with default options pre-applied. Per-call
1381
+ * options override defaults.
1382
+ */
1383
+ function createSverka(defaultOptions) {
1384
+ return {
1385
+ async plan(options) {
1386
+ return doPlan(mergeOptions(defaultOptions, options));
1387
+ },
1388
+ async toPlan(options) {
1389
+ return buildPlan(mergeOptions(defaultOptions, options));
1390
+ },
1391
+ async execute(options) {
1392
+ return doExecute(mergeOptions(defaultOptions, options));
1393
+ }
1394
+ };
1395
+ }
1396
+ /** Top-level plan convenience function. */
1397
+ async function plan(options) {
1398
+ return doPlan(options ?? {});
1399
+ }
1400
+ /** Top-level toPlan convenience function. Returns the canonical Plan IR. */
1401
+ async function toPlan(options) {
1402
+ return buildPlan(options ?? {});
1403
+ }
1404
+ /** Top-level execute convenience function. */
1405
+ async function execute(options) {
1406
+ return doExecute(options ?? {});
1407
+ }
1408
+ async function doPlan(options) {
1409
+ const root = options.root ?? process.cwd();
1410
+ const planner = createPlanner();
1411
+ const context = await planner.discover({
1412
+ root,
1413
+ ...options.baseRef !== void 0 ? { baseRef: options.baseRef } : {}
1414
+ });
1415
+ const configPath = await resolveConfigPath(options, root);
1416
+ if (configPath !== null) {
1417
+ const def = await loadWorkflow(configPath, root);
1418
+ const operations = await evaluateWorkflow(def);
1419
+ return {
1420
+ context,
1421
+ operations,
1422
+ proposal: null,
1423
+ plan: buildPlanFromOps(operations, def.name, context)
1424
+ };
1425
+ }
1426
+ const proposal = await planner.plan(context);
1427
+ const resolver = options.resolver ?? createBuiltinResolver$1();
1428
+ const operations = [];
1429
+ for (const check of proposal.checks) try {
1430
+ const r = resolver.resolve(check, context);
1431
+ if (r !== null) operations.push(r.operation);
1432
+ } catch {}
1433
+ return {
1434
+ context,
1435
+ operations,
1436
+ proposal,
1437
+ plan: buildPlanFromOps(operations, "sverka-plan", context)
1438
+ };
1439
+ }
1440
+ /** Build a canonical Plan IR from resolved operations. */
1441
+ async function buildPlan(options) {
1442
+ return (await doPlan(options)).plan;
1443
+ }
1444
+ /** Convert operations into a validated Plan with host executor defaults. */
1445
+ function buildPlanFromOps(operations, name, context) {
1446
+ const plan = convertToPlan(operations, {
1447
+ name,
1448
+ executor: "host",
1449
+ context
1450
+ });
1451
+ if (operations.length > 0) {
1452
+ const validation = validatePlan$1(plan);
1453
+ if (!validation.valid) throw new SdkError(`plan validation failed: ${validation.errors.map((e) => e.message).join("; ")}`, "EXECUTION_FAILED");
1454
+ }
1455
+ return plan;
1456
+ }
1457
+ async function doExecute(options) {
1458
+ const root = options.root ?? process.cwd();
1459
+ const executorType = options.executor ?? "host";
1460
+ const context = await createPlanner().discover({
1461
+ root,
1462
+ ...options.baseRef !== void 0 ? { baseRef: options.baseRef } : {}
1463
+ });
1464
+ const { def, operations, resolvedChecks, planName } = await resolveOperations(options, root, context, executorType);
1465
+ const { runtimeResult, findings } = await runPlan(buildAndValidatePlan(operations, planName, executorType, context), resolvedChecks, root, executorType);
1466
+ return buildExecutionResult(runtimeResult, findings, options, def);
1467
+ }
1468
+ /** Load or auto-discover the operations to execute. */
1469
+ async function resolveOperations(options, root, context, executorType) {
1470
+ const configPath = await resolveConfigPath(options, root);
1471
+ if (configPath !== null) {
1472
+ const def = await loadWorkflow(configPath, root);
1473
+ return {
1474
+ def,
1475
+ operations: await evaluateWorkflow(def),
1476
+ resolvedChecks: [],
1477
+ planName: def.name
1478
+ };
1479
+ }
1480
+ const proposal = await createPlanner().plan(context);
1481
+ const resolver = options.resolver ?? createBuiltinResolver$1();
1482
+ const tmpChecks = [];
1483
+ for (const check of proposal.checks) try {
1484
+ const r = resolver.resolve(check, context);
1485
+ if (r !== null) tmpChecks.push(r);
1486
+ } catch {}
1487
+ if (executorType === "docker" && tmpChecks.some((r) => r.operation.image === void 0)) throw new SdkError("docker executor requires container images; every operation must declare an image", "EXECUTION_FAILED");
1488
+ const operations = tmpChecks.map((r) => buildOperationWithOutputs(r));
1489
+ if (operations.length === 0) throw new SdkError("no config found and auto-discovery produced no resolvable checks", "CONFIG_NOT_FOUND");
1490
+ return {
1491
+ def: null,
1492
+ operations,
1493
+ resolvedChecks: tmpChecks,
1494
+ planName: "sverka-plan"
1495
+ };
1496
+ }
1497
+ /** Convert operations into a Plan and validate it. */
1498
+ function buildAndValidatePlan(operations, planName, executorType, context) {
1499
+ const plan = convertToPlan(operations, {
1500
+ name: planName,
1501
+ executor: executorType,
1502
+ context
1503
+ });
1504
+ if (operations.length > 0) {
1505
+ const validation = validatePlan$1(plan);
1506
+ if (!validation.valid) throw new SdkError(`plan validation failed: ${validation.errors.map((e) => e.message).join("; ")}`, "EXECUTION_FAILED");
1507
+ }
1508
+ return plan;
1509
+ }
1510
+ /** Execute a Plan and extract findings from resolved checks. */
1511
+ async function runPlan(plan, resolvedChecks, root, executorType) {
1512
+ const artifactDir = mkdtempSync(join(tmpdir(), "sverka-artifacts-"));
1513
+ const cacheDir = mkdtempSync(join(tmpdir(), "sverka-cache-"));
1514
+ try {
1515
+ const { Scheduler } = await import("@sverka/runtime");
1516
+ const scheduler = new Scheduler({
1517
+ executors: [await createExecutor(executorType, cacheDir, plan.operations)],
1518
+ maxConcurrent: 4,
1519
+ workspace: root,
1520
+ artifactDir,
1521
+ cacheDir,
1522
+ credentials: {},
1523
+ resume: false
1524
+ });
1525
+ try {
1526
+ return {
1527
+ runtimeResult: await scheduler.execute(plan),
1528
+ findings: await extractResolvedFindings(resolvedChecks, artifactDir)
1529
+ };
1530
+ } catch (e) {
1531
+ throw new SdkError(`execution failed: ${e instanceof Error ? e.message : String(e)}`, "EXECUTION_FAILED", e);
1532
+ } finally {
1533
+ await scheduler.dispose().catch(() => {});
1534
+ }
1535
+ } finally {
1536
+ await Promise.all([rm(artifactDir, {
1537
+ recursive: true,
1538
+ force: true
1539
+ }).catch(() => {}), rm(cacheDir, {
1540
+ recursive: true,
1541
+ force: true
1542
+ }).catch(() => {})]);
1543
+ }
1544
+ }
1545
+ /** Extract findings from resolved checks' SARIF output artifacts. */
1546
+ async function extractResolvedFindings(resolvedChecks, artifactDir) {
1547
+ const all = [];
1548
+ for (const r of resolvedChecks) {
1549
+ if (r.outputs.length === 0) continue;
1550
+ const extracted = await extractFindings$1(r.outputs, artifactDir, r.checkId);
1551
+ all.push(...extracted);
1552
+ }
1553
+ return all;
1554
+ }
1555
+ /** Build the final ExecutionResult from runtime output and user options. */
1556
+ async function buildExecutionResult(runtimeResult, findings, options, def) {
1557
+ let baselineFingerprints = [];
1558
+ let filteredFindings = findings;
1559
+ if (options.baselinePath) {
1560
+ const baseline = await loadBaseline$1(options.baselinePath);
1561
+ baselineFingerprints = baseline.fingerprints;
1562
+ filteredFindings = options.onlyNew ? filterOnlyNew$1(findings, baseline) : findings;
1563
+ }
1564
+ const policy = def?.policy ? createPolicy$1(def.policy) : DEFAULT_POLICY$1;
1565
+ const policyResult = evaluatePolicy$1(filteredFindings, policy, baselineFingerprints);
1566
+ const verdict = runtimeResult.status === "success" ? policyResult.verdict : "fail";
1567
+ return {
1568
+ findings: filteredFindings,
1569
+ policyResult,
1570
+ verdict,
1571
+ status: runtimeResult.status,
1572
+ ...runtimeResult.runtimeFailure ? { runtimeFailure: runtimeResult.runtimeFailure } : {},
1573
+ outcomes: runtimeResult.outcomes,
1574
+ durationMs: runtimeResult.durationMs
1575
+ };
1576
+ }
1577
+ function mergeOptions(defaults, callOpts) {
1578
+ return {
1579
+ ...defaults,
1580
+ ...callOpts
1581
+ };
1582
+ }
1583
+ async function resolveConfigPath(options, root) {
1584
+ if (options.configPath) return options.configPath;
1585
+ return findConfig(root);
1586
+ }
1587
+ async function evaluateWorkflow(def) {
1588
+ const wf = normalizeWorkflow(def.name, def.workflow);
1589
+ const runtime = new PlanRuntime();
1590
+ return (await wf.plan(runtime)).operations;
1591
+ }
1592
+ /**
1593
+ * Normalize a WorkflowDefinition's workflow field to a Workflow.
1594
+ * If it's already a Workflow (has `roots` and `plan`), return as-is.
1595
+ * If it's a bare Operation, wrap it with `workflow(name, op)`.
1596
+ */
1597
+ function normalizeWorkflow(name, wf) {
1598
+ if (isWorkflow(wf)) return wf;
1599
+ return workflow$1(name, wf);
1600
+ }
1601
+ function isWorkflow(wf) {
1602
+ return "roots" in wf && "plan" in wf;
1603
+ }
1604
+ async function createExecutor(type, cacheDir, operations) {
1605
+ if (type === "docker") {
1606
+ const { DockerExecutor } = await import("@sverka/runtime");
1607
+ return new DockerExecutor({
1608
+ runAs: `${process.getuid?.() ?? 1e3}:${process.getgid?.() ?? 1e3}`,
1609
+ cacheDir
1610
+ });
1611
+ }
1612
+ const { HostExecutor, createAllowlist } = await import("@sverka/runtime");
1613
+ return new HostExecutor({
1614
+ enabled: true,
1615
+ allowlist: createAllowlist(operations.map((op) => op.command).filter((c) => typeof c === "string" && c.length > 0)),
1616
+ envAllowlist: ["PATH"]
1617
+ });
1618
+ }
1619
+ /** Merge resolved-check outputs into the operation's artifact declarations. */
1620
+ function buildOperationWithOutputs(r) {
1621
+ if (r.outputs.length === 0) return r.operation;
1622
+ const artifacts = [...r.operation.artifacts ?? [], ...r.outputs.map((o) => ({
1623
+ path: o.path,
1624
+ retain: false
1625
+ }))];
1626
+ return {
1627
+ ...r.operation,
1628
+ artifacts
1629
+ };
1630
+ }
1631
+ //#endregion
1632
+ //#region src/compat/helpers.ts
1633
+ /** Name an operation. Sugar for `op.named(name)`. */
1634
+ function task(name, op) {
1635
+ return op.named(name);
1636
+ }
1637
+ /** Type-safe helper for sverka.config.ts. Identity function. */
1638
+ function defineWorkflow(definition) {
1639
+ return definition;
1640
+ }
1641
+ //#endregion
1642
+ //#region src/internal/is-reference.ts
1643
+ const OUTPUT_TYPES = /* @__PURE__ */ new Set([
1644
+ "string",
1645
+ "number",
1646
+ "boolean",
1647
+ "artifact"
1648
+ ]);
1649
+ const CONTEXT_NAMESPACES = /* @__PURE__ */ new Set([
1650
+ "env",
1651
+ "secrets",
1652
+ "git",
1653
+ "change",
1654
+ "event",
1655
+ "run",
1656
+ "inputs",
1657
+ "matrix"
1658
+ ]);
1659
+ /**
1660
+ * Type guard for Reference values.
1661
+ * Shared between expr.ts and sh.ts to avoid duplication.
1662
+ * Validates that type and namespace values are within their finite unions.
1663
+ */
1664
+ function isReference(value) {
1665
+ if (typeof value !== "object" || value === null || !("kind" in value)) return false;
1666
+ const ref = value;
1667
+ if (ref.kind === "step") return isStepRef(value);
1668
+ if (ref.kind === "context") return isContextRef(value);
1669
+ return false;
1670
+ }
1671
+ function isStepRef(value) {
1672
+ const s = value;
1673
+ return typeof s.step === "string" && typeof s.output === "string" && typeof s.type === "string" && OUTPUT_TYPES.has(s.type);
1674
+ }
1675
+ function isContextRef(value) {
1676
+ const c = value;
1677
+ return typeof c.namespace === "string" && typeof c.field === "string" && CONTEXT_NAMESPACES.has(c.namespace);
1678
+ }
1679
+ //#endregion
1680
+ //#region src/dollar.ts
1681
+ function createBuilder(state) {
1682
+ const builder = {
1683
+ outputs(outputs) {
1684
+ state.outputs = outputs;
1685
+ return builder;
1686
+ },
1687
+ inputs(inputs) {
1688
+ state.inputs = inputs;
1689
+ return builder;
1690
+ },
1691
+ dependsOn(steps) {
1692
+ state.dependsOn = steps;
1693
+ return builder;
1694
+ },
1695
+ runtime(runtime) {
1696
+ state.runtime = runtime;
1697
+ return builder;
1698
+ },
1699
+ timeout(ms) {
1700
+ state.timeout = ms;
1701
+ return builder;
1702
+ },
1703
+ condition(ref) {
1704
+ state.condition = ref;
1705
+ return builder;
1706
+ },
1707
+ matrix(spec) {
1708
+ state.matrix = spec;
1709
+ return builder;
1710
+ },
1711
+ interruptible(value) {
1712
+ state.interruptible = value ?? true;
1713
+ return builder;
1714
+ },
1715
+ compensate(command) {
1716
+ state.compensation = {
1717
+ kind: "shell",
1718
+ command
1719
+ };
1720
+ return builder;
1721
+ },
1722
+ build(pipeline, id) {
1723
+ const allInputs = [...state.inputs ? [...state.inputs] : [], ...state.collectedInputs];
1724
+ return new ShellStep(pipeline, id, {
1725
+ command: state.command,
1726
+ ...state.outputs ? { outputs: state.outputs } : {},
1727
+ ...allInputs.length > 0 ? { inputs: allInputs } : {},
1728
+ ...state.dependsOn ? { dependsOn: state.dependsOn } : {},
1729
+ ...state.runtime ? { runtime: state.runtime } : {},
1730
+ ...state.timeout !== void 0 ? { timeout: state.timeout } : {},
1731
+ ...state.condition !== void 0 ? { condition: state.condition } : {},
1732
+ ...state.matrix !== void 0 ? { matrix: state.matrix } : {},
1733
+ ...state.interruptible !== void 0 ? { interruptible: state.interruptible } : {},
1734
+ ...state.compensation !== void 0 ? { compensation: state.compensation } : {}
1735
+ });
1736
+ }
1737
+ };
1738
+ return builder;
1739
+ }
1740
+ /**
1741
+ * Create a shell command step builder from a tagged template.
1742
+ * References interpolated in the template are collected and added to the
1743
+ * step's inputs automatically.
1744
+ */
1745
+ function $(strings, ...values) {
1746
+ let command = "";
1747
+ const collectedInputs = [];
1748
+ for (let i = 0; i < strings.length; i++) {
1749
+ command += strings[i];
1750
+ if (i < values.length) {
1751
+ const v = values[i];
1752
+ if (typeof v === "string") command += v;
1753
+ else if (isReference(v)) {
1754
+ const ref = v;
1755
+ collectedInputs.push(ref);
1756
+ if (ref.kind === "step") command += `\${${ref.step}.${ref.output}}`;
1757
+ else command += `\${${ref.namespace}.${ref.field}}`;
1758
+ } else throw new SdkError(`invalid interpolation value at position ${i}: expected string or Reference`, "INVALID_INTERPOLATION");
1759
+ }
1760
+ }
1761
+ return createBuilder({
1762
+ command,
1763
+ collectedInputs
1764
+ });
1765
+ }
1766
+ //#endregion
1767
+ //#region src/shell.ts
1768
+ /**
1769
+ * Create a ShellProxy bound to an optional command prefix and shell interpreter.
1770
+ * - prefix: accumulated property-access chain (e.g. "git" for shell.git)
1771
+ * - shellOpt: interpreter string from shell("bash"), applied as runtime.shell
1772
+ */
1773
+ function createShellProxy(prefix, shellOpt) {
1774
+ const proxy = function(stringsOrInterpreter, ...values) {
1775
+ if (typeof stringsOrInterpreter === "string") return createShellProxy(prefix, stringsOrInterpreter);
1776
+ const builder = $(stringsOrInterpreter, ...values);
1777
+ if (prefix) return wrapBuilder(builder, (step) => applyPrefixAndShell(step, prefix, shellOpt));
1778
+ if (shellOpt) return wrapBuilder(builder, (step) => applyShell(step, shellOpt));
1779
+ return builder;
1780
+ };
1781
+ return new Proxy(proxy, { get(_target, prop) {
1782
+ if (typeof prop !== "string") return void 0;
1783
+ if (prop === "then" || prop === "toJSON" || prop === "toString") return void 0;
1784
+ return createShellProxy(prefix ? `${prefix} ${prop}` : prop, shellOpt);
1785
+ } });
1786
+ }
1787
+ /**
1788
+ * Apply a command prefix and optional shell interpreter to a built step.
1789
+ * Mutates the step in-place to preserve pipeline registration.
1790
+ */
1791
+ function applyPrefixAndShell(step, prefix, shellOpt) {
1792
+ const runtime = shellOpt ? {
1793
+ ...step.runtime,
1794
+ shell: shellOpt
1795
+ } : step.runtime;
1796
+ Object.assign(step, {
1797
+ command: `${prefix} ${step.command}`,
1798
+ runtime
1799
+ });
1800
+ return step;
1801
+ }
1802
+ /**
1803
+ * Apply a shell interpreter to a built step (no prefix).
1804
+ * Mutates the step in-place to preserve pipeline registration.
1805
+ */
1806
+ function applyShell(step, interpreter) {
1807
+ const runtime = {
1808
+ ...step.runtime,
1809
+ shell: interpreter
1810
+ };
1811
+ Object.assign(step, { runtime });
1812
+ return step;
1813
+ }
1814
+ /**
1815
+ * Wrap a StepBuilder so `build()` applies a transform to the final step.
1816
+ * Chain methods delegate to the original builder and return the wrapper.
1817
+ */
1818
+ function wrapBuilder(builder, transform) {
1819
+ const origBuild = builder.build.bind(builder);
1820
+ const wrapped = {
1821
+ outputs(o) {
1822
+ builder.outputs(o);
1823
+ return wrapped;
1824
+ },
1825
+ inputs(i) {
1826
+ builder.inputs(i);
1827
+ return wrapped;
1828
+ },
1829
+ dependsOn(s) {
1830
+ builder.dependsOn(s);
1831
+ return wrapped;
1832
+ },
1833
+ runtime(r) {
1834
+ builder.runtime(r);
1835
+ return wrapped;
1836
+ },
1837
+ timeout(ms) {
1838
+ builder.timeout(ms);
1839
+ return wrapped;
1840
+ },
1841
+ condition(ref) {
1842
+ builder.condition(ref);
1843
+ return wrapped;
1844
+ },
1845
+ matrix(spec) {
1846
+ builder.matrix(spec);
1847
+ return wrapped;
1848
+ },
1849
+ interruptible(value) {
1850
+ builder.interruptible(value);
1851
+ return wrapped;
1852
+ },
1853
+ compensate(command) {
1854
+ builder.compensate(command);
1855
+ return wrapped;
1856
+ },
1857
+ build(pipeline, id) {
1858
+ return transform(origBuild(pipeline, id));
1859
+ }
1860
+ };
1861
+ return wrapped;
1862
+ }
1863
+ /**
1864
+ * Shell proxy export. Provides command-prefix and shell-selector DX.
1865
+ *
1866
+ * @example
1867
+ * const { git, npm, bun, make, docker } = shell;
1868
+ * git`push origin main`.build(p, "push") // → "git push origin main"
1869
+ * bun`run build`.build(p, "build") // → "bun run build"
1870
+ *
1871
+ * @example
1872
+ * const bash = shell("bash");
1873
+ * bash.git`push`.build(p, "push") // → "git push" + runtime.shell="bash"
1874
+ * shell("pwsh")`Write-Host hello`.build(p, "greet") // → "Write-Host hello" + runtime.shell="pwsh"
1875
+ */
1876
+ const shell = createShellProxy("");
1877
+ //#endregion
1878
+ //#region src/artifact.ts
1879
+ /** Create an artifact output declaration with the given path. */
1880
+ function artifact(path) {
1881
+ return {
1882
+ type: "artifact",
1883
+ path
1884
+ };
1885
+ }
1886
+ //#endregion
1887
+ //#region src/pipeline.ts
1888
+ /** Create a Pipeline, run step functions, then entry functions. */
1889
+ function pipeline$1(project, id, config) {
1890
+ const p = new Pipeline(project, id, { ...config.inputs ? { inputs: config.inputs } : {} });
1891
+ if (config.steps) for (const stepFn of config.steps) stepFn(p);
1892
+ if (config.entries) for (const entryFn of config.entries) entryFn(p);
1893
+ return p;
1894
+ }
1895
+ //#endregion
1896
+ //#region src/call-pipeline.ts
1897
+ function createCallBuilder(state) {
1898
+ const builder = {
1899
+ outputs(outputs) {
1900
+ state.outputs = outputs;
1901
+ return builder;
1902
+ },
1903
+ dependsOn(steps) {
1904
+ state.dependsOn = steps;
1905
+ return builder;
1906
+ },
1907
+ runtime(runtime) {
1908
+ state.runtime = runtime;
1909
+ return builder;
1910
+ },
1911
+ timeout(ms) {
1912
+ state.timeout = ms;
1913
+ return builder;
1914
+ },
1915
+ condition(ref) {
1916
+ state.condition = ref;
1917
+ return builder;
1918
+ },
1919
+ interruptible(value) {
1920
+ state.interruptible = value;
1921
+ return builder;
1922
+ },
1923
+ build(pipeline, id) {
1924
+ return new PipelineCallStep(pipeline, id, {
1925
+ callee: state.callee,
1926
+ callInputs: state.callInputs,
1927
+ ...state.outputs ? { outputs: state.outputs } : {},
1928
+ ...state.dependsOn ? { dependsOn: state.dependsOn } : {},
1929
+ ...state.runtime ? { runtime: state.runtime } : {},
1930
+ ...state.timeout !== void 0 ? { timeout: state.timeout } : {},
1931
+ ...state.condition !== void 0 ? { condition: state.condition } : {},
1932
+ ...state.interruptible !== void 0 ? { interruptible: state.interruptible } : {}
1933
+ });
1934
+ }
1935
+ };
1936
+ return builder;
1937
+ }
1938
+ /**
1939
+ * Create a pipeline-call step builder.
1940
+ * @param callee The callee pipeline id (within the same Project).
1941
+ * @param callInputs Bound inputs for the callee pipeline.
1942
+ */
1943
+ function callPipeline(callee, callInputs) {
1944
+ return createCallBuilder({
1945
+ callee,
1946
+ callInputs: callInputs ? { ...callInputs } : {}
1947
+ });
1948
+ }
1949
+ //#endregion
1950
+ //#region src/component.ts
1951
+ function createComponentBuilder(state) {
1952
+ const builder = {
1953
+ outputs(outputs) {
1954
+ state.outputs = outputs;
1955
+ return builder;
1956
+ },
1957
+ dependsOn(steps) {
1958
+ state.dependsOn = steps;
1959
+ return builder;
1960
+ },
1961
+ runtime(runtime) {
1962
+ state.runtime = runtime;
1963
+ return builder;
1964
+ },
1965
+ timeout(ms) {
1966
+ state.timeout = ms;
1967
+ return builder;
1968
+ },
1969
+ condition(ref) {
1970
+ state.condition = ref;
1971
+ return builder;
1972
+ },
1973
+ interruptible(value) {
1974
+ state.interruptible = value;
1975
+ return builder;
1976
+ },
1977
+ build(pipeline, id) {
1978
+ return new ComponentStep(pipeline, id, {
1979
+ component: state.component,
1980
+ ...state.outputs ? { outputs: state.outputs } : {},
1981
+ ...state.dependsOn ? { dependsOn: state.dependsOn } : {},
1982
+ ...state.runtime ? { runtime: state.runtime } : {},
1983
+ ...state.timeout !== void 0 ? { timeout: state.timeout } : {},
1984
+ ...state.condition !== void 0 ? { condition: state.condition } : {},
1985
+ ...state.interruptible !== void 0 ? { interruptible: state.interruptible } : {}
1986
+ });
1987
+ }
1988
+ };
1989
+ return builder;
1990
+ }
1991
+ /**
1992
+ * Create a component step builder.
1993
+ * @param name The component name (e.g. "deploy").
1994
+ * @param version The component version (e.g. "1.0.0").
1995
+ * @param inputs Bound inputs for the component.
1996
+ */
1997
+ function component(name, version, inputs) {
1998
+ return createComponentBuilder({ component: {
1999
+ name,
2000
+ version,
2001
+ inputs: inputs ? { ...inputs } : {}
2002
+ } });
2003
+ }
2004
+ //#endregion
2005
+ //#region src/when.ts
2006
+ /** Mark a reference, expression, or status condition as a step condition. Returns it unchanged. */
2007
+ function when$1(cond) {
2008
+ return cond;
2009
+ }
2010
+ //#endregion
2011
+ //#region src/expr.ts
2012
+ /**
2013
+ * Create a symbolic expression from a tagged template.
2014
+ * Context references produce `${namespace.field}` placeholders while step
2015
+ * references produce `${step.output}` placeholders; both are collected into
2016
+ * `refs` for dependency inference.
2017
+ * String, number, and boolean values are inlined into the template.
2018
+ */
2019
+ function expr(strings, ...values) {
2020
+ let template = "";
2021
+ const refs = [];
2022
+ for (let i = 0; i < strings.length; i++) {
2023
+ template += strings[i];
2024
+ if (i < values.length) {
2025
+ const v = values[i];
2026
+ if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") template += String(v);
2027
+ else if (isReference(v)) {
2028
+ refs.push(v);
2029
+ if (v.kind === "step") template += `\${${v.step}.${v.output}}`;
2030
+ else template += `\${${v.namespace}.${v.field}}`;
2031
+ } else throw new SdkError(`invalid interpolation value at position ${i}: expected string, number, boolean, or Reference`, "INVALID_INTERPOLATION");
2032
+ }
2033
+ }
2034
+ return {
2035
+ kind: "expression",
2036
+ template,
2037
+ refs
2038
+ };
2039
+ }
2040
+ //#endregion
2041
+ //#region src/status.ts
2042
+ /**
2043
+ * Create a status-based condition.
2044
+ * @example
2045
+ * sh`notify-slack`.condition(status("failure"))
2046
+ */
2047
+ function status(s) {
2048
+ return {
2049
+ kind: "status",
2050
+ status: s
2051
+ };
2052
+ }
2053
+ //#endregion
2054
+ //#region src/internal/proxy-props.ts
2055
+ const PROTECTED_PROXY_PROPS = /* @__PURE__ */ new Set([
2056
+ "then",
2057
+ "toJSON",
2058
+ "inspect",
2059
+ "constructor",
2060
+ "valueOf",
2061
+ Symbol.toPrimitive,
2062
+ Symbol.iterator,
2063
+ Symbol.toStringTag
2064
+ ]);
2065
+ /**
2066
+ * Create a dynamic Proxy that delegates protected/symbol properties to
2067
+ * Reflect.get and calls `resolve(prop)` for everything else.
2068
+ *
2069
+ * This shared factory eliminates duplication between context namespace
2070
+ * proxies and image reference proxies.
2071
+ */
2072
+ function createDynamicProxy(resolve) {
2073
+ return new Proxy({}, { get(target, prop) {
2074
+ if (typeof prop === "symbol" || PROTECTED_PROXY_PROPS.has(prop)) return Reflect.get(target, prop);
2075
+ return resolve(prop);
2076
+ } });
2077
+ }
2078
+ //#endregion
2079
+ //#region src/images.ts
2080
+ /** Create an ImageRef from a raw OCI reference string. */
2081
+ function image(ref) {
2082
+ if (typeof ref !== "string") throw new SdkError("image reference must be a string", "INVALID_IMAGE");
2083
+ if (ref.length === 0) throw new SdkError("image reference must be a non-empty string", "INVALID_IMAGE");
2084
+ return { ref };
2085
+ }
2086
+ /** Create a Proxy that maps property access to `image(prefix:prop)`. */
2087
+ function createImageProxy(prefix) {
2088
+ return createDynamicProxy((prop) => image(`${prefix}:${prop}`));
2089
+ }
2090
+ const images = {
2091
+ node: createImageProxy("node"),
2092
+ ubuntu: createImageProxy("ubuntu")
2093
+ };
2094
+ //#endregion
2095
+ //#region src/context.ts
2096
+ /** Create a ContextRef for a namespace + field. */
2097
+ function ctx(namespace, field) {
2098
+ return {
2099
+ kind: "context",
2100
+ namespace,
2101
+ field
2102
+ };
2103
+ }
2104
+ /** Dynamic namespace proxy — any property access returns a ContextRef. */
2105
+ function dynamicNamespace(namespace) {
2106
+ return createDynamicProxy((prop) => ctx(namespace, prop));
2107
+ }
2108
+ /** Environment variable references: env.CI_TRACE, env.MY_VAR. */
2109
+ const env = dynamicNamespace("env");
2110
+ /** Secret references: secrets.NPM_TOKEN. */
2111
+ const secrets = dynamicNamespace("secrets");
2112
+ /** Git metadata references: git.sha, git.branch, git.tag. */
2113
+ const git = {
2114
+ sha: ctx("git", "sha"),
2115
+ branch: ctx("git", "branch"),
2116
+ tag: ctx("git", "tag")
2117
+ };
2118
+ /** Change request metadata: change.id, change.source, change.target, change.draft. */
2119
+ const change = {
2120
+ id: ctx("change", "id"),
2121
+ source: ctx("change", "source"),
2122
+ target: ctx("change", "target"),
2123
+ draft: ctx("change", "draft")
2124
+ };
2125
+ /** Event metadata: event.type. */
2126
+ const event = { type: ctx("event", "type") };
2127
+ /** Run metadata: run.id, run.attempt. */
2128
+ const run$1 = {
2129
+ id: ctx("run", "id"),
2130
+ attempt: ctx("run", "attempt")
2131
+ };
2132
+ /** Pipeline input references: inputs.environment. */
2133
+ const inputs = dynamicNamespace("inputs");
2134
+ /** Matrix variable references: matrix.node, matrix.os. */
2135
+ const matrix$1 = dynamicNamespace("matrix");
2136
+ //#endregion
2137
+ //#region src/agent.ts
2138
+ function createAgentBuilder(state) {
2139
+ const builder = {
2140
+ outputs(outputs) {
2141
+ state.outputs = outputs;
2142
+ return builder;
2143
+ },
2144
+ inputs(inputs) {
2145
+ state.inputs = inputs;
2146
+ return builder;
2147
+ },
2148
+ dependsOn(steps) {
2149
+ state.dependsOn = steps;
2150
+ return builder;
2151
+ },
2152
+ tools(...tools) {
2153
+ state.tools = [...state.tools, ...tools];
2154
+ return builder;
2155
+ },
2156
+ engine(engine) {
2157
+ state.engine = engine;
2158
+ return builder;
2159
+ },
2160
+ model(model) {
2161
+ state.model = model;
2162
+ return builder;
2163
+ },
2164
+ maxTokens(n) {
2165
+ state.maxTokens = n;
2166
+ return builder;
2167
+ },
2168
+ build(pipeline, id) {
2169
+ const allInputs = [...state.inputs ? [...state.inputs] : [], ...state.collectedInputs];
2170
+ return new AgentStep(pipeline, id, {
2171
+ engine: state.engine,
2172
+ prompt: state.prompt,
2173
+ ...state.model !== void 0 ? { model: state.model } : {},
2174
+ ...state.tools.length > 0 ? { tools: state.tools } : {},
2175
+ ...state.maxTokens !== void 0 ? { maxTokens: state.maxTokens } : {},
2176
+ ...state.outputs ? { outputs: state.outputs } : {},
2177
+ ...allInputs.length > 0 ? { inputs: allInputs } : {},
2178
+ ...state.dependsOn ? { dependsOn: state.dependsOn } : {}
2179
+ });
2180
+ }
2181
+ };
2182
+ return builder;
2183
+ }
2184
+ /**
2185
+ * Create an AI agent step builder from a tagged template.
2186
+ * `agent\`Build and test\`` creates a builder with `engine: "default"`.
2187
+ * References interpolated in the template are collected and added to the
2188
+ * step's inputs automatically (same as `$`).
2189
+ */
2190
+ function agent(strings, ...values) {
2191
+ let prompt = "";
2192
+ const collectedInputs = [];
2193
+ for (let i = 0; i < strings.length; i++) {
2194
+ prompt += strings[i];
2195
+ if (i < values.length) {
2196
+ const v = values[i];
2197
+ if (typeof v === "string") prompt += v;
2198
+ else if (isReference(v)) {
2199
+ const ref = v;
2200
+ collectedInputs.push(ref);
2201
+ if (ref.kind === "step") prompt += `\${${ref.step}.${ref.output}}`;
2202
+ else prompt += `\${${ref.namespace}.${ref.field}}`;
2203
+ } else throw new SdkError(`invalid interpolation value at position ${i}: expected string or Reference`, "INVALID_INTERPOLATION");
2204
+ }
2205
+ }
2206
+ return createAgentBuilder({
2207
+ prompt,
2208
+ engine: "default",
2209
+ tools: [],
2210
+ collectedInputs
2211
+ });
2212
+ }
2213
+ //#endregion
2214
+ export { $, CheckError, CompositionError, CoreError, DEFAULT_POLICY, DiscoveryError, PlannerError, PlanningError, SdkError, agent, artifact, bindRunPlan, callPipeline, change, component, computeFingerprint, computePlanId, computeReachableSteps, createBaseline, createBuiltinResolver, createPlanner, createPolicy, createSverka, defineWorkflow, env, evaluatePolicy, event, execute, expandMatrixSteps, expr, extractFindings, filterOnlyNew, findConfig, git, image, images, inputs, loadBaseline, loadWorkflow, matrix, matrix$1 as matrixContext, normalizeSarif, parallel, pipeline, pipeline$1 as pipelineV0, plan, run, run$1 as runContext, saveBaseline, secrets, shell, status, task, toPlan, updateBaseline, validatePlan, when, when$1 as whenV0, workflow };
2215
+
2216
+ //# sourceMappingURL=index.mjs.map