stageflow 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +56 -0
  3. package/dist/agent/activity.d.ts +76 -0
  4. package/dist/agent/activity.js +89 -0
  5. package/dist/agent/activityObserver.d.ts +17 -0
  6. package/dist/agent/activityObserver.js +66 -0
  7. package/dist/agent/cursorExtension.d.ts +12 -0
  8. package/dist/agent/cursorExtension.js +88 -0
  9. package/dist/agent/cursorProvider.d.ts +14 -0
  10. package/dist/agent/cursorProvider.js +139 -0
  11. package/dist/agent/fakeAgent.d.ts +29 -0
  12. package/dist/agent/fakeAgent.js +295 -0
  13. package/dist/agent/oauthSessionManager.d.ts +33 -0
  14. package/dist/agent/oauthSessionManager.js +214 -0
  15. package/dist/agent/piAdapter.d.ts +128 -0
  16. package/dist/agent/piAdapter.js +967 -0
  17. package/dist/agent/port.d.ts +63 -0
  18. package/dist/agent/port.js +46 -0
  19. package/dist/agent/providerAuth.d.ts +101 -0
  20. package/dist/agent/providerAuth.js +282 -0
  21. package/dist/agent/providerSupport.d.ts +28 -0
  22. package/dist/agent/providerSupport.js +19 -0
  23. package/dist/cli/providersCommand.d.ts +11 -0
  24. package/dist/cli/providersCommand.js +275 -0
  25. package/dist/cli/terminalAuthInteraction.d.ts +8 -0
  26. package/dist/cli/terminalAuthInteraction.js +65 -0
  27. package/dist/cli/terminalSecret.d.ts +2 -0
  28. package/dist/cli/terminalSecret.js +67 -0
  29. package/dist/cli/validateCommand.d.ts +11 -0
  30. package/dist/cli/validateCommand.js +81 -0
  31. package/dist/cli/validateOutput.d.ts +8 -0
  32. package/dist/cli/validateOutput.js +75 -0
  33. package/dist/cli.d.ts +2 -0
  34. package/dist/cli.js +273 -0
  35. package/dist/config/createPipeline.d.ts +31 -0
  36. package/dist/config/createPipeline.js +255 -0
  37. package/dist/config/createStage.d.ts +25 -0
  38. package/dist/config/createStage.js +181 -0
  39. package/dist/config/listConfig.d.ts +33 -0
  40. package/dist/config/listConfig.js +195 -0
  41. package/dist/config/listExtensions.d.ts +26 -0
  42. package/dist/config/listExtensions.js +50 -0
  43. package/dist/config/listSkills.d.ts +29 -0
  44. package/dist/config/listSkills.js +55 -0
  45. package/dist/config/loadOutcome.d.ts +27 -0
  46. package/dist/config/loadOutcome.js +6 -0
  47. package/dist/config/loadPipeline.d.ts +25 -0
  48. package/dist/config/loadPipeline.js +196 -0
  49. package/dist/config/loadStage.d.ts +4 -0
  50. package/dist/config/loadStage.js +127 -0
  51. package/dist/config/loadTask.d.ts +7 -0
  52. package/dist/config/loadTask.js +69 -0
  53. package/dist/config/readYamlObject.d.ts +1 -0
  54. package/dist/config/readYamlObject.js +5 -0
  55. package/dist/config/resolvePipelineDag.d.ts +12 -0
  56. package/dist/config/resolvePipelineDag.js +282 -0
  57. package/dist/config/validateCatalog.d.ts +36 -0
  58. package/dist/config/validateCatalog.js +322 -0
  59. package/dist/envelope/check.d.ts +3 -0
  60. package/dist/envelope/check.js +52 -0
  61. package/dist/envelope/payloadSchema.d.ts +10 -0
  62. package/dist/envelope/payloadSchema.js +91 -0
  63. package/dist/hitl/qaTrail.d.ts +31 -0
  64. package/dist/hitl/qaTrail.js +65 -0
  65. package/dist/index.d.ts +13 -0
  66. package/dist/index.js +7 -0
  67. package/dist/mcp/projectRun.d.ts +37 -0
  68. package/dist/mcp/projectRun.js +57 -0
  69. package/dist/mcp/readArtifact.d.ts +2 -0
  70. package/dist/mcp/readArtifact.js +46 -0
  71. package/dist/mcp/server.d.ts +7 -0
  72. package/dist/mcp/server.js +26 -0
  73. package/dist/mcp/tools.d.ts +9 -0
  74. package/dist/mcp/tools.js +96 -0
  75. package/dist/package-meta.d.ts +1 -0
  76. package/dist/package-meta.js +1 -0
  77. package/dist/prompt/priorEnvelope.d.ts +2 -0
  78. package/dist/prompt/priorEnvelope.js +5 -0
  79. package/dist/runstore/catalog.d.ts +8 -0
  80. package/dist/runstore/catalog.js +16 -0
  81. package/dist/runstore/createStore.d.ts +15 -0
  82. package/dist/runstore/createStore.js +23 -0
  83. package/dist/runstore/disk/DiskRunStore.d.ts +30 -0
  84. package/dist/runstore/disk/DiskRunStore.js +238 -0
  85. package/dist/runstore/disk/catalogHelpers.d.ts +11 -0
  86. package/dist/runstore/disk/catalogHelpers.js +102 -0
  87. package/dist/runstore/layout.d.ts +22 -0
  88. package/dist/runstore/layout.js +58 -0
  89. package/dist/runstore/paths.d.ts +7 -0
  90. package/dist/runstore/paths.js +60 -0
  91. package/dist/runstore/pipelineDagSnapshot.d.ts +5 -0
  92. package/dist/runstore/pipelineDagSnapshot.js +43 -0
  93. package/dist/runstore/port.d.ts +146 -0
  94. package/dist/runstore/port.js +28 -0
  95. package/dist/runstore/runProjection.d.ts +6 -0
  96. package/dist/runstore/runProjection.js +102 -0
  97. package/dist/runstore/sqlite/SqliteRunStore.d.ts +48 -0
  98. package/dist/runstore/sqlite/SqliteRunStore.js +460 -0
  99. package/dist/runstore/sqlite/migrateFromDisk.d.ts +5 -0
  100. package/dist/runstore/sqlite/migrateFromDisk.js +108 -0
  101. package/dist/runstore/sqlite/schema.d.ts +1 -0
  102. package/dist/runstore/sqlite/schema.js +59 -0
  103. package/dist/runstore/stageExecution.d.ts +13 -0
  104. package/dist/runstore/stageExecution.js +22 -0
  105. package/dist/runstore/stageSnapshot.d.ts +2 -0
  106. package/dist/runstore/stageSnapshot.js +46 -0
  107. package/dist/runstore/syntheticStageSnapshot.d.ts +2 -0
  108. package/dist/runstore/syntheticStageSnapshot.js +10 -0
  109. package/dist/runstore/trackProjection.d.ts +7 -0
  110. package/dist/runstore/trackProjection.js +94 -0
  111. package/dist/runstore/workspaceLayout.d.ts +31 -0
  112. package/dist/runstore/workspaceLayout.js +172 -0
  113. package/dist/runtime/answerResume.d.ts +16 -0
  114. package/dist/runtime/answerResume.js +22 -0
  115. package/dist/runtime/credentialBinding.d.ts +16 -0
  116. package/dist/runtime/credentialBinding.js +80 -0
  117. package/dist/runtime/dagTraversal.d.ts +3 -0
  118. package/dist/runtime/dagTraversal.js +26 -0
  119. package/dist/runtime/envelopeRouting.d.ts +20 -0
  120. package/dist/runtime/envelopeRouting.js +59 -0
  121. package/dist/runtime/hitlSeams.d.ts +38 -0
  122. package/dist/runtime/hitlSeams.js +2 -0
  123. package/dist/runtime/pipelineRunner.d.ts +77 -0
  124. package/dist/runtime/pipelineRunner.js +141 -0
  125. package/dist/runtime/pipelineScheduler.d.ts +90 -0
  126. package/dist/runtime/pipelineScheduler.js +494 -0
  127. package/dist/runtime/pipelineValidationError.d.ts +6 -0
  128. package/dist/runtime/pipelineValidationError.js +8 -0
  129. package/dist/runtime/resumeReconstruct.d.ts +33 -0
  130. package/dist/runtime/resumeReconstruct.js +144 -0
  131. package/dist/runtime/runManager.d.ts +129 -0
  132. package/dist/runtime/runManager.js +818 -0
  133. package/dist/runtime/runRetryCoordinator.d.ts +85 -0
  134. package/dist/runtime/runRetryCoordinator.js +372 -0
  135. package/dist/runtime/settingsFile.d.ts +17 -0
  136. package/dist/runtime/settingsFile.js +88 -0
  137. package/dist/runtime/stageAttemptBootstrap.d.ts +40 -0
  138. package/dist/runtime/stageAttemptBootstrap.js +82 -0
  139. package/dist/runtime/stageAttemptContext.d.ts +11 -0
  140. package/dist/runtime/stageAttemptContext.js +26 -0
  141. package/dist/runtime/stageConcurrency.d.ts +10 -0
  142. package/dist/runtime/stageConcurrency.js +51 -0
  143. package/dist/runtime/stageHitl.d.ts +95 -0
  144. package/dist/runtime/stageHitl.js +198 -0
  145. package/dist/runtime/stageProcessLauncher.d.ts +45 -0
  146. package/dist/runtime/stageProcessLauncher.js +199 -0
  147. package/dist/runtime/stageRecovery.d.ts +10 -0
  148. package/dist/runtime/stageRecovery.js +43 -0
  149. package/dist/runtime/stageRoots.d.ts +22 -0
  150. package/dist/runtime/stageRoots.js +93 -0
  151. package/dist/runtime/stageRunner.d.ts +59 -0
  152. package/dist/runtime/stageRunner.js +221 -0
  153. package/dist/runtime/stageWorker.d.ts +6 -0
  154. package/dist/runtime/stageWorker.js +109 -0
  155. package/dist/runtime/stageWorkerProtocol.d.ts +27 -0
  156. package/dist/runtime/stageWorkerProtocol.js +16 -0
  157. package/dist/runtime/taskInput.d.ts +15 -0
  158. package/dist/runtime/taskInput.js +38 -0
  159. package/dist/server/http.d.ts +31 -0
  160. package/dist/server/http.js +561 -0
  161. package/dist/server/providerRoutes.d.ts +15 -0
  162. package/dist/server/providerRoutes.js +186 -0
  163. package/dist/tools/askOperator.d.ts +185 -0
  164. package/dist/tools/askOperator.js +409 -0
  165. package/dist/tools/emitStageEnvelope.d.ts +42 -0
  166. package/dist/tools/emitStageEnvelope.js +61 -0
  167. package/dist/tools/writeStageArtifact.d.ts +26 -0
  168. package/dist/tools/writeStageArtifact.js +103 -0
  169. package/dist/types/envelope.d.ts +12 -0
  170. package/dist/types/envelope.js +6 -0
  171. package/dist/types/pipeline.d.ts +25 -0
  172. package/dist/types/pipeline.js +1 -0
  173. package/dist/types/stage.d.ts +12 -0
  174. package/dist/types/stage.js +6 -0
  175. package/dist/types/task.d.ts +7 -0
  176. package/dist/types/task.js +1 -0
  177. package/dist/ui/assets/index-Cry0Tpfx.js +118 -0
  178. package/dist/ui/assets/index-DCsDopak.css +1 -0
  179. package/dist/ui/index.html +21 -0
  180. package/package.json +72 -0
