stageflow 0.9.0 → 0.12.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.
- package/README.md +1 -0
- package/dist/agent/activity.d.ts +9 -0
- package/dist/agent/agentBackend.d.ts +32 -0
- package/dist/agent/agentBackend.js +38 -0
- package/dist/agent/claudeActivity.d.ts +15 -0
- package/dist/agent/claudeActivity.js +58 -0
- package/dist/agent/claudeAdapter.d.ts +5 -0
- package/dist/agent/claudeAdapter.js +301 -0
- package/dist/agent/claudeSession.d.ts +18 -0
- package/dist/agent/claudeSession.js +54 -0
- package/dist/agent/claudeTools.d.ts +89 -0
- package/dist/agent/claudeTools.js +135 -0
- package/dist/agent/fakeAgent.js +24 -4
- package/dist/agent/piAdapter.d.ts +6 -3
- package/dist/agent/piAdapter.js +129 -21
- package/dist/agent/port.d.ts +50 -1
- package/dist/agent/resolveAgentPort.d.ts +26 -0
- package/dist/agent/resolveAgentPort.js +39 -0
- package/dist/cli/runCommand.js +2 -2
- package/dist/cli/runsCommand.d.ts +1 -1
- package/dist/cli/runsCommand.js +184 -2
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +11 -5
- package/dist/config/browseCatalog.js +1 -1
- package/dist/config/createPipeline.d.ts +2 -1
- package/dist/config/createPipeline.js +34 -9
- package/dist/config/createStage.js +14 -9
- package/dist/config/loadPipeline.js +10 -1
- package/dist/config/loadStage.js +138 -2
- package/dist/config/loadStageflowManifest.js +14 -1
- package/dist/config/mergePipelineIncludes.d.ts +1 -0
- package/dist/config/mergePipelineIncludes.js +14 -1
- package/dist/config/normalizePipelineStageEntry.d.ts +7 -2
- package/dist/config/normalizePipelineStageEntry.js +49 -6
- package/dist/config/parseCompletionContract.d.ts +8 -0
- package/dist/config/parseCompletionContract.js +218 -0
- package/dist/config/parsePreEmitChecks.d.ts +3 -0
- package/dist/config/parsePreEmitChecks.js +65 -0
- package/dist/config/pipelineNeeds.d.ts +19 -0
- package/dist/config/pipelineNeeds.js +151 -0
- package/dist/config/pipelineStageKeys.js +16 -1
- package/dist/config/resolveForkEmitContext.d.ts +6 -2
- package/dist/config/resolveForkEmitContext.js +9 -2
- package/dist/config/resolvePipelineDag.d.ts +2 -1
- package/dist/config/resolvePipelineDag.js +182 -39
- package/dist/config/validateCatalog.d.ts +1 -1
- package/dist/config/validateCompletionContract.d.ts +4 -0
- package/dist/config/validateCompletionContract.js +52 -0
- package/dist/envelope/check.d.ts +1 -1
- package/dist/envelope/check.js +86 -13
- package/dist/envelope/cloneForks.d.ts +1 -1
- package/dist/envelope/cloneForks.js +24 -9
- package/dist/envelope/feedbackLoop.d.ts +4 -0
- package/dist/envelope/feedbackLoop.js +31 -0
- package/dist/envelope/payloadSchema.d.ts +7 -1
- package/dist/envelope/payloadSchema.js +170 -13
- package/dist/envelope/preEmitChecks.d.ts +29 -0
- package/dist/envelope/preEmitChecks.js +57 -0
- package/dist/hitl/qaTrail.d.ts +25 -0
- package/dist/hitl/qaTrail.js +51 -0
- package/dist/mcp/catalogTools.js +4 -2
- package/dist/mcp/controlTools.js +72 -0
- package/dist/mcp/waitingGates.d.ts +2 -1
- package/dist/mcp/waitingGates.js +17 -1
- package/dist/package-meta.d.ts +1 -1
- package/dist/package-meta.js +1 -1
- package/dist/projection/projectRun.d.ts +4 -1
- package/dist/projection/projectRun.js +7 -0
- package/dist/prompt/feedbackLoopContext.d.ts +2 -0
- package/dist/prompt/feedbackLoopContext.js +17 -0
- package/dist/prompt/priorEnvelope.d.ts +2 -2
- package/dist/prompt/priorEnvelope.js +4 -1
- package/dist/runstore/pipelineDagSnapshot.d.ts +1 -0
- package/dist/runstore/pipelineDagSnapshot.js +51 -11
- package/dist/runstore/port.d.ts +185 -3
- package/dist/runstore/port.js +42 -3
- package/dist/runstore/runProjection.d.ts +7 -3
- package/dist/runstore/runProjection.js +30 -15
- package/dist/runstore/sqlite/SqliteRunStore.d.ts +25 -1
- package/dist/runstore/sqlite/SqliteRunStore.js +626 -8
- package/dist/runstore/sqlite/schema.d.ts +1 -1
- package/dist/runstore/sqlite/schema.js +99 -0
- package/dist/runstore/stageInstanceId.d.ts +1 -1
- package/dist/runstore/stageInstanceId.js +3 -2
- package/dist/runstore/trackProjection.js +59 -27
- package/dist/runstore/verificationHistory.d.ts +19 -0
- package/dist/runstore/verificationHistory.js +30 -0
- package/dist/runtime/cloneSchedule.d.ts +40 -4
- package/dist/runtime/cloneSchedule.js +212 -28
- package/dist/runtime/completionCheckRunner.d.ts +137 -0
- package/dist/runtime/completionCheckRunner.js +365 -0
- package/dist/runtime/completionCheckoutBaseline.d.ts +3 -0
- package/dist/runtime/completionCheckoutBaseline.js +44 -0
- package/dist/runtime/envelopeRouting.d.ts +10 -3
- package/dist/runtime/envelopeRouting.js +146 -5
- package/dist/runtime/feedbackLoopCoordinator.d.ts +74 -0
- package/dist/runtime/feedbackLoopCoordinator.js +446 -0
- package/dist/runtime/feedbackLoopDecision.d.ts +44 -0
- package/dist/runtime/feedbackLoopDecision.js +207 -0
- package/dist/runtime/feedbackLoopRoute.d.ts +5 -0
- package/dist/runtime/feedbackLoopRoute.js +13 -0
- package/dist/runtime/feedbackLoopSchedule.d.ts +38 -0
- package/dist/runtime/feedbackLoopSchedule.js +72 -0
- package/dist/runtime/forkGeneration.d.ts +72 -0
- package/dist/runtime/forkGeneration.js +295 -0
- package/dist/runtime/gitCheckoutCapability.d.ts +7 -0
- package/dist/runtime/gitCheckoutCapability.js +97 -0
- package/dist/runtime/manualRecoveryState.d.ts +40 -0
- package/dist/runtime/manualRecoveryState.js +73 -0
- package/dist/runtime/pipelineScheduler.d.ts +13 -1
- package/dist/runtime/pipelineScheduler.js +328 -45
- package/dist/runtime/replayLifecycle.d.ts +107 -0
- package/dist/runtime/replayLifecycle.js +168 -0
- package/dist/runtime/runManager.d.ts +54 -1
- package/dist/runtime/runManager.js +471 -6
- package/dist/runtime/runRetryCoordinator.d.ts +6 -1
- package/dist/runtime/runRetryCoordinator.js +43 -9
- package/dist/runtime/stageAttemptBootstrap.d.ts +3 -1
- package/dist/runtime/stageAttemptBootstrap.js +79 -1
- package/dist/runtime/stageProcessLauncher.d.ts +1 -1
- package/dist/runtime/stageRecovery.js +1 -1
- package/dist/runtime/stageRunner.d.ts +4 -1
- package/dist/runtime/stageRunner.js +29 -3
- package/dist/runtime/stageWorker.js +69 -2
- package/dist/runtime/stageWorkerProtocol.d.ts +1 -1
- package/dist/runtime/verifiedStageExecution.d.ts +29 -0
- package/dist/runtime/verifiedStageExecution.js +119 -0
- package/dist/server/bootstrap.js +1 -0
- package/dist/server/http.js +114 -0
- package/dist/server/operatorResults.js +2 -0
- package/dist/tools/askOperator.d.ts +161 -5
- package/dist/tools/askOperator.js +58 -25
- package/dist/tools/emitStageEnvelope.d.ts +12 -12
- package/dist/tools/emitStageEnvelope.js +112 -24
- package/dist/types/completion.d.ts +43 -0
- package/dist/types/completion.js +1 -0
- package/dist/types/envelope.d.ts +18 -0
- package/dist/types/forkChoice.d.ts +9 -4
- package/dist/types/forkChoice.js +1 -1
- package/dist/types/pipeline.d.ts +39 -2
- package/dist/types/preEmitCheck.d.ts +18 -0
- package/dist/types/preEmitCheck.js +1 -0
- package/dist/types/stage.d.ts +13 -0
- package/dist/types/stageflowManifest.d.ts +2 -0
- package/dist/ui/assets/index-CXGz4nL7.js +118 -0
- package/dist/ui/assets/{index-C3N7MXAC.css → index-uVHiae8s.css} +1 -1
- package/dist/ui/index.html +2 -2
- package/package.json +2 -1
- package/skills/stageflow-author/references/catalog-mapping.md +26 -2
- package/skills/stageflow-author/references/catalog-write-conventions.md +1 -1
- package/dist/ui/assets/index-CSoGDA3A.js +0 -118
package/dist/config/loadStage.js
CHANGED
|
@@ -1,10 +1,69 @@
|
|
|
1
|
+
import { parseAgentField } from "../agent/agentBackend.js";
|
|
2
|
+
import { CLONE_ACTIONS } from "../types/forkChoice.js";
|
|
1
3
|
import { STAGE_GATE_KINDS, } from "../types/stage.js";
|
|
2
4
|
import { compilePayloadSchema } from "../envelope/payloadSchema.js";
|
|
3
5
|
import { loadFailure, loadSuccess } from "./loadOutcome.js";
|
|
6
|
+
import { parsePreEmitChecks } from "./parsePreEmitChecks.js";
|
|
4
7
|
import { readYamlObject } from "./readYamlObject.js";
|
|
5
8
|
function isGateKind(value) {
|
|
6
9
|
return STAGE_GATE_KINDS.includes(value);
|
|
7
10
|
}
|
|
11
|
+
function isCloneAction(value) {
|
|
12
|
+
return CLONE_ACTIONS.includes(value);
|
|
13
|
+
}
|
|
14
|
+
function parseCloneActions(raw, label) {
|
|
15
|
+
if (raw === undefined)
|
|
16
|
+
return loadSuccess(undefined);
|
|
17
|
+
if (!Array.isArray(raw) || !raw.every((item) => typeof item === "string")) {
|
|
18
|
+
return loadFailure([
|
|
19
|
+
{
|
|
20
|
+
code: "stage.invalid_clone_actions",
|
|
21
|
+
message: `Invalid stage ${label}: clone_actions must be a non-empty array of strings`,
|
|
22
|
+
category: "stage",
|
|
23
|
+
},
|
|
24
|
+
]);
|
|
25
|
+
}
|
|
26
|
+
if (raw.length === 0) {
|
|
27
|
+
return loadFailure([
|
|
28
|
+
{
|
|
29
|
+
code: "stage.invalid_clone_actions",
|
|
30
|
+
message: `Invalid stage ${label}: clone_actions must not be empty`,
|
|
31
|
+
category: "stage",
|
|
32
|
+
},
|
|
33
|
+
]);
|
|
34
|
+
}
|
|
35
|
+
const actions = [];
|
|
36
|
+
for (const item of raw) {
|
|
37
|
+
if (!isCloneAction(item)) {
|
|
38
|
+
return loadFailure([
|
|
39
|
+
{
|
|
40
|
+
code: "stage.invalid_clone_actions",
|
|
41
|
+
message: `Invalid stage ${label}: unsupported clone_actions value "${item}" (allowed: ${CLONE_ACTIONS.join(", ")})`,
|
|
42
|
+
category: "stage",
|
|
43
|
+
},
|
|
44
|
+
]);
|
|
45
|
+
}
|
|
46
|
+
actions.push(item);
|
|
47
|
+
}
|
|
48
|
+
return loadSuccess(actions);
|
|
49
|
+
}
|
|
50
|
+
function parseTimeoutMs(raw, label) {
|
|
51
|
+
if (raw === undefined)
|
|
52
|
+
return loadSuccess(undefined);
|
|
53
|
+
if (typeof raw !== "number" ||
|
|
54
|
+
!Number.isFinite(raw) ||
|
|
55
|
+
!Number.isInteger(raw) ||
|
|
56
|
+
raw <= 0) {
|
|
57
|
+
return loadFailure([
|
|
58
|
+
{
|
|
59
|
+
code: "stage.invalid_timeout_ms",
|
|
60
|
+
message: `Invalid stage ${label}: timeout_ms must be a positive integer (milliseconds)`,
|
|
61
|
+
category: "stage",
|
|
62
|
+
},
|
|
63
|
+
]);
|
|
64
|
+
}
|
|
65
|
+
return loadSuccess(raw);
|
|
66
|
+
}
|
|
8
67
|
function parseGateKinds(raw, label) {
|
|
9
68
|
if (raw === undefined)
|
|
10
69
|
return loadSuccess(undefined);
|
|
@@ -30,7 +89,7 @@ function parseGateKinds(raw, label) {
|
|
|
30
89
|
}
|
|
31
90
|
kinds.push(item);
|
|
32
91
|
}
|
|
33
|
-
return loadSuccess(kinds
|
|
92
|
+
return loadSuccess(kinds);
|
|
34
93
|
}
|
|
35
94
|
function parseStageFields(raw, label, entryId) {
|
|
36
95
|
if (typeof raw.system_prompt !== "string" ||
|
|
@@ -78,6 +137,46 @@ function parseStageFields(raw, label, entryId) {
|
|
|
78
137
|
}
|
|
79
138
|
stage.payload_schema = raw.payload_schema;
|
|
80
139
|
}
|
|
140
|
+
if (raw.clone_input_schema !== undefined) {
|
|
141
|
+
if (raw.clone_input_schema === null ||
|
|
142
|
+
typeof raw.clone_input_schema !== "object" ||
|
|
143
|
+
Array.isArray(raw.clone_input_schema)) {
|
|
144
|
+
return loadFailure([
|
|
145
|
+
{
|
|
146
|
+
code: "stage.invalid_clone_input_schema",
|
|
147
|
+
message: `Invalid stage ${label}: clone_input_schema must be an object`,
|
|
148
|
+
category: "stage",
|
|
149
|
+
stageId: entryId,
|
|
150
|
+
},
|
|
151
|
+
]);
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
compilePayloadSchema(raw.clone_input_schema);
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
158
|
+
return loadFailure([
|
|
159
|
+
{
|
|
160
|
+
code: "stage.invalid_clone_input_schema",
|
|
161
|
+
message: `Invalid stage ${label}: invalid clone_input_schema: ${message}`,
|
|
162
|
+
category: "stage",
|
|
163
|
+
stageId: entryId,
|
|
164
|
+
},
|
|
165
|
+
]);
|
|
166
|
+
}
|
|
167
|
+
stage.clone_input_schema = raw.clone_input_schema;
|
|
168
|
+
}
|
|
169
|
+
const cloneActionsOutcome = parseCloneActions(raw.clone_actions, label);
|
|
170
|
+
if (!cloneActionsOutcome.ok) {
|
|
171
|
+
const issues = cloneActionsOutcome.issues.map((issue) => ({
|
|
172
|
+
...issue,
|
|
173
|
+
stageId: entryId,
|
|
174
|
+
}));
|
|
175
|
+
return loadFailure(issues);
|
|
176
|
+
}
|
|
177
|
+
if (cloneActionsOutcome.value !== undefined) {
|
|
178
|
+
stage.clone_actions = cloneActionsOutcome.value;
|
|
179
|
+
}
|
|
81
180
|
const gateKindsOutcome = parseGateKinds(raw.gate_kinds, label);
|
|
82
181
|
if (!gateKindsOutcome.ok) {
|
|
83
182
|
const issues = gateKindsOutcome.issues.map((issue) => ({
|
|
@@ -86,8 +185,31 @@ function parseStageFields(raw, label, entryId) {
|
|
|
86
185
|
}));
|
|
87
186
|
return loadFailure(issues);
|
|
88
187
|
}
|
|
89
|
-
if (gateKindsOutcome.value)
|
|
188
|
+
if (gateKindsOutcome.value !== undefined) {
|
|
90
189
|
stage.gate_kinds = gateKindsOutcome.value;
|
|
190
|
+
}
|
|
191
|
+
const timeoutMsOutcome = parseTimeoutMs(raw.timeout_ms, label);
|
|
192
|
+
if (!timeoutMsOutcome.ok) {
|
|
193
|
+
const issues = timeoutMsOutcome.issues.map((issue) => ({
|
|
194
|
+
...issue,
|
|
195
|
+
stageId: entryId,
|
|
196
|
+
}));
|
|
197
|
+
return loadFailure(issues);
|
|
198
|
+
}
|
|
199
|
+
if (timeoutMsOutcome.value !== undefined) {
|
|
200
|
+
stage.timeout_ms = timeoutMsOutcome.value;
|
|
201
|
+
}
|
|
202
|
+
const preEmitChecksOutcome = parsePreEmitChecks(raw.pre_emit_checks, label);
|
|
203
|
+
if (!preEmitChecksOutcome.ok) {
|
|
204
|
+
const issues = preEmitChecksOutcome.issues.map((issue) => ({
|
|
205
|
+
...issue,
|
|
206
|
+
stageId: entryId,
|
|
207
|
+
}));
|
|
208
|
+
return loadFailure(issues);
|
|
209
|
+
}
|
|
210
|
+
if (preEmitChecksOutcome.value !== undefined) {
|
|
211
|
+
stage.pre_emit_checks = preEmitChecksOutcome.value;
|
|
212
|
+
}
|
|
91
213
|
if (raw.skill !== undefined) {
|
|
92
214
|
if (typeof raw.skill !== "string" || raw.skill.trim() === "") {
|
|
93
215
|
return loadFailure([
|
|
@@ -101,6 +223,20 @@ function parseStageFields(raw, label, entryId) {
|
|
|
101
223
|
}
|
|
102
224
|
stage.skill = raw.skill.trim();
|
|
103
225
|
}
|
|
226
|
+
const agentField = parseAgentField(raw.agent);
|
|
227
|
+
if (!agentField.ok) {
|
|
228
|
+
return loadFailure([
|
|
229
|
+
{
|
|
230
|
+
code: "stage.invalid_agent",
|
|
231
|
+
message: `Invalid stage ${label}: ${agentField.message}`,
|
|
232
|
+
category: "stage",
|
|
233
|
+
stageId: entryId,
|
|
234
|
+
},
|
|
235
|
+
]);
|
|
236
|
+
}
|
|
237
|
+
if (agentField.value !== undefined) {
|
|
238
|
+
stage.agent = agentField.value;
|
|
239
|
+
}
|
|
104
240
|
return loadSuccess(stage);
|
|
105
241
|
}
|
|
106
242
|
export function loadStageFromObjectOutcome(raw, ctx) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { access, readFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { parse as parseYaml } from "yaml";
|
|
4
|
+
import { parseAgentField } from "../agent/agentBackend.js";
|
|
4
5
|
import { loadFailure, loadSuccess } from "./loadOutcome.js";
|
|
5
6
|
const DEFAULT_PIPELINE_PATTERN = "*.pipeline.yaml";
|
|
6
7
|
const DEFAULT_TASK_PATTERN = "*.task.yaml";
|
|
@@ -108,6 +109,14 @@ export function parseStageflowManifestOutcome(yamlText, manifestPath, projectRoo
|
|
|
108
109
|
}
|
|
109
110
|
}
|
|
110
111
|
const patterns = parsePatterns(catalogRecord.patterns, issues);
|
|
112
|
+
let agent;
|
|
113
|
+
const agentField = parseAgentField(record.agent);
|
|
114
|
+
if (!agentField.ok) {
|
|
115
|
+
issues.push(catalogIssue("catalog.manifest_invalid", agentField.message));
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
agent = agentField.value;
|
|
119
|
+
}
|
|
111
120
|
if (issues.length > 0) {
|
|
112
121
|
return loadFailure(issues);
|
|
113
122
|
}
|
|
@@ -127,7 +136,11 @@ export function parseStageflowManifestOutcome(yamlText, manifestPath, projectRoo
|
|
|
127
136
|
catalog.patterns.task = patterns.task;
|
|
128
137
|
}
|
|
129
138
|
}
|
|
130
|
-
const manifest = {
|
|
139
|
+
const manifest = {
|
|
140
|
+
version: 1,
|
|
141
|
+
catalog,
|
|
142
|
+
...(agent !== undefined ? { agent } : {}),
|
|
143
|
+
};
|
|
131
144
|
return loadSuccess({
|
|
132
145
|
path: manifestPath,
|
|
133
146
|
projectRoot,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import { parseAgentField } from "../agent/agentBackend.js";
|
|
2
3
|
import { loadFailure, loadSuccess } from "./loadOutcome.js";
|
|
3
4
|
import { readYamlObject } from "./readYamlObject.js";
|
|
4
5
|
function isPlainObject(value) {
|
|
@@ -123,6 +124,18 @@ export async function mergePipelineStages(rootPath) {
|
|
|
123
124
|
]);
|
|
124
125
|
}
|
|
125
126
|
const pipelineId = raw.id;
|
|
127
|
+
const agentField = parseAgentField(raw.agent);
|
|
128
|
+
if (!agentField.ok) {
|
|
129
|
+
return loadFailure([
|
|
130
|
+
{
|
|
131
|
+
code: "pipeline.invalid_agent",
|
|
132
|
+
message: `Invalid pipeline ${absRoot}: ${agentField.message}`,
|
|
133
|
+
category: "pipeline",
|
|
134
|
+
pipelineId,
|
|
135
|
+
},
|
|
136
|
+
]);
|
|
137
|
+
}
|
|
138
|
+
const agent = agentField.value;
|
|
126
139
|
const idLocations = new Map();
|
|
127
140
|
const entries = [];
|
|
128
141
|
const mergeResult = await visitPipelineFile(absRoot, [], idLocations, entries);
|
|
@@ -137,5 +150,5 @@ export async function mergePipelineStages(rootPath) {
|
|
|
137
150
|
},
|
|
138
151
|
]);
|
|
139
152
|
}
|
|
140
|
-
return loadSuccess({ entries, pipelineId });
|
|
153
|
+
return loadSuccess({ entries, pipelineId, ...(agent !== undefined ? { agent } : {}) });
|
|
141
154
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { CompletionContract, RecoveryPolicy } from "../types/completion.js";
|
|
2
|
+
import type { NormalizedPipelineStageEntry, PipelineNeeds } from "../types/pipeline.js";
|
|
2
3
|
import { type LoadOutcome } from "./loadOutcome.js";
|
|
3
4
|
import type { RawMergedEntry } from "./mergePipelineIncludes.js";
|
|
4
5
|
export declare function inferIdFromUsesPath(usesPath: string): string | null;
|
|
@@ -8,11 +9,15 @@ export declare function normalizePipelineStageEntries(rawEntries: RawMergedEntry
|
|
|
8
9
|
}): LoadOutcome<NormalizedPipelineStageEntry[]>;
|
|
9
10
|
export declare function toWiringRefs(entries: NormalizedPipelineStageEntry[]): Array<{
|
|
10
11
|
id: string;
|
|
11
|
-
needs?:
|
|
12
|
+
needs?: PipelineNeeds;
|
|
12
13
|
fork?: {
|
|
13
14
|
select: "one" | "subset";
|
|
14
15
|
allow_none?: boolean;
|
|
15
16
|
};
|
|
16
17
|
clonable?: boolean;
|
|
17
18
|
clone_cap?: number;
|
|
19
|
+
completion?: CompletionContract;
|
|
20
|
+
recovery?: RecoveryPolicy;
|
|
21
|
+
feedback_loop?: NormalizedPipelineStageEntry["feedback_loop"];
|
|
22
|
+
replay_safe?: boolean;
|
|
18
23
|
}>;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { loadFailure, loadSuccess } from "./loadOutcome.js";
|
|
3
3
|
import { BODY_KEYS, isAllowedPipelineStageEntryKey, } from "./pipelineStageKeys.js";
|
|
4
|
+
import { parseExecutionPolicy } from "./parseCompletionContract.js";
|
|
5
|
+
import { parsePipelineNeeds } from "./pipelineNeeds.js";
|
|
6
|
+
import { parseFeedbackLoopConfig } from "./resolvePipelineDag.js";
|
|
4
7
|
function isPlainObject(value) {
|
|
5
8
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6
9
|
}
|
|
@@ -130,27 +133,49 @@ export function normalizePipelineStageEntries(rawEntries, ctx) {
|
|
|
130
133
|
},
|
|
131
134
|
]);
|
|
132
135
|
}
|
|
133
|
-
|
|
134
|
-
|
|
136
|
+
const policyOutcome = parseExecutionPolicy(raw, id);
|
|
137
|
+
if (!policyOutcome.ok)
|
|
138
|
+
return policyOutcome;
|
|
139
|
+
let needs;
|
|
140
|
+
let feedbackLoop;
|
|
141
|
+
if (raw.feedback_loop !== undefined) {
|
|
142
|
+
try {
|
|
143
|
+
feedbackLoop = parseFeedbackLoopConfig(raw.feedback_loop, id, ctx);
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
135
146
|
return loadFailure([
|
|
136
147
|
{
|
|
137
148
|
code: "pipeline.dag_error",
|
|
138
|
-
message:
|
|
149
|
+
message: err instanceof Error ? err.message : String(err),
|
|
139
150
|
category: "pipeline",
|
|
140
151
|
pipelineId: ctx.pipelineId,
|
|
141
152
|
},
|
|
142
153
|
]);
|
|
143
154
|
}
|
|
144
|
-
|
|
155
|
+
}
|
|
156
|
+
if (raw.replay_safe !== undefined && typeof raw.replay_safe !== "boolean") {
|
|
157
|
+
return loadFailure([
|
|
158
|
+
{
|
|
159
|
+
code: "pipeline.dag_error",
|
|
160
|
+
message: `Pipeline ${ctx.pipelineId} (${ctx.path}): stage "${id}": replay_safe must be a boolean`,
|
|
161
|
+
category: "pipeline",
|
|
162
|
+
pipelineId: ctx.pipelineId,
|
|
163
|
+
},
|
|
164
|
+
]);
|
|
165
|
+
}
|
|
166
|
+
if (raw.needs !== undefined) {
|
|
167
|
+
const parsedNeeds = parsePipelineNeeds(raw.needs, id);
|
|
168
|
+
if (!parsedNeeds.ok) {
|
|
145
169
|
return loadFailure([
|
|
146
170
|
{
|
|
147
171
|
code: "pipeline.dag_error",
|
|
148
|
-
message: `Pipeline ${ctx.pipelineId} (${ctx.path}):
|
|
172
|
+
message: `Pipeline ${ctx.pipelineId} (${ctx.path}): ${parsedNeeds.message}`,
|
|
149
173
|
category: "pipeline",
|
|
150
174
|
pipelineId: ctx.pipelineId,
|
|
151
175
|
},
|
|
152
176
|
]);
|
|
153
177
|
}
|
|
178
|
+
needs = parsedNeeds.value;
|
|
154
179
|
}
|
|
155
180
|
let forkValue;
|
|
156
181
|
if (raw.fork !== undefined) {
|
|
@@ -178,10 +203,22 @@ export function normalizePipelineStageEntries(rawEntries, ctx) {
|
|
|
178
203
|
id,
|
|
179
204
|
declaringPath,
|
|
180
205
|
body,
|
|
181
|
-
...(
|
|
206
|
+
...(needs !== undefined ? { needs } : {}),
|
|
182
207
|
...(forkValue !== undefined ? { fork: forkValue } : {}),
|
|
183
208
|
...(raw.clonable !== undefined ? { clonable: raw.clonable } : {}),
|
|
184
209
|
...(raw.clone_cap !== undefined ? { clone_cap: raw.clone_cap } : {}),
|
|
210
|
+
...(policyOutcome.value.completion !== undefined
|
|
211
|
+
? { completion: policyOutcome.value.completion }
|
|
212
|
+
: {}),
|
|
213
|
+
...(policyOutcome.value.recovery !== undefined
|
|
214
|
+
? { recovery: policyOutcome.value.recovery }
|
|
215
|
+
: {}),
|
|
216
|
+
...(feedbackLoop !== undefined
|
|
217
|
+
? { feedback_loop: feedbackLoop }
|
|
218
|
+
: {}),
|
|
219
|
+
...(raw.replay_safe !== undefined
|
|
220
|
+
? { replay_safe: raw.replay_safe }
|
|
221
|
+
: {}),
|
|
185
222
|
...(skill !== undefined ? { skill } : {}),
|
|
186
223
|
};
|
|
187
224
|
const priorPath = normalized.find((e) => e.id === id)?.declaringPath;
|
|
@@ -206,5 +243,11 @@ export function toWiringRefs(entries) {
|
|
|
206
243
|
...(entry.fork !== undefined ? { fork: entry.fork } : {}),
|
|
207
244
|
...(entry.clonable !== undefined ? { clonable: entry.clonable } : {}),
|
|
208
245
|
...(entry.clone_cap !== undefined ? { clone_cap: entry.clone_cap } : {}),
|
|
246
|
+
...(entry.completion !== undefined ? { completion: entry.completion } : {}),
|
|
247
|
+
...(entry.recovery !== undefined ? { recovery: entry.recovery } : {}),
|
|
248
|
+
...(entry.feedback_loop !== undefined
|
|
249
|
+
? { feedback_loop: entry.feedback_loop }
|
|
250
|
+
: {}),
|
|
251
|
+
...(entry.replay_safe !== undefined ? { replay_safe: entry.replay_safe } : {}),
|
|
209
252
|
}));
|
|
210
253
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { CompletionContract, RecoveryPolicy } from "../types/completion.js";
|
|
2
|
+
import { type LoadOutcome } from "./loadOutcome.js";
|
|
3
|
+
type ExecutionPolicy = {
|
|
4
|
+
completion?: CompletionContract;
|
|
5
|
+
recovery?: RecoveryPolicy;
|
|
6
|
+
};
|
|
7
|
+
export declare function parseExecutionPolicy(raw: Record<string, unknown>, stageId: string): LoadOutcome<ExecutionPolicy>;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { STAGE_GATE_KINDS } from "../types/stage.js";
|
|
3
|
+
import { loadFailure, loadSuccess } from "./loadOutcome.js";
|
|
4
|
+
function isPlainObject(value) {
|
|
5
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
function failure(stageId, field, message) {
|
|
8
|
+
return loadFailure([
|
|
9
|
+
{
|
|
10
|
+
code: field === "completion"
|
|
11
|
+
? "pipeline.invalid_completion"
|
|
12
|
+
: "pipeline.invalid_recovery",
|
|
13
|
+
message: `Stage "${stageId}" ${field}: ${message}`,
|
|
14
|
+
category: "pipeline",
|
|
15
|
+
},
|
|
16
|
+
]);
|
|
17
|
+
}
|
|
18
|
+
function hasOnlyKeys(value, allowed) {
|
|
19
|
+
return Object.keys(value).find((key) => !allowed.includes(key));
|
|
20
|
+
}
|
|
21
|
+
function isPositiveInteger(value) {
|
|
22
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
23
|
+
}
|
|
24
|
+
function isGateKind(value) {
|
|
25
|
+
return typeof value === "string" && STAGE_GATE_KINDS.includes(value);
|
|
26
|
+
}
|
|
27
|
+
function parsePathFields(value) {
|
|
28
|
+
if (value === undefined)
|
|
29
|
+
return undefined;
|
|
30
|
+
if (!Array.isArray(value) ||
|
|
31
|
+
value.length === 0 ||
|
|
32
|
+
!value.every((item) => typeof item === "string" && item.trim() !== "")) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
const fields = value.map((item) => item.trim());
|
|
36
|
+
return new Set(fields).size === fields.length ? fields : undefined;
|
|
37
|
+
}
|
|
38
|
+
function parseCheck(raw, stageId, index) {
|
|
39
|
+
if (!isPlainObject(raw)) {
|
|
40
|
+
return failure(stageId, "completion", `checks[${index}] must be an object`);
|
|
41
|
+
}
|
|
42
|
+
if (typeof raw.id !== "string" || raw.id.trim() === "") {
|
|
43
|
+
return failure(stageId, "completion", `checks[${index}].id must be a non-empty string`);
|
|
44
|
+
}
|
|
45
|
+
if (typeof raw.type !== "string") {
|
|
46
|
+
return failure(stageId, "completion", `checks[${index}].type must be a string`);
|
|
47
|
+
}
|
|
48
|
+
const id = raw.id.trim();
|
|
49
|
+
if (raw.type === "command") {
|
|
50
|
+
const unknown = hasOnlyKeys(raw, ["id", "type", "run", "cwd", "timeout_ms"]);
|
|
51
|
+
if (unknown)
|
|
52
|
+
return failure(stageId, "completion", `checks[${index}]: unknown key "${unknown}"`);
|
|
53
|
+
if (typeof raw.run !== "string" || raw.run.trim() === "") {
|
|
54
|
+
return failure(stageId, "completion", `checks[${index}] command check requires a non-empty run`);
|
|
55
|
+
}
|
|
56
|
+
if (raw.cwd !== undefined && (typeof raw.cwd !== "string" || raw.cwd.trim() === "")) {
|
|
57
|
+
return failure(stageId, "completion", `checks[${index}].cwd must be a non-empty string`);
|
|
58
|
+
}
|
|
59
|
+
if (raw.timeout_ms !== undefined && !isPositiveInteger(raw.timeout_ms)) {
|
|
60
|
+
return failure(stageId, "completion", `checks[${index}].timeout_ms must be a positive integer`);
|
|
61
|
+
}
|
|
62
|
+
return loadSuccess({
|
|
63
|
+
id,
|
|
64
|
+
type: "command",
|
|
65
|
+
run: raw.run.trim(),
|
|
66
|
+
...(typeof raw.cwd === "string" ? { cwd: raw.cwd.trim() } : {}),
|
|
67
|
+
...(typeof raw.timeout_ms === "number" ? { timeout_ms: raw.timeout_ms } : {}),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
if (raw.type === "artifact") {
|
|
71
|
+
const unknown = hasOnlyKeys(raw, ["id", "type", "path", "nonempty"]);
|
|
72
|
+
if (unknown)
|
|
73
|
+
return failure(stageId, "completion", `checks[${index}]: unknown key "${unknown}"`);
|
|
74
|
+
if (typeof raw.path !== "string" || raw.path.trim() === "") {
|
|
75
|
+
return failure(stageId, "completion", `checks[${index}] artifact check requires a non-empty path`);
|
|
76
|
+
}
|
|
77
|
+
const artifactPath = raw.path.trim();
|
|
78
|
+
const normalizedPath = path.normalize(artifactPath);
|
|
79
|
+
if (path.isAbsolute(artifactPath) ||
|
|
80
|
+
normalizedPath === ".." ||
|
|
81
|
+
normalizedPath.startsWith(`..${path.sep}`)) {
|
|
82
|
+
return failure(stageId, "completion", `checks[${index}] artifact path must stay inside the attempt artifact directory`);
|
|
83
|
+
}
|
|
84
|
+
if (raw.nonempty !== undefined && typeof raw.nonempty !== "boolean") {
|
|
85
|
+
return failure(stageId, "completion", `checks[${index}].nonempty must be a boolean`);
|
|
86
|
+
}
|
|
87
|
+
return loadSuccess({
|
|
88
|
+
id,
|
|
89
|
+
type: "artifact",
|
|
90
|
+
path: artifactPath,
|
|
91
|
+
...(typeof raw.nonempty === "boolean" ? { nonempty: raw.nonempty } : {}),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
if (raw.type === "checklist") {
|
|
95
|
+
const unknown = hasOnlyKeys(raw, ["id", "type", "items"]);
|
|
96
|
+
if (unknown)
|
|
97
|
+
return failure(stageId, "completion", `checks[${index}]: unknown key "${unknown}"`);
|
|
98
|
+
if (!Array.isArray(raw.items) ||
|
|
99
|
+
raw.items.length === 0 ||
|
|
100
|
+
!raw.items.every((item) => typeof item === "string" && item.trim() !== "")) {
|
|
101
|
+
return failure(stageId, "completion", `checks[${index}] checklist requires a non-empty items array of strings`);
|
|
102
|
+
}
|
|
103
|
+
const items = raw.items.map((item) => item.trim());
|
|
104
|
+
if (new Set(items).size !== items.length) {
|
|
105
|
+
return failure(stageId, "completion", `checks[${index}] checklist items must be unique`);
|
|
106
|
+
}
|
|
107
|
+
return loadSuccess({ id, type: "checklist", items });
|
|
108
|
+
}
|
|
109
|
+
if (raw.type === "payload_schema") {
|
|
110
|
+
const unknown = hasOnlyKeys(raw, ["id", "type"]);
|
|
111
|
+
if (unknown)
|
|
112
|
+
return failure(stageId, "completion", `checks[${index}]: unknown key "${unknown}"`);
|
|
113
|
+
return loadSuccess({ id, type: "payload_schema" });
|
|
114
|
+
}
|
|
115
|
+
if (raw.type === "gate") {
|
|
116
|
+
const unknown = hasOnlyKeys(raw, ["id", "type", "kind"]);
|
|
117
|
+
if (unknown)
|
|
118
|
+
return failure(stageId, "completion", `checks[${index}]: unknown key "${unknown}"`);
|
|
119
|
+
if (!isGateKind(raw.kind)) {
|
|
120
|
+
return failure(stageId, "completion", `checks[${index}].kind must be one of ${STAGE_GATE_KINDS.join(", ")}`);
|
|
121
|
+
}
|
|
122
|
+
return loadSuccess({ id, type: "gate", kind: raw.kind });
|
|
123
|
+
}
|
|
124
|
+
if (raw.type === "checkout_changes") {
|
|
125
|
+
const unknown = hasOnlyKeys(raw, ["id", "type", "path_fields"]);
|
|
126
|
+
if (unknown)
|
|
127
|
+
return failure(stageId, "completion", `checks[${index}]: unknown key "${unknown}"`);
|
|
128
|
+
const pathFields = parsePathFields(raw.path_fields);
|
|
129
|
+
if (raw.path_fields !== undefined && pathFields === undefined) {
|
|
130
|
+
return failure(stageId, "completion", `checks[${index}].path_fields must be a unique, non-empty array of strings`);
|
|
131
|
+
}
|
|
132
|
+
return loadSuccess({
|
|
133
|
+
id,
|
|
134
|
+
type: "checkout_changes",
|
|
135
|
+
...(pathFields !== undefined ? { path_fields: pathFields } : {}),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return failure(stageId, "completion", `checks[${index}].type "${raw.type}" is unsupported`);
|
|
139
|
+
}
|
|
140
|
+
function parseCompletion(raw, stageId) {
|
|
141
|
+
if (raw === undefined)
|
|
142
|
+
return loadSuccess(undefined);
|
|
143
|
+
if (!isPlainObject(raw))
|
|
144
|
+
return failure(stageId, "completion", "must be an object");
|
|
145
|
+
const unknown = hasOnlyKeys(raw, ["mode", "checks"]);
|
|
146
|
+
if (unknown)
|
|
147
|
+
return failure(stageId, "completion", `unknown key "${unknown}"`);
|
|
148
|
+
if (raw.mode !== undefined && raw.mode !== "all") {
|
|
149
|
+
return failure(stageId, "completion", 'mode must be "all"');
|
|
150
|
+
}
|
|
151
|
+
if (!Array.isArray(raw.checks) || raw.checks.length === 0) {
|
|
152
|
+
return failure(stageId, "completion", "checks must be a non-empty array");
|
|
153
|
+
}
|
|
154
|
+
const checks = [];
|
|
155
|
+
for (let index = 0; index < raw.checks.length; index++) {
|
|
156
|
+
const outcome = parseCheck(raw.checks[index], stageId, index);
|
|
157
|
+
if (!outcome.ok)
|
|
158
|
+
return outcome;
|
|
159
|
+
checks.push(outcome.value);
|
|
160
|
+
}
|
|
161
|
+
const duplicates = checks.find((check, index) => checks.findIndex((item) => item.id === check.id) !== index);
|
|
162
|
+
if (duplicates)
|
|
163
|
+
return failure(stageId, "completion", `check id "${duplicates.id}" is duplicated`);
|
|
164
|
+
return loadSuccess({ mode: "all", checks });
|
|
165
|
+
}
|
|
166
|
+
function parseRecovery(raw, stageId, hasCompletion) {
|
|
167
|
+
if (raw === undefined)
|
|
168
|
+
return loadSuccess(undefined);
|
|
169
|
+
if (!hasCompletion)
|
|
170
|
+
return failure(stageId, "recovery", "requires a completion contract");
|
|
171
|
+
if (!isPlainObject(raw))
|
|
172
|
+
return failure(stageId, "recovery", "must be an object");
|
|
173
|
+
const unknown = hasOnlyKeys(raw, ["mode", "max_attempts", "retry_safety", "include_failed_checks"]);
|
|
174
|
+
if (unknown)
|
|
175
|
+
return failure(stageId, "recovery", `unknown key "${unknown}"`);
|
|
176
|
+
if (raw.mode !== "repair" && raw.mode !== "manual") {
|
|
177
|
+
return failure(stageId, "recovery", 'mode must be "repair" or "manual"');
|
|
178
|
+
}
|
|
179
|
+
if (raw.retry_safety !== "idempotent" && raw.retry_safety !== "side_effecting") {
|
|
180
|
+
return failure(stageId, "recovery", 'retry_safety must be "idempotent" or "side_effecting"');
|
|
181
|
+
}
|
|
182
|
+
if (raw.include_failed_checks !== undefined && typeof raw.include_failed_checks !== "boolean") {
|
|
183
|
+
return failure(stageId, "recovery", "include_failed_checks must be a boolean");
|
|
184
|
+
}
|
|
185
|
+
if (raw.mode === "repair") {
|
|
186
|
+
if (!isPositiveInteger(raw.max_attempts)) {
|
|
187
|
+
return failure(stageId, "recovery", "repair requires max_attempts to be a positive integer");
|
|
188
|
+
}
|
|
189
|
+
if (raw.retry_safety !== "idempotent") {
|
|
190
|
+
return failure(stageId, "recovery", "automatic repair requires retry_safety: idempotent");
|
|
191
|
+
}
|
|
192
|
+
return loadSuccess({
|
|
193
|
+
mode: "repair",
|
|
194
|
+
max_attempts: raw.max_attempts,
|
|
195
|
+
retry_safety: "idempotent",
|
|
196
|
+
include_failed_checks: raw.include_failed_checks ?? true,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
return loadSuccess({
|
|
200
|
+
mode: "manual",
|
|
201
|
+
retry_safety: raw.retry_safety,
|
|
202
|
+
...(typeof raw.include_failed_checks === "boolean"
|
|
203
|
+
? { include_failed_checks: raw.include_failed_checks }
|
|
204
|
+
: {}),
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
export function parseExecutionPolicy(raw, stageId) {
|
|
208
|
+
const completion = parseCompletion(raw.completion, stageId);
|
|
209
|
+
if (!completion.ok)
|
|
210
|
+
return completion;
|
|
211
|
+
const recovery = parseRecovery(raw.recovery, stageId, completion.value !== undefined);
|
|
212
|
+
if (!recovery.ok)
|
|
213
|
+
return recovery;
|
|
214
|
+
return loadSuccess({
|
|
215
|
+
...(completion.value !== undefined ? { completion: completion.value } : {}),
|
|
216
|
+
...(recovery.value !== undefined ? { recovery: recovery.value } : {}),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { STAGE_GATE_KINDS } from "../types/stage.js";
|
|
2
|
+
import { loadFailure, loadSuccess } from "./loadOutcome.js";
|
|
3
|
+
function isPlainObject(value) {
|
|
4
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5
|
+
}
|
|
6
|
+
function isGateKind(value) {
|
|
7
|
+
return typeof value === "string" && STAGE_GATE_KINDS.includes(value);
|
|
8
|
+
}
|
|
9
|
+
function failure(label, message) {
|
|
10
|
+
return loadFailure([
|
|
11
|
+
{
|
|
12
|
+
code: "stage.invalid_pre_emit_checks",
|
|
13
|
+
message: `Invalid stage ${label}: pre_emit_checks ${message}`,
|
|
14
|
+
category: "stage",
|
|
15
|
+
},
|
|
16
|
+
]);
|
|
17
|
+
}
|
|
18
|
+
function parsePreEmitCheck(raw, label, index) {
|
|
19
|
+
if (!isPlainObject(raw)) {
|
|
20
|
+
return failure(label, `[${index}] must be an object`);
|
|
21
|
+
}
|
|
22
|
+
if (typeof raw.id !== "string" || raw.id.trim() === "") {
|
|
23
|
+
return failure(label, `[${index}].id must be a non-empty string`);
|
|
24
|
+
}
|
|
25
|
+
if (typeof raw.type !== "string") {
|
|
26
|
+
return failure(label, `[${index}].type must be a string`);
|
|
27
|
+
}
|
|
28
|
+
const id = raw.id.trim();
|
|
29
|
+
if (raw.type === "gate") {
|
|
30
|
+
if (!isGateKind(raw.kind)) {
|
|
31
|
+
return failure(label, `[${index}].kind must be one of ${STAGE_GATE_KINDS.join(", ")}`);
|
|
32
|
+
}
|
|
33
|
+
return loadSuccess({ id, type: "gate", kind: raw.kind });
|
|
34
|
+
}
|
|
35
|
+
if (raw.type === "artifact_declared") {
|
|
36
|
+
if (typeof raw.basename !== "string" || raw.basename.trim() === "") {
|
|
37
|
+
return failure(label, `[${index}].basename must be a non-empty string`);
|
|
38
|
+
}
|
|
39
|
+
return loadSuccess({
|
|
40
|
+
id,
|
|
41
|
+
type: "artifact_declared",
|
|
42
|
+
basename: raw.basename.trim(),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return failure(label, `[${index}].type "${raw.type}" is unsupported`);
|
|
46
|
+
}
|
|
47
|
+
export function parsePreEmitChecks(raw, label) {
|
|
48
|
+
if (raw === undefined)
|
|
49
|
+
return loadSuccess(undefined);
|
|
50
|
+
if (!Array.isArray(raw) || raw.length === 0) {
|
|
51
|
+
return failure(label, "must be a non-empty array");
|
|
52
|
+
}
|
|
53
|
+
const checks = [];
|
|
54
|
+
for (let index = 0; index < raw.length; index++) {
|
|
55
|
+
const outcome = parsePreEmitCheck(raw[index], label, index);
|
|
56
|
+
if (!outcome.ok)
|
|
57
|
+
return outcome;
|
|
58
|
+
checks.push(outcome.value);
|
|
59
|
+
}
|
|
60
|
+
const duplicate = checks.find((check, index) => checks.findIndex((item) => item.id === check.id) !== index);
|
|
61
|
+
if (duplicate) {
|
|
62
|
+
return failure(label, `id "${duplicate.id}" is duplicated`);
|
|
63
|
+
}
|
|
64
|
+
return loadSuccess(checks);
|
|
65
|
+
}
|