stageflow 0.2.0 → 0.3.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 (113) hide show
  1. package/README.md +158 -14
  2. package/dist/agent/activity.d.ts +10 -1
  3. package/dist/agent/activity.js +37 -2
  4. package/dist/agent/activityObserver.d.ts +5 -1
  5. package/dist/agent/activityObserver.js +174 -11
  6. package/dist/agent/fakeAgent.js +4 -0
  7. package/dist/agent/piAdapter.d.ts +16 -0
  8. package/dist/agent/piAdapter.js +88 -21
  9. package/dist/agent/port.d.ts +2 -0
  10. package/dist/agent/providerAuth.js +12 -7
  11. package/dist/catalog/displayCatalogPath.d.ts +12 -0
  12. package/dist/catalog/displayCatalogPath.js +28 -0
  13. package/dist/cli/initCommand.d.ts +9 -0
  14. package/dist/cli/initCommand.js +71 -0
  15. package/dist/cli/initTemplates.d.ts +3 -0
  16. package/dist/cli/initTemplates.js +19 -0
  17. package/dist/cli/operatorCatalog.d.ts +10 -0
  18. package/dist/cli/operatorCatalog.js +16 -0
  19. package/dist/cli/runCommand.d.ts +3 -1
  20. package/dist/cli/runCommand.js +48 -7
  21. package/dist/cli/validateCommand.d.ts +1 -1
  22. package/dist/cli/validateCommand.js +20 -3
  23. package/dist/cli.d.ts +1 -0
  24. package/dist/cli.js +52 -19
  25. package/dist/config/browseCatalog.d.ts +45 -0
  26. package/dist/config/browseCatalog.js +170 -0
  27. package/dist/config/createPipeline.d.ts +12 -3
  28. package/dist/config/createPipeline.js +292 -100
  29. package/dist/config/createStage.d.ts +10 -4
  30. package/dist/config/createStage.js +44 -14
  31. package/dist/config/listConfig.d.ts +14 -19
  32. package/dist/config/listConfig.js +26 -191
  33. package/dist/config/listModelsFromManifest.d.ts +2 -0
  34. package/dist/config/listModelsFromManifest.js +4 -0
  35. package/dist/config/loadOutcome.d.ts +4 -0
  36. package/dist/config/loadPipeline.d.ts +1 -15
  37. package/dist/config/loadPipeline.js +84 -142
  38. package/dist/config/loadStage.d.ts +4 -0
  39. package/dist/config/loadStage.js +60 -32
  40. package/dist/config/loadStageflowManifest.d.ts +5 -0
  41. package/dist/config/loadStageflowManifest.js +157 -0
  42. package/dist/config/mergePipelineIncludes.d.ts +9 -0
  43. package/dist/config/mergePipelineIncludes.js +141 -0
  44. package/dist/config/normalizePipelineStageEntry.d.ts +16 -0
  45. package/dist/config/normalizePipelineStageEntry.js +186 -0
  46. package/dist/config/pipelineStageKeys.d.ts +4 -0
  47. package/dist/config/pipelineStageKeys.js +14 -0
  48. package/dist/config/resolveCatalogContext.d.ts +13 -0
  49. package/dist/config/resolveCatalogContext.js +13 -0
  50. package/dist/config/resolveForkEmitContext.d.ts +3 -0
  51. package/dist/config/resolveForkEmitContext.js +12 -0
  52. package/dist/config/resolvePipelineDag.d.ts +5 -1
  53. package/dist/config/resolvePipelineDag.js +54 -36
  54. package/dist/config/scanCatalogPaths.d.ts +4 -0
  55. package/dist/config/scanCatalogPaths.js +83 -0
  56. package/dist/config/validateCatalog.d.ts +22 -4
  57. package/dist/config/validateCatalog.js +169 -132
  58. package/dist/envelope/check.js +7 -0
  59. package/dist/envelope/forkChoice.d.ts +3 -0
  60. package/dist/envelope/forkChoice.js +47 -0
  61. package/dist/index.d.ts +4 -0
  62. package/dist/index.js +4 -0
  63. package/dist/mcp/projectRun.d.ts +3 -0
  64. package/dist/mcp/projectRun.js +7 -0
  65. package/dist/mcp/tools.js +28 -11
  66. package/dist/project/findProjectRoot.d.ts +2 -0
  67. package/dist/project/findProjectRoot.js +51 -0
  68. package/dist/project/globalHome.d.ts +2 -0
  69. package/dist/project/globalHome.js +19 -0
  70. package/dist/project/resolveProjectContext.d.ts +9 -0
  71. package/dist/project/resolveProjectContext.js +38 -0
  72. package/dist/project/resolveStageflowContext.d.ts +11 -0
  73. package/dist/project/resolveStageflowContext.js +66 -0
  74. package/dist/runstore/normalizeCatalogPath.d.ts +1 -0
  75. package/dist/runstore/normalizeCatalogPath.js +4 -0
  76. package/dist/runstore/port.d.ts +9 -0
  77. package/dist/runstore/runProjection.js +11 -1
  78. package/dist/runstore/sqlite/SqliteRunStore.js +30 -4
  79. package/dist/runstore/sqlite/schema.d.ts +1 -1
  80. package/dist/runstore/sqlite/schema.js +4 -1
  81. package/dist/runtime/credentialBinding.d.ts +8 -6
  82. package/dist/runtime/credentialBinding.js +18 -24
  83. package/dist/runtime/pipelineRunner.d.ts +3 -0
  84. package/dist/runtime/pipelineRunner.js +15 -1
  85. package/dist/runtime/pipelineScheduler.d.ts +2 -0
  86. package/dist/runtime/pipelineScheduler.js +51 -11
  87. package/dist/runtime/reloadRunCatalog.d.ts +5 -0
  88. package/dist/runtime/reloadRunCatalog.js +44 -0
  89. package/dist/runtime/resumeReconstruct.d.ts +1 -0
  90. package/dist/runtime/resumeReconstruct.js +23 -7
  91. package/dist/runtime/runManager.d.ts +4 -0
  92. package/dist/runtime/runManager.js +36 -16
  93. package/dist/runtime/settingsFile.d.ts +12 -0
  94. package/dist/runtime/settingsFile.js +87 -6
  95. package/dist/runtime/stageAttemptBootstrap.js +3 -0
  96. package/dist/runtime/stageProcessLauncher.js +3 -0
  97. package/dist/runtime/stageRecovery.js +3 -1
  98. package/dist/runtime/stageWorker.js +2 -6
  99. package/dist/server/http.js +33 -9
  100. package/dist/tools/emitStageEnvelope.d.ts +3 -1
  101. package/dist/tools/emitStageEnvelope.js +11 -1
  102. package/dist/types/envelope.d.ts +1 -0
  103. package/dist/types/forkChoice.d.ts +8 -0
  104. package/dist/types/forkChoice.js +1 -0
  105. package/dist/types/pipeline.d.ts +51 -2
  106. package/dist/types/stageflowManifest.d.ts +23 -0
  107. package/dist/types/stageflowManifest.js +1 -0
  108. package/dist/ui/assets/index-CFSzDZje.js +118 -0
  109. package/dist/ui/assets/{index-DCsDopak.css → index-DefBlEvN.css} +1 -1
  110. package/dist/ui/index.html +3 -2
  111. package/dist/ui/stageflow-icon.svg +12 -0
  112. package/package.json +7 -5
  113. package/dist/ui/assets/index-Cry0Tpfx.js +0 -118