@@ -0,0 +1,282 @@
1
+ const ALLOWED_STAGE_REF_KEYS = new Set(["id", "needs"]);
2
+ function pipelineLabel(ctx) {
3
+ return `Pipeline ${ctx.pipelineId} (${ctx.path})`;
4
+ }
5
+ function formatError(ctx, message) {
6
+ return `${pipelineLabel(ctx)}: ${message}`;
7
+ }
8
+ function isPlainObject(value) {
9
+ return typeof value === "object" && value !== null && !Array.isArray(value);
10
+ }
11
+ export function parsePipelineStageEntries(raw, ctx) {
12
+ if (!Array.isArray(raw)) {
13
+ throw new Error(formatError(ctx, "stages[] is required"));
14
+ }
15
+ if (raw.length === 0) {
16
+ throw new Error(formatError(ctx, "stages must be non-empty"));
17
+ }
18
+ const entries = [];
19
+ for (let index = 0; index < raw.length; index++) {
20
+ const entry = raw[index];
21
+ if (typeof entry === "string") {
22
+ if (!entry) {
23
+ throw new Error(formatError(ctx, `invalid stage entry at index ${index}: stage id must be a non-empty string`));
24
+ }
25
+ entries.push(entry);
26
+ continue;
27
+ }
28
+ if (!isPlainObject(entry)) {
29
+ throw new Error(formatError(ctx, `invalid stage entry at index ${index}`));
30
+ }
31
+ const keys = Object.keys(entry);
32
+ for (const key of keys) {
33
+ if (!ALLOWED_STAGE_REF_KEYS.has(key)) {
34
+ throw new Error(formatError(ctx, `invalid stage entry "${String(entry.id ?? index)}": unknown key "${key}"`));
35
+ }
36
+ }
37
+ if (typeof entry.id !== "string" || !entry.id) {
38
+ throw new Error(formatError(ctx, `invalid stage entry at index ${index}: id must be a non-empty string`));
39
+ }
40
+ if (entry.needs === undefined) {
41
+ entries.push({ id: entry.id });
42
+ continue;
43
+ }
44
+ if (Array.isArray(entry.needs)) {
45
+ if (entry.needs.length > 1) {
46
+ throw new Error(formatError(ctx, `stage "${entry.id}": fan-in not supported; needs must be a single stage id`));
47
+ }
48
+ throw new Error(formatError(ctx, `stage "${entry.id}": needs must be a string, not an array`));
49
+ }
50
+ if (typeof entry.needs !== "string" || !entry.needs) {
51
+ throw new Error(formatError(ctx, `stage "${entry.id}": needs must be a non-empty string`));
52
+ }
53
+ entries.push({ id: entry.id, needs: entry.needs });
54
+ }
55
+ return entries;
56
+ }
57
+ function normalizeToEdges(entries) {
58
+ const edges = [];
59
+ for (let index = 0; index < entries.length; index++) {
60
+ const entry = entries[index];
61
+ if (typeof entry === "string") {
62
+ edges.push({
63
+ id: entry,
64
+ needs: index > 0 ? entries[index - 1] : null,
65
+ stageIndex: index,
66
+ });
67
+ continue;
68
+ }
69
+ edges.push({
70
+ id: entry.id,
71
+ needs: entry.needs ?? null,
72
+ stageIndex: index,
73
+ });
74
+ }
75
+ return edges;
76
+ }
77
+ function detectDuplicateIds(edges, ctx) {
78
+ const seen = new Set();
79
+ for (const edge of edges) {
80
+ if (seen.has(edge.id)) {
81
+ throw new Error(formatError(ctx, `duplicate stage "${edge.id}"`));
82
+ }
83
+ seen.add(edge.id);
84
+ }
85
+ }
86
+ function validateNeedsTargets(edges, ctx) {
87
+ const declared = new Set(edges.map((edge) => edge.id));
88
+ for (const edge of edges) {
89
+ if (edge.needs !== null && !declared.has(edge.needs)) {
90
+ throw new Error(formatError(ctx, `stage "${edge.id}" has unknown needs "${edge.needs}"`));
91
+ }
92
+ }
93
+ }
94
+ function detectCycle(edges, ctx) {
95
+ const indegree = new Map();
96
+ const children = new Map();
97
+ for (const edge of edges) {
98
+ indegree.set(edge.id, 0);
99
+ children.set(edge.id, []);
100
+ }
101
+ for (const edge of edges) {
102
+ if (edge.needs === null)
103
+ continue;
104
+ indegree.set(edge.id, (indegree.get(edge.id) ?? 0) + 1);
105
+ children.get(edge.needs)?.push(edge.id);
106
+ }
107
+ const queue = [];
108
+ for (const [id, degree] of indegree) {
109
+ if (degree === 0)
110
+ queue.push(id);
111
+ }
112
+ let visited = 0;
113
+ while (queue.length > 0) {
114
+ const current = queue.shift();
115
+ visited++;
116
+ for (const child of children.get(current) ?? []) {
117
+ const nextDegree = (indegree.get(child) ?? 0) - 1;
118
+ indegree.set(child, nextDegree);
119
+ if (nextDegree === 0)
120
+ queue.push(child);
121
+ }
122
+ }
123
+ if (visited !== edges.length) {
124
+ throw new Error(formatError(ctx, "dependency cycle detected"));
125
+ }
126
+ }
127
+ function computeAncestors(edges) {
128
+ const needsById = new Map(edges.map((edge) => [edge.id, edge.needs]));
129
+ const ancestorsById = new Map();
130
+ function ancestorsFor(id) {
131
+ const cached = ancestorsById.get(id);
132
+ if (cached)
133
+ return cached;
134
+ const needs = needsById.get(id) ?? null;
135
+ if (needs === null) {
136
+ ancestorsById.set(id, []);
137
+ return [];
138
+ }
139
+ const parentAncestors = ancestorsFor(needs);
140
+ const ancestors = [...parentAncestors, needs];
141
+ ancestorsById.set(id, ancestors);
142
+ return ancestors;
143
+ }
144
+ for (const edge of edges) {
145
+ ancestorsFor(edge.id);
146
+ }
147
+ return ancestorsById;
148
+ }
149
+ function topologicalSort(edges) {
150
+ const byId = new Map(edges.map((edge) => [edge.id, edge]));
151
+ const indegree = new Map();
152
+ const children = new Map();
153
+ for (const edge of edges) {
154
+ indegree.set(edge.id, 0);
155
+ children.set(edge.id, []);
156
+ }
157
+ for (const edge of edges) {
158
+ if (edge.needs === null)
159
+ continue;
160
+ indegree.set(edge.id, (indegree.get(edge.id) ?? 0) + 1);
161
+ children.get(edge.needs)?.push(edge.id);
162
+ }
163
+ for (const [parent, childIds] of children) {
164
+ childIds.sort((a, b) => (byId.get(a)?.stageIndex ?? 0) - (byId.get(b)?.stageIndex ?? 0));
165
+ }
166
+ const roots = edges
167
+ .filter((edge) => edge.needs === null)
168
+ .sort((a, b) => a.stageIndex - b.stageIndex)
169
+ .map((edge) => edge.id);
170
+ const sorted = [];
171
+ const queue = [...roots];
172
+ while (queue.length > 0) {
173
+ const current = queue.shift();
174
+ const edge = byId.get(current);
175
+ if (edge)
176
+ sorted.push(edge);
177
+ for (const child of children.get(current) ?? []) {
178
+ const nextDegree = (indegree.get(child) ?? 0) - 1;
179
+ indegree.set(child, nextDegree);
180
+ if (nextDegree === 0)
181
+ queue.push(child);
182
+ }
183
+ }
184
+ return sorted;
185
+ }
186
+ function buildResolvedPipelineDag(edges) {
187
+ const ancestorsById = computeAncestors(edges);
188
+ const sortedEdges = topologicalSort(edges);
189
+ const childrenOf = {};
190
+ for (const edge of edges) {
191
+ childrenOf[edge.id] = [];
192
+ }
193
+ for (const edge of edges) {
194
+ if (edge.needs !== null) {
195
+ childrenOf[edge.needs].push(edge.id);
196
+ }
197
+ }
198
+ for (const id of Object.keys(childrenOf)) {
199
+ childrenOf[id].sort((a, b) => (edges.find((edge) => edge.id === a)?.stageIndex ?? 0) -
200
+ (edges.find((edge) => edge.id === b)?.stageIndex ?? 0));
201
+ }
202
+ const roots = edges
203
+ .filter((edge) => edge.needs === null)
204
+ .sort((a, b) => a.stageIndex - b.stageIndex)
205
+ .map((edge) => edge.id);
206
+ const nodes = sortedEdges.map((edge) => ({
207
+ id: edge.id,
208
+ needs: edge.needs,
209
+ ancestors: ancestorsById.get(edge.id) ?? [],
210
+ stageIndex: edge.stageIndex,
211
+ }));
212
+ return { nodes, roots, childrenOf };
213
+ }
214
+ export function resolvePipelineDag(rawStages, ctx) {
215
+ const entries = parsePipelineStageEntries(rawStages, ctx);
216
+ const edges = normalizeToEdges(entries);
217
+ detectDuplicateIds(edges, ctx);
218
+ validateNeedsTargets(edges, ctx);
219
+ detectCycle(edges, ctx);
220
+ const stages = edges
221
+ .slice()
222
+ .sort((a, b) => a.stageIndex - b.stageIndex)
223
+ .map((edge) => edge.id);
224
+ const dag = buildResolvedPipelineDag(edges);
225
+ return { stages, dag };
226
+ }
227
+ export function areResolvedDagsEquivalent(a, b) {
228
+ const nodeIdsA = new Set(a.nodes.map((node) => node.id));
229
+ const nodeIdsB = new Set(b.nodes.map((node) => node.id));
230
+ if (nodeIdsA.size !== nodeIdsB.size)
231
+ return false;
232
+ for (const id of nodeIdsA) {
233
+ if (!nodeIdsB.has(id))
234
+ return false;
235
+ }
236
+ const byIdA = new Map(a.nodes.map((node) => [node.id, node]));
237
+ const byIdB = new Map(b.nodes.map((node) => [node.id, node]));
238
+ for (const id of nodeIdsA) {
239
+ const nodeA = byIdA.get(id);
240
+ const nodeB = byIdB.get(id);
241
+ if (nodeA.needs !== nodeB.needs)
242
+ return false;
243
+ if (nodeA.ancestors.length !== nodeB.ancestors.length)
244
+ return false;
245
+ for (let i = 0; i < nodeA.ancestors.length; i++) {
246
+ if (nodeA.ancestors[i] !== nodeB.ancestors[i])
247
+ return false;
248
+ }
249
+ }
250
+ return true;
251
+ }
252
+ export function extractPipelineStageIds(rawStages) {
253
+ if (!Array.isArray(rawStages) || rawStages.length === 0) {
254
+ return null;
255
+ }
256
+ const stageIds = [];
257
+ for (const entry of rawStages) {
258
+ if (typeof entry === "string") {
259
+ if (!entry)
260
+ return null;
261
+ stageIds.push(entry);
262
+ continue;
263
+ }
264
+ if (!isPlainObject(entry))
265
+ return null;
266
+ const keys = Object.keys(entry);
267
+ for (const key of keys) {
268
+ if (!ALLOWED_STAGE_REF_KEYS.has(key))
269
+ return null;
270
+ }
271
+ if (typeof entry.id !== "string" || !entry.id)
272
+ return null;
273
+ if (entry.needs !== undefined) {
274
+ if (Array.isArray(entry.needs))
275
+ return null;
276
+ if (typeof entry.needs !== "string" || !entry.needs)
277
+ return null;
278
+ }
279
+ stageIds.push(entry.id);
280
+ }
281
+ return stageIds;
282
+ }
@@ -0,0 +1,36 @@
1
+ import type { LoadIssue } from "./loadOutcome.js";
2
+ export type ValidationSeverity = "error" | "warning";
3
+ export type ValidationCategory = "pipeline" | "stage" | "catalog";
4
+ export type ValidationFindingCode = "pipeline.invalid_shape" | "pipeline.dag_error" | "pipeline.missing_stage" | "pipeline.load_error" | "stage.invalid_shape" | "stage.invalid_payload_schema" | "stage.invalid_gate_kinds" | "stage.invalid_skill" | "stage.load_error" | "stage.id_filename_mismatch" | "catalog.duplicate_pipeline_id" | "catalog.orphan_stage";
5
+ export type ValidationFinding = {
6
+ severity: ValidationSeverity;
7
+ code: ValidationFindingCode | string;
8
+ path: string;
9
+ message: string;
10
+ category: ValidationCategory;
11
+ pipelineId?: string;
12
+ stageId?: string;
13
+ };
14
+ export type ValidationScope = "full" | "pipeline";
15
+ export type ValidateCatalogOptions = {
16
+ cwd?: string;
17
+ stagesDir?: string;
18
+ scope: ValidationScope;
19
+ pipeline?: string;
20
+ strict?: boolean;
21
+ };
22
+ export type ValidationResult = {
23
+ scope: ValidationScope;
24
+ ok: boolean;
25
+ summary: {
26
+ errors: number;
27
+ warnings: number;
28
+ };
29
+ findings: ValidationFinding[];
30
+ };
31
+ export declare function relPath(cwd: string, absPath: string): string;
32
+ export declare function effectiveSeverity(finding: ValidationFinding, strict: boolean): ValidationSeverity;
33
+ export declare function buildValidationResult(scope: ValidationScope, findings: ValidationFinding[], strict: boolean): ValidationResult;
34
+ export declare function findingsFromLoadIssues(cwd: string, absPath: string, issues: LoadIssue[]): ValidationFinding[];
35
+ export declare function findingStageIdFilenameMismatch(cwd: string, absPath: string, fileStem: string, declaredId: string): ValidationFinding;
36
+ export declare function validateCatalog(options: ValidateCatalogOptions): Promise<ValidationResult>;
@@ -0,0 +1,322 @@
1
+ import { readdir } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { loadPipelineOutcome, loadPipelineValidated } from "./loadPipeline.js";
4
+ import { loadStageOutcome } from "./loadStage.js";
5
+ import { readYamlObject } from "./readYamlObject.js";
6
+ import { extractPipelineStageIds } from "./resolvePipelineDag.js";
7
+ export function relPath(cwd, absPath) {
8
+ return path.relative(cwd, absPath);
9
+ }
10
+ const SEVERITY_RANK = {
11
+ error: 0,
12
+ warning: 1,
13
+ };
14
+ function sortFindings(findings) {
15
+ return [...findings].sort((a, b) => {
16
+ const pathCmp = a.path.localeCompare(b.path);
17
+ if (pathCmp !== 0)
18
+ return pathCmp;
19
+ const sevCmp = SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity];
20
+ if (sevCmp !== 0)
21
+ return sevCmp;
22
+ const codeCmp = a.code.localeCompare(b.code);
23
+ if (codeCmp !== 0)
24
+ return codeCmp;
25
+ return a.message.localeCompare(b.message);
26
+ });
27
+ }
28
+ export function effectiveSeverity(finding, strict) {
29
+ if (strict &&
30
+ finding.severity === "warning" &&
31
+ finding.code === "catalog.orphan_stage") {
32
+ return "error";
33
+ }
34
+ return finding.severity;
35
+ }
36
+ function summarizeFindings(findings, strict) {
37
+ let errors = 0;
38
+ let warnings = 0;
39
+ for (const finding of findings) {
40
+ const severity = effectiveSeverity(finding, strict);
41
+ if (severity === "error")
42
+ errors++;
43
+ else
44
+ warnings++;
45
+ }
46
+ return { errors, warnings };
47
+ }
48
+ export function buildValidationResult(scope, findings, strict) {
49
+ const sorted = sortFindings(findings);
50
+ const summary = summarizeFindings(sorted, strict);
51
+ const ok = summary.errors === 0;
52
+ return { scope, ok, summary, findings: sorted };
53
+ }
54
+ function baseFinding(fields, severity) {
55
+ return {
56
+ severity,
57
+ code: fields.code,
58
+ path: relPath(fields.cwd, fields.absPath),
59
+ message: fields.message,
60
+ category: fields.category,
61
+ ...(fields.pipelineId !== undefined ? { pipelineId: fields.pipelineId } : {}),
62
+ ...(fields.stageId !== undefined ? { stageId: fields.stageId } : {}),
63
+ };
64
+ }
65
+ function findingPipelineError(cwd, absPath, message, code, pipelineId) {
66
+ return baseFinding({
67
+ cwd,
68
+ absPath,
69
+ message,
70
+ code,
71
+ category: "pipeline",
72
+ pipelineId,
73
+ }, "error");
74
+ }
75
+ function findingStageError(cwd, absPath, message, code, stageId) {
76
+ return baseFinding({
77
+ cwd,
78
+ absPath,
79
+ message,
80
+ code,
81
+ category: "stage",
82
+ stageId,
83
+ }, "error");
84
+ }
85
+ export function findingsFromLoadIssues(cwd, absPath, issues) {
86
+ return issues.flatMap((issue) => {
87
+ if (issue.category === "pipeline") {
88
+ return [
89
+ findingPipelineError(cwd, absPath, issue.message, issue.code, issue.pipelineId),
90
+ ];
91
+ }
92
+ if (issue.category === "stage") {
93
+ return [
94
+ findingStageError(cwd, absPath, issue.message, issue.code, issue.stageId),
95
+ ];
96
+ }
97
+ return [];
98
+ });
99
+ }
100
+ export function findingStageIdFilenameMismatch(cwd, absPath, fileStem, declaredId) {
101
+ return baseFinding({
102
+ cwd,
103
+ absPath,
104
+ message: `Stage id "${declaredId}" does not match filename stem "${fileStem}"`,
105
+ code: "stage.id_filename_mismatch",
106
+ category: "stage",
107
+ stageId: declaredId,
108
+ }, "error");
109
+ }
110
+ function findingCatalogDuplicatePipelineId(cwd, absPath, pipelineId, conflictingPaths) {
111
+ const relConflicts = conflictingPaths.map((p) => relPath(cwd, p));
112
+ return baseFinding({
113
+ cwd,
114
+ absPath,
115
+ message: `Duplicate pipeline id "${pipelineId}" declared in: ${relConflicts.join(", ")}`,
116
+ code: "catalog.duplicate_pipeline_id",
117
+ category: "catalog",
118
+ pipelineId,
119
+ }, "error");
120
+ }
121
+ function findingCatalogOrphanStage(cwd, absPath, stageId) {
122
+ return baseFinding({
123
+ cwd,
124
+ absPath,
125
+ message: `Stage "${stageId}" is not referenced by any pipeline`,
126
+ code: "catalog.orphan_stage",
127
+ category: "catalog",
128
+ stageId,
129
+ }, "warning");
130
+ }
131
+ async function listYamlFiles(dir) {
132
+ let entries;
133
+ try {
134
+ entries = await readdir(dir, { withFileTypes: true });
135
+ }
136
+ catch {
137
+ return [];
138
+ }
139
+ return entries
140
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".yaml"))
141
+ .map((entry) => path.join(dir, entry.name))
142
+ .sort((a, b) => a.localeCompare(b));
143
+ }
144
+ async function checkStageIdFilename(cwd, stagePath) {
145
+ const fileStem = path.basename(stagePath, ".yaml");
146
+ try {
147
+ const raw = await readYamlObject(stagePath);
148
+ if (typeof raw?.id === "string" && raw.id !== fileStem) {
149
+ return findingStageIdFilenameMismatch(cwd, stagePath, fileStem, raw.id);
150
+ }
151
+ }
152
+ catch {
153
+ // loadStageOutcome will report read errors
154
+ }
155
+ return null;
156
+ }
157
+ async function validateStageFile(cwd, stagePath) {
158
+ const findings = [];
159
+ const mismatch = await checkStageIdFilename(cwd, stagePath);
160
+ if (mismatch)
161
+ findings.push(mismatch);
162
+ const outcome = await loadStageOutcome(stagePath);
163
+ if (!outcome.ok) {
164
+ findings.push(...findingsFromLoadIssues(cwd, stagePath, outcome.issues));
165
+ return findings;
166
+ }
167
+ if (!mismatch && path.basename(stagePath, ".yaml") !== outcome.value.id) {
168
+ findings.push(findingStageIdFilenameMismatch(cwd, stagePath, path.basename(stagePath, ".yaml"), outcome.value.id));
169
+ }
170
+ return findings;
171
+ }
172
+ async function validatePipelineFile(pipelinePath, options) {
173
+ const { cwd, stagesDir, validateStages } = options;
174
+ const findings = [];
175
+ const referencedStageIds = new Set();
176
+ let pipelineId;
177
+ try {
178
+ const raw = await readYamlObject(pipelinePath);
179
+ if (typeof raw?.id === "string") {
180
+ pipelineId = raw.id;
181
+ if (Array.isArray(raw.stages)) {
182
+ const stageIds = extractPipelineStageIds(raw.stages);
183
+ if (stageIds) {
184
+ for (const stageId of stageIds) {
185
+ referencedStageIds.add(stageId);
186
+ }
187
+ }
188
+ }
189
+ }
190
+ }
191
+ catch {
192
+ // loadPipelineOutcome will report read errors
193
+ }
194
+ const outcome = await loadPipelineOutcome(pipelinePath, { cwd, stagesDir });
195
+ if (!outcome.ok) {
196
+ findings.push(...findingsFromLoadIssues(cwd, pipelinePath, outcome.issues));
197
+ if (validateStages) {
198
+ for (const stageId of referencedStageIds) {
199
+ const stagePath = path.join(stagesDir, `${stageId}.yaml`);
200
+ findings.push(...(await validateStageFile(cwd, stagePath)));
201
+ }
202
+ }
203
+ return { findings, pipelineId, referencedStageIds };
204
+ }
205
+ for (const stageId of outcome.value.pipeline.stages) {
206
+ referencedStageIds.add(stageId);
207
+ if (validateStages) {
208
+ const stagePath = path.join(stagesDir, `${stageId}.yaml`);
209
+ findings.push(...(await validateStageFile(cwd, stagePath)));
210
+ }
211
+ }
212
+ return { findings, pipelineId: outcome.value.pipeline.id, referencedStageIds };
213
+ }
214
+ async function collectPipelineIds(pipelinesDir) {
215
+ const files = await listYamlFiles(pipelinesDir);
216
+ const idToPaths = new Map();
217
+ for (const filePath of files) {
218
+ try {
219
+ const raw = await readYamlObject(filePath);
220
+ if (typeof raw?.id !== "string")
221
+ continue;
222
+ const existing = idToPaths.get(raw.id) ?? [];
223
+ existing.push(filePath);
224
+ idToPaths.set(raw.id, existing);
225
+ }
226
+ catch {
227
+ // unreadable files handled in pipeline validation loop
228
+ }
229
+ }
230
+ return idToPaths;
231
+ }
232
+ function findingsForDuplicatePipelineIds(cwd, idToPaths) {
233
+ const findings = [];
234
+ for (const [pipelineId, paths] of idToPaths) {
235
+ if (paths.length <= 1)
236
+ continue;
237
+ for (const filePath of paths) {
238
+ findings.push(findingCatalogDuplicatePipelineId(cwd, filePath, pipelineId, paths));
239
+ }
240
+ }
241
+ return findings;
242
+ }
243
+ async function collectReferencedStageIds(pipelinesDir) {
244
+ const files = await listYamlFiles(pipelinesDir);
245
+ const referenced = new Set();
246
+ for (const filePath of files) {
247
+ try {
248
+ const raw = await readYamlObject(filePath);
249
+ if (typeof raw?.id !== "string" || !Array.isArray(raw.stages))
250
+ continue;
251
+ const stageIds = extractPipelineStageIds(raw.stages);
252
+ if (!stageIds)
253
+ continue;
254
+ for (const stageId of stageIds) {
255
+ referenced.add(stageId);
256
+ }
257
+ }
258
+ catch {
259
+ // skip unreadable
260
+ }
261
+ }
262
+ return referenced;
263
+ }
264
+ async function findingsForOrphanStages(cwd, stagesDir, referencedStageIds) {
265
+ const findings = [];
266
+ const stageFiles = await listYamlFiles(stagesDir);
267
+ for (const stagePath of stageFiles) {
268
+ const stageId = path.basename(stagePath, ".yaml");
269
+ if (referencedStageIds.has(stageId))
270
+ continue;
271
+ findings.push(findingCatalogOrphanStage(cwd, stagePath, stageId));
272
+ findings.push(...(await validateStageFile(cwd, stagePath)));
273
+ }
274
+ return findings;
275
+ }
276
+ export async function validateCatalog(options) {
277
+ const cwd = options.cwd ?? process.cwd();
278
+ const stagesDir = options.stagesDir ?? path.join(cwd, "stages");
279
+ const strict = options.strict ?? false;
280
+ const pipelinesDir = path.join(cwd, "pipelines");
281
+ if (options.scope === "pipeline" && !options.pipeline) {
282
+ throw new Error("validateCatalog: pipeline is required when scope is \"pipeline\"");
283
+ }
284
+ const allFindings = [];
285
+ if (options.scope === "pipeline") {
286
+ const result = await loadPipelineValidated(options.pipeline, {
287
+ cwd,
288
+ stagesDir,
289
+ validateStages: true,
290
+ });
291
+ if (!result.ok) {
292
+ allFindings.push(...result.findings);
293
+ }
294
+ }
295
+ else {
296
+ const referencedStageIds = await collectReferencedStageIds(pipelinesDir);
297
+ const pipelineFiles = await listYamlFiles(pipelinesDir);
298
+ for (const pipelinePath of pipelineFiles) {
299
+ const result = await validatePipelineFile(pipelinePath, {
300
+ cwd,
301
+ stagesDir,
302
+ validateStages: false,
303
+ });
304
+ allFindings.push(...result.findings);
305
+ for (const stageId of result.referencedStageIds) {
306
+ referencedStageIds.add(stageId);
307
+ }
308
+ }
309
+ const validatedStages = new Set();
310
+ for (const stageId of referencedStageIds) {
311
+ if (validatedStages.has(stageId))
312
+ continue;
313
+ validatedStages.add(stageId);
314
+ const stagePath = path.join(stagesDir, `${stageId}.yaml`);
315
+ allFindings.push(...(await validateStageFile(cwd, stagePath)));
316
+ }
317
+ const idToPaths = await collectPipelineIds(pipelinesDir);
318
+ allFindings.push(...findingsForDuplicatePipelineIds(cwd, idToPaths));
319
+ allFindings.push(...(await findingsForOrphanStages(cwd, stagesDir, referencedStageIds)));
320
+ }
321
+ return buildValidationResult(options.scope, allFindings, strict);
322
+ }
@@ -0,0 +1,3 @@
1
+ import { type StageEnvelope } from "../types/envelope.js";
2
+ export declare function assertRequiredEnvelope(value: unknown): StageEnvelope;
3
+ export declare function isAdvancingEnvelope(envelope: StageEnvelope): boolean;
@@ -0,0 +1,52 @@
1
+ import { EnvelopeError, } from "../types/envelope.js";
2
+ export function assertRequiredEnvelope(value) {
3
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
4
+ throw new EnvelopeError("envelope must be an object");
5
+ }
6
+ const record = value;
7
+ const status = record.status;
8
+ if (status !== "success" && status !== "failure") {
9
+ throw new EnvelopeError('status must be "success" or "failure"');
10
+ }
11
+ if (typeof record.summary !== "string" || record.summary.trim() === "") {
12
+ throw new EnvelopeError("summary must be a non-empty string");
13
+ }
14
+ if (!("artifacts" in record)) {
15
+ throw new EnvelopeError("artifacts field is required");
16
+ }
17
+ if (!Array.isArray(record.artifacts)) {
18
+ throw new EnvelopeError("artifacts must be an array");
19
+ }
20
+ if (!record.artifacts.every((item) => typeof item === "string")) {
21
+ throw new EnvelopeError("artifacts must be an array of strings");
22
+ }
23
+ const envelope = {
24
+ status,
25
+ summary: record.summary,
26
+ artifacts: record.artifacts,
27
+ };
28
+ if (record.payload !== undefined) {
29
+ if (record.payload === null ||
30
+ typeof record.payload !== "object" ||
31
+ Array.isArray(record.payload)) {
32
+ throw new EnvelopeError("payload must be an object when present");
33
+ }
34
+ envelope.payload = record.payload;
35
+ }
36
+ if (record.stage_id !== undefined) {
37
+ if (typeof record.stage_id !== "string") {
38
+ throw new EnvelopeError("stage_id must be a string when present");
39
+ }
40
+ envelope.stage_id = record.stage_id;
41
+ }
42
+ if (record.notes !== undefined) {
43
+ if (typeof record.notes !== "string") {
44
+ throw new EnvelopeError("notes must be a string when present");
45
+ }
46
+ envelope.notes = record.notes;
47
+ }
48
+ return envelope;
49
+ }
50
+ export function isAdvancingEnvelope(envelope) {
51
+ return envelope.status === "success";
52
+ }