@@ -1,18 +1,9 @@
1
- import { type StageGateKind } from "../types/stage.js";
2
- export type TaskListing = {
3
- path: string;
4
- id: string;
5
- goal: string;
6
- };
7
- export type PipelineStageListing = {
8
- id: string;
9
- gate_kinds?: StageGateKind[];
10
- };
11
- export type PipelineListing = {
12
- path: string;
13
- id: string;
14
- stages: PipelineStageListing[];
15
- };
1
+ import type { StageGateKind } from "../types/stage.js";
2
+ import type { LoadedManifest } from "../types/stageflowManifest.js";
3
+ import { type PipelineListing, type TaskListing } from "./browseCatalog.js";
4
+ import { type StageflowContext } from "../project/resolveStageflowContext.js";
5
+ import { type CatalogContext } from "./resolveCatalogContext.js";
6
+ export type { PipelineListing, PipelineStageListing, TaskListing } from "./browseCatalog.js";
16
7
  export type ValidStageListing = {
17
8
  path: string;
18
9
  id: string;
@@ -27,7 +18,11 @@ export type BrokenStageListing = {
27
18
  gate_kinds?: StageGateKind[];
28
19
  };
29
20
  export type StageListing = ValidStageListing | BrokenStageListing;
30
- export declare function listTasks(cwd: string): Promise<TaskListing[]>;
31
- export declare function listPipelines(cwd: string): Promise<PipelineListing[]>;
32
- export declare function listStages(cwd: string): Promise<StageListing[]>;
33
- export declare function listModels(cwd: string): Promise<string[]>;
21
+ export type CatalogListOptions = {
22
+ projectRoot: string;
23
+ manifest: LoadedManifest;
24
+ };
25
+ export declare function listTasks(options: CatalogListOptions): Promise<TaskListing[]>;
26
+ export declare function listPipelines(options: CatalogListOptions): Promise<PipelineListing[]>;
27
+ export declare function listStages(_options?: unknown): Promise<StageListing[]>;
28
+ export declare function listModels(cwdOrContext: string | CatalogContext | StageflowContext): Promise<string[]>;
@@ -1,195 +1,30 @@
1
- import { readdir } from "node:fs/promises";
2
- import path from "node:path";
3
- import { STAGE_GATE_KINDS, } from "../types/stage.js";
4
- import { loadPipeline } from "./loadPipeline.js";
5
- import { loadStage } from "./loadStage.js";
6
- import { loadTask } from "./loadTask.js";
7
- import { readYamlObject } from "./readYamlObject.js";
8
- import { extractPipelineStageIds } from "./resolvePipelineDag.js";
9
- const BAKED_MODEL_IDS = [
10
- "anthropic/claude-sonnet-4-5",
11
- "cursor/auto",
12
- "cursor/composer-2-5",
13
- ];
14
- function parseSafeGateKinds(raw) {
15
- if (!Array.isArray(raw) || !raw.every((item) => typeof item === "string")) {
16
- return undefined;
17
- }
18
- const allowed = new Set(STAGE_GATE_KINDS);
19
- const gateKinds = raw.filter((item) => allowed.has(item));
20
- return gateKinds.length > 0 ? gateKinds : undefined;
1
+ import { listModelsForContext, listPipelinesForContext, listTasksForContext, } from "./browseCatalog.js";
2
+ import { resolveStageflowContext } from "../project/resolveStageflowContext.js";
3
+ import { catalogContextFromStageflow } from "./resolveCatalogContext.js";
4
+ export async function listTasks(options) {
5
+ return listTasksForContext({
6
+ projectRoot: options.projectRoot,
7
+ manifest: options.manifest,
8
+ manifestStatus: "ok",
9
+ issues: [],
10
+ });
21
11
  }
22
- async function listPipelineUsageByStage(cwd) {
23
- const dir = path.join(cwd, "pipelines");
24
- let entries;
25
- try {
26
- entries = await readdir(dir, { withFileTypes: true });
27
- }
28
- catch {
29
- return new Map();
30
- }
31
- const usage = new Map();
32
- for (const entry of entries) {
33
- if (!entry.isFile() || !entry.name.endsWith(".yaml"))
34
- continue;
35
- const filePath = path.join(dir, entry.name);
36
- try {
37
- const raw = await readYamlObject(filePath);
38
- if (typeof raw?.id !== "string" || !Array.isArray(raw.stages))
39
- continue;
40
- const stageIds = extractPipelineStageIds(raw.stages);
41
- if (!stageIds)
42
- continue;
43
- for (const stageId of stageIds) {
44
- let pipelineIds = usage.get(stageId);
45
- if (!pipelineIds) {
46
- pipelineIds = new Set();
47
- usage.set(stageId, pipelineIds);
48
- }
49
- pipelineIds.add(raw.id);
50
- }
51
- }
52
- catch {
53
- // skip invalid pipeline files
54
- }
55
- }
56
- return usage;
12
+ export async function listPipelines(options) {
13
+ return listPipelinesForContext({
14
+ projectRoot: options.projectRoot,
15
+ manifest: options.manifest,
16
+ manifestStatus: "ok",
17
+ issues: [],
18
+ });
57
19
  }
58
- export async function listTasks(cwd) {
59
- const dir = path.join(cwd, "tasks");
60
- let entries;
61
- try {
62
- entries = await readdir(dir, { withFileTypes: true });
63
- }
64
- catch {
65
- return [];
66
- }
67
- const listings = [];
68
- for (const entry of entries) {
69
- if (!entry.isFile() || !entry.name.endsWith(".yaml"))
70
- continue;
71
- const filePath = path.join(dir, entry.name);
72
- try {
73
- const task = await loadTask(filePath);
74
- listings.push({
75
- path: path.relative(cwd, filePath),
76
- id: task.id,
77
- goal: task.goal,
78
- });
79
- }
80
- catch {
81
- // skip invalid
82
- }
83
- }
84
- listings.sort((a, b) => a.id.localeCompare(b.id));
85
- return listings;
20
+ export async function listStages(_options) {
21
+ return [];
86
22
  }
87
- export async function listPipelines(cwd) {
88
- const dir = path.join(cwd, "pipelines");
89
- let entries;
90
- try {
91
- entries = await readdir(dir, { withFileTypes: true });
92
- }
93
- catch {
94
- return [];
95
- }
96
- const listings = [];
97
- for (const entry of entries) {
98
- if (!entry.isFile() || !entry.name.endsWith(".yaml"))
99
- continue;
100
- const filePath = path.join(dir, entry.name);
101
- try {
102
- const loaded = await loadPipeline(filePath, { cwd });
103
- listings.push({
104
- path: path.relative(cwd, filePath),
105
- id: loaded.pipeline.id,
106
- stages: loaded.stages.map((stage) => ({
107
- id: stage.id,
108
- ...(stage.gate_kinds ? { gate_kinds: stage.gate_kinds } : {}),
109
- })),
110
- });
111
- }
112
- catch {
113
- // skip invalid
114
- }
115
- }
116
- listings.sort((a, b) => a.id.localeCompare(b.id));
117
- return listings;
118
- }
119
- export async function listStages(cwd) {
120
- const dir = path.join(cwd, "stages");
121
- let entries;
122
- try {
123
- entries = await readdir(dir, { withFileTypes: true });
124
- }
125
- catch {
126
- return [];
127
- }
128
- const usageByStage = await listPipelineUsageByStage(cwd);
129
- const valid = [];
130
- const broken = [];
131
- for (const entry of entries) {
132
- if (!entry.isFile() || !entry.name.endsWith(".yaml"))
133
- continue;
134
- const filePath = path.join(dir, entry.name);
135
- const relPath = path.relative(cwd, filePath);
136
- try {
137
- const stage = await loadStage(filePath);
138
- valid.push({
139
- path: relPath,
140
- id: stage.id,
141
- used_by_pipeline_ids: [...(usageByStage.get(stage.id) ?? new Set())].sort(),
142
- ...(stage.gate_kinds ? { gate_kinds: stage.gate_kinds } : {}),
143
- });
144
- }
145
- catch (err) {
146
- const message = err instanceof Error ? err.message : String(err);
147
- const brokenRow = {
148
- path: relPath,
149
- error: message,
150
- };
151
- try {
152
- const raw = await readYamlObject(filePath);
153
- if (typeof raw?.id === "string")
154
- brokenRow.id = raw.id;
155
- if (typeof raw?.model === "string")
156
- brokenRow.model = raw.model;
157
- const gateKinds = parseSafeGateKinds(raw?.gate_kinds);
158
- if (gateKinds)
159
- brokenRow.gate_kinds = gateKinds;
160
- }
161
- catch {
162
- // keep path + error only when parsing itself fails
163
- }
164
- broken.push(brokenRow);
165
- }
166
- }
167
- valid.sort((a, b) => a.id.localeCompare(b.id));
168
- broken.sort((a, b) => a.path.localeCompare(b.path));
169
- return [...valid, ...broken];
170
- }
171
- export async function listModels(cwd) {
172
- const dir = path.join(cwd, "stages");
173
- const models = new Set(BAKED_MODEL_IDS);
174
- let entries;
175
- try {
176
- entries = await readdir(dir, { withFileTypes: true });
177
- }
178
- catch {
179
- return [...models].sort((a, b) => a.localeCompare(b));
180
- }
181
- for (const entry of entries) {
182
- if (!entry.isFile() || !entry.name.endsWith(".yaml"))
183
- continue;
184
- try {
185
- const raw = await readYamlObject(path.join(dir, entry.name));
186
- if (typeof raw?.model === "string" && raw.model.trim()) {
187
- models.add(raw.model);
188
- }
189
- }
190
- catch {
191
- // ignore unreadable or invalid stage YAML
192
- }
193
- }
194
- return [...models].sort((a, b) => a.localeCompare(b));
23
+ export async function listModels(cwdOrContext) {
24
+ const ctx = typeof cwdOrContext === "string"
25
+ ? catalogContextFromStageflow(await resolveStageflowContext(cwdOrContext))
26
+ : "manifestIssues" in cwdOrContext
27
+ ? catalogContextFromStageflow(cwdOrContext)
28
+ : cwdOrContext;
29
+ return listModelsForContext(ctx);
195
30
  }
@@ -0,0 +1,2 @@
1
+ import type { CatalogContext } from "./resolveCatalogContext.js";
2
+ export declare function listModelsFromManifest(ctx: CatalogContext): Promise<string[]>;
@@ -0,0 +1,4 @@
1
+ import { listModelsForContext } from "./browseCatalog.js";
2
+ export async function listModelsFromManifest(ctx) {
3
+ return listModelsForContext(ctx);
4
+ }
@@ -15,6 +15,10 @@ export type LoadIssue = {
15
15
  message: string;
16
16
  category: "task";
17
17
  taskId?: string;
18
+ } | {
19
+ code: ValidationFindingCode | string;
20
+ message: string;
21
+ category: "catalog";
18
22
  };
19
23
  export type LoadOutcome<T> = {
20
24
  ok: true;
@@ -1,25 +1,11 @@
1
1
  import type { LoadedPipeline } from "../types/pipeline.js";
2
2
  import { type LoadOutcome } from "./loadOutcome.js";
3
- import type { ValidationFinding } from "./validateCatalog.js";
4
3
  export type { LoadedPipeline } from "../types/pipeline.js";
5
4
  export type { LoadIssue, LoadOutcome } from "./loadOutcome.js";
6
- export type LoadPipelineValidatedResult = {
7
- ok: true;
8
- loaded: LoadedPipeline;
9
- } | {
10
- ok: false;
11
- findings: ValidationFinding[];
12
- };
5
+ export declare function resolvePipelinePath(pipelinePath: string, cwd: string): Promise<string>;
13
6
  export declare function loadPipelineOutcome(nameOrPath: string, options?: {
14
7
  cwd?: string;
15
- stagesDir?: string;
16
8
  }): Promise<LoadOutcome<LoadedPipeline>>;
17
- export declare function loadPipelineValidated(nameOrPath: string, options?: {
18
- cwd?: string;
19
- stagesDir?: string;
20
- validateStages?: boolean;
21
- }): Promise<LoadPipelineValidatedResult>;
22
9
  export declare function loadPipeline(nameOrPath: string, options?: {
23
10
  cwd?: string;
24
- stagesDir?: string;
25
11
  }): Promise<LoadedPipeline>;
@@ -1,68 +1,38 @@
1
1
  import { access } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { loadFailure, loadSuccess } from "./loadOutcome.js";
4
- import { loadStageOutcome } from "./loadStage.js";
5
- import { readYamlObject } from "./readYamlObject.js";
6
- import { extractPipelineStageIds, resolvePipelineDag } from "./resolvePipelineDag.js";
7
- import { findingStageIdFilenameMismatch, findingsFromLoadIssues, } from "./validateCatalog.js";
8
- async function resolvePipelinePath(nameOrPath, cwd) {
9
- const direct = path.resolve(cwd, nameOrPath);
4
+ import { mergePipelineStages } from "./mergePipelineIncludes.js";
5
+ import { normalizePipelineStageEntries, toWiringRefs, } from "./normalizePipelineStageEntry.js";
6
+ import { loadStageFromObjectOutcome, loadStageOutcome } from "./loadStage.js";
7
+ import { resolvePipelineDagFromRefs } from "./resolvePipelineDag.js";
8
+ export async function resolvePipelinePath(pipelinePath, cwd) {
9
+ const resolved = path.normalize(path.resolve(cwd, pipelinePath));
10
10
  try {
11
- await access(direct);
12
- return direct;
11
+ await access(resolved);
12
+ return resolved;
13
13
  }
14
14
  catch {
15
- const candidate = path.resolve(cwd, "pipelines", `${nameOrPath}.yaml`);
16
- await access(candidate);
17
- return candidate;
15
+ throw new Error(`Pipeline file not found: ${resolved}. Pass a filesystem path to --pipeline (e.g. pipelines/hello.pipeline.yaml), not a bare pipeline id.`);
18
16
  }
19
17
  }
20
- export async function loadPipelineOutcome(nameOrPath, options = {}) {
21
- const cwd = options.cwd ?? process.cwd();
22
- const stagesDir = options.stagesDir ?? path.join(cwd, "stages");
23
- let pipelinePath;
24
- try {
25
- pipelinePath = await resolvePipelinePath(nameOrPath, cwd);
26
- }
27
- catch (err) {
28
- const message = err instanceof Error ? err.message : String(err);
29
- return loadFailure([
30
- {
31
- code: "pipeline.load_error",
32
- message,
33
- category: "pipeline",
34
- },
35
- ]);
36
- }
37
- let raw;
38
- try {
39
- raw = await readYamlObject(pipelinePath);
40
- }
41
- catch (err) {
42
- const message = err instanceof Error ? err.message : String(err);
43
- return loadFailure([
44
- {
45
- code: "pipeline.load_error",
46
- message,
47
- category: "pipeline",
48
- },
49
- ]);
50
- }
51
- if (typeof raw?.id !== "string" || !Array.isArray(raw.stages)) {
52
- return loadFailure([
53
- {
54
- code: "pipeline.invalid_shape",
55
- message: `Invalid pipeline ${pipelinePath}: id and stages[] are required`,
56
- category: "pipeline",
57
- },
58
- ]);
59
- }
60
- const pipelineId = raw.id;
61
- const ctx = { pipelineId: raw.id, path: pipelinePath };
18
+ async function loadPipelineFromPath(pipelinePath) {
19
+ const normalizedPipelinePath = path.normalize(path.resolve(pipelinePath));
20
+ const mergeOutcome = await mergePipelineStages(normalizedPipelinePath);
21
+ if (!mergeOutcome.ok) {
22
+ return loadFailure(mergeOutcome.issues);
23
+ }
24
+ const { entries: rawEntries, pipelineId } = mergeOutcome.value;
25
+ const ctx = { pipelineId, path: normalizedPipelinePath };
26
+ const normalizeOutcome = normalizePipelineStageEntries(rawEntries, ctx);
27
+ if (!normalizeOutcome.ok) {
28
+ return loadFailure(normalizeOutcome.issues);
29
+ }
30
+ const normalizedEntries = normalizeOutcome.value;
31
+ const wiringRefs = toWiringRefs(normalizedEntries);
62
32
  let stageIds;
63
33
  let dag;
64
34
  try {
65
- const resolved = resolvePipelineDag(raw.stages, ctx);
35
+ const resolved = resolvePipelineDagFromRefs(wiringRefs, ctx);
66
36
  stageIds = resolved.stages;
67
37
  dag = resolved.dag;
68
38
  }
@@ -77,115 +47,87 @@ export async function loadPipelineOutcome(nameOrPath, options = {}) {
77
47
  },
78
48
  ]);
79
49
  }
80
- const pipeline = {
81
- id: raw.id,
82
- stages: stageIds,
83
- };
50
+ const entryById = new Map(normalizedEntries.map((entry) => [entry.id, entry]));
51
+ const stageSources = {};
84
52
  const stages = [];
85
- for (const stageId of pipeline.stages) {
86
- const stagePath = path.join(stagesDir, `${stageId}.yaml`);
87
- const stageOutcome = await loadStageOutcome(stagePath);
53
+ for (const stageId of stageIds) {
54
+ const entry = entryById.get(stageId);
55
+ if (!entry) {
56
+ return loadFailure([
57
+ {
58
+ code: "pipeline.invalid_shape",
59
+ message: `Pipeline ${pipelineId}: missing normalized entry for stage "${stageId}"`,
60
+ category: "pipeline",
61
+ pipelineId,
62
+ },
63
+ ]);
64
+ }
65
+ if (entry.body.kind === "inline") {
66
+ const inlineOutcome = loadStageFromObjectOutcome(entry.body.raw, {
67
+ entryId: entry.id,
68
+ declaringPath: entry.declaringPath,
69
+ });
70
+ if (!inlineOutcome.ok) {
71
+ return loadFailure(inlineOutcome.issues);
72
+ }
73
+ stages.push(inlineOutcome.value);
74
+ stageSources[stageId] = { kind: "inline" };
75
+ continue;
76
+ }
77
+ const stageOutcome = await loadStageOutcome(entry.body.absolutePath);
88
78
  if (!stageOutcome.ok) {
89
- const message = `Pipeline ${pipeline.id} references missing stage "${stageId}" at ${stagePath}`;
90
79
  return loadFailure([
91
80
  {
92
81
  code: "pipeline.missing_stage",
93
- message,
82
+ message: `Pipeline ${pipelineId} references missing stage "${stageId}" at ${entry.body.absolutePath}`,
94
83
  category: "pipeline",
95
84
  pipelineId,
96
85
  },
86
+ ...stageOutcome.issues,
97
87
  ]);
98
88
  }
99
- stages.push(stageOutcome.value);
100
- }
101
- return loadSuccess({ pipeline, stages, dag });
102
- }
103
- async function checkStageIdFilename(cwd, stagePath) {
104
- const fileStem = path.basename(stagePath, ".yaml");
105
- try {
106
- const raw = await readYamlObject(stagePath);
107
- if (typeof raw?.id === "string" && raw.id !== fileStem) {
108
- return findingStageIdFilenameMismatch(cwd, stagePath, fileStem, raw.id);
89
+ if (stageOutcome.value.id !== entry.id) {
90
+ return loadFailure([
91
+ {
92
+ code: "pipeline.stage_id_mismatch",
93
+ message: `Pipeline ${pipelineId}: stage entry "${entry.id}" in ${entry.declaringPath} uses ${entry.body.path} which declares id "${stageOutcome.value.id}"`,
94
+ category: "pipeline",
95
+ pipelineId,
96
+ },
97
+ ]);
109
98
  }
99
+ stages.push(stageOutcome.value);
100
+ stageSources[stageId] = { kind: "file", path: entry.body.absolutePath };
110
101
  }
111
- catch {
112
- // loadStageOutcome will report read errors
113
- }
114
- return null;
115
- }
116
- async function validateStageFile(cwd, stagePath) {
117
- const findings = [];
118
- const mismatch = await checkStageIdFilename(cwd, stagePath);
119
- if (mismatch)
120
- findings.push(mismatch);
121
- const outcome = await loadStageOutcome(stagePath);
122
- if (!outcome.ok) {
123
- findings.push(...findingsFromLoadIssues(cwd, stagePath, outcome.issues));
124
- return findings;
125
- }
126
- if (!mismatch && path.basename(stagePath, ".yaml") !== outcome.value.id) {
127
- findings.push(findingStageIdFilenameMismatch(cwd, stagePath, path.basename(stagePath, ".yaml"), outcome.value.id));
128
- }
129
- return findings;
102
+ const pipeline = {
103
+ id: pipelineId,
104
+ stages: stageIds,
105
+ };
106
+ return loadSuccess({
107
+ pipeline,
108
+ stages,
109
+ dag,
110
+ pipelinePath: normalizedPipelinePath,
111
+ stageSources,
112
+ });
130
113
  }
131
- export async function loadPipelineValidated(nameOrPath, options = {}) {
114
+ export async function loadPipelineOutcome(nameOrPath, options = {}) {
132
115
  const cwd = options.cwd ?? process.cwd();
133
- const stagesDir = options.stagesDir ?? path.join(cwd, "stages");
134
- const validateStages = options.validateStages ?? true;
135
116
  let pipelinePath;
136
117
  try {
137
118
  pipelinePath = await resolvePipelinePath(nameOrPath, cwd);
138
119
  }
139
120
  catch (err) {
140
121
  const message = err instanceof Error ? err.message : String(err);
141
- return {
142
- ok: false,
143
- findings: findingsFromLoadIssues(cwd, path.resolve(cwd, nameOrPath), [
144
- {
145
- code: "pipeline.load_error",
146
- message,
147
- category: "pipeline",
148
- },
149
- ]),
150
- };
151
- }
152
- const referencedStageIds = new Set();
153
- try {
154
- const raw = await readYamlObject(pipelinePath);
155
- if (typeof raw?.id === "string" && Array.isArray(raw.stages)) {
156
- const stageIds = extractPipelineStageIds(raw.stages);
157
- if (stageIds) {
158
- for (const stageId of stageIds) {
159
- referencedStageIds.add(stageId);
160
- }
161
- }
162
- }
163
- }
164
- catch {
165
- // loadPipelineOutcome will report read errors
166
- }
167
- const outcome = await loadPipelineOutcome(nameOrPath, { cwd, stagesDir });
168
- const findings = [];
169
- if (!outcome.ok) {
170
- findings.push(...findingsFromLoadIssues(cwd, pipelinePath, outcome.issues));
171
- if (validateStages) {
172
- for (const stageId of referencedStageIds) {
173
- const stagePath = path.join(stagesDir, `${stageId}.yaml`);
174
- findings.push(...(await validateStageFile(cwd, stagePath)));
175
- }
176
- }
177
- return { ok: false, findings };
178
- }
179
- if (validateStages) {
180
- for (const stageId of outcome.value.pipeline.stages) {
181
- const stagePath = path.join(stagesDir, `${stageId}.yaml`);
182
- findings.push(...(await validateStageFile(cwd, stagePath)));
183
- }
184
- }
185
- if (findings.some((finding) => finding.severity === "error")) {
186
- return { ok: false, findings };
122
+ return loadFailure([
123
+ {
124
+ code: "pipeline.load_error",
125
+ message,
126
+ category: "pipeline",
127
+ },
128
+ ]);
187
129
  }
188
- return { ok: true, loaded: outcome.value };
130
+ return loadPipelineFromPath(pipelinePath);
189
131
  }
190
132
  export async function loadPipeline(nameOrPath, options = {}) {
191
133
  const outcome = await loadPipelineOutcome(nameOrPath, options);
@@ -1,4 +1,8 @@
1
1
  import { type StageConfig } from "../types/stage.js";
2
2
  import { type LoadOutcome } from "./loadOutcome.js";
3
+ export declare function loadStageFromObjectOutcome(raw: Record<string, unknown>, ctx: {
4
+ entryId: string;
5
+ declaringPath: string;
6
+ }): LoadOutcome<StageConfig>;
3
7
  export declare function loadStageOutcome(filePath: string): Promise<LoadOutcome<StageConfig>>;
4
8
  export declare function loadStage(filePath: string): Promise<StageConfig>;