akm-cli 0.9.2-alpha.4 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +493 -0
- package/STABILITY.md +23 -5
- package/dist/assets/hints/cli-hints-full.md +12 -7
- package/dist/assets/tasks/core/extract.yml +3 -5
- package/dist/assets/tasks/core/improve.yml +3 -5
- package/dist/assets/tasks/core/index-refresh.yml +3 -5
- package/dist/assets/tasks/core/sync.yml +3 -5
- package/dist/assets/tasks/core/version-check.yml +3 -5
- package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +3 -5
- package/dist/assets/tasks/improve/akm-improve-catchup.yml +6 -6
- package/dist/assets/tasks/improve/akm-improve-consolidate.yml +3 -5
- package/dist/assets/tasks/improve/akm-improve-frequent.yml +3 -5
- package/dist/assets/tasks/improve/akm-improve-nightly.yml +3 -5
- package/dist/cli/unknown-flags.js +12 -1
- package/dist/cli.js +8 -1
- package/dist/commands/command/command-execution.js +23 -2
- package/dist/commands/health/improve-metrics.js +38 -0
- package/dist/commands/health/windows.js +8 -4
- package/dist/commands/health.js +8 -4
- package/dist/commands/lint/index.js +1 -1
- package/dist/commands/migrate-cli.js +130 -24
- package/dist/commands/proposal/validators/proposal-validators.js +7 -2
- package/dist/commands/tasks/explain.js +304 -0
- package/dist/commands/tasks/tasks-cli.js +185 -3
- package/dist/commands/tasks/tasks.js +265 -52
- package/dist/commands/workflow/plan.js +159 -0
- package/dist/commands/workflow-cli.js +94 -2
- package/dist/core/activation-policy.js +2 -12
- package/dist/core/adapter/adapters/akm-lint.js +7 -4
- package/dist/core/adapter/adapters/akm-metadata.js +26 -14
- package/dist/core/adapter/adapters/akm-task-adapter.js +13 -10
- package/dist/core/errors.js +45 -0
- package/dist/core/json-schema.js +15 -5
- package/dist/core/state/migrations.js +57 -0
- package/dist/core/state-db.js +16 -14
- package/dist/core/subprocess.js +47 -13
- package/dist/execution/guarded-source.js +44 -0
- package/dist/execution/input-contract.js +250 -0
- package/dist/execution/target-ref.js +63 -0
- package/dist/indexer/usage/usage-events.js +14 -3
- package/dist/integrations/agent/execution-lowering.js +12 -1
- package/dist/output/shapes/passthrough.js +2 -0
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/migrate.js +12 -3
- package/dist/output/text/workflow-format.js +192 -10
- package/dist/output/text/workflow.js +2 -1
- package/dist/runtime.js +1 -0
- package/dist/scripts/akm-migrate-node.js +11838 -10118
- package/dist/scripts/akm-migrate.js +11828 -10117
- package/dist/setup/steps/tasks.js +34 -17
- package/dist/storage/repositories/task-history-repository.js +5 -1
- package/dist/storage/repositories/workflow-runs-repository.js +144 -6
- package/dist/tasks/backends/launchd.js +31 -84
- package/dist/tasks/embedded.js +13 -7
- package/dist/tasks/model/invocation.js +4 -0
- package/dist/tasks/prepare/prepare-script-target.js +9 -0
- package/dist/tasks/prepare/prepare-support.js +154 -0
- package/dist/tasks/prepare/prepare.js +117 -0
- package/dist/tasks/prepare/prepared-execution.js +4 -0
- package/dist/tasks/prepare/script-capture.js +80 -0
- package/dist/tasks/run/attempt-lifecycle.js +165 -0
- package/dist/tasks/run/load-task.js +117 -0
- package/dist/tasks/run/provenance.js +20 -0
- package/dist/tasks/run/run-command-task.js +92 -0
- package/dist/tasks/run/run-native-task.js +222 -0
- package/dist/tasks/run/run-task.js +99 -0
- package/dist/tasks/run/run-workflow-task.js +222 -0
- package/dist/tasks/run/task-history.js +134 -0
- package/dist/tasks/run/task-log.js +179 -0
- package/dist/tasks/run/task-result.js +19 -0
- package/dist/tasks/scheduler-binding.js +66 -2
- package/dist/tasks/scheduler-invocation.js +63 -3
- package/dist/tasks/scheduler-sync.js +77 -14
- package/dist/tasks/source/bounded-document.js +455 -0
- package/dist/tasks/source/parse-task-source.js +59 -0
- package/dist/tasks/source/project-v4.js +62 -0
- package/dist/tasks/source/task-input-diagnostics.js +36 -0
- package/dist/tasks/source/task-source-v4.js +626 -0
- package/dist/tasks/source-v3.js +10 -733
- package/dist/tasks/task-run-reserved-flags.js +79 -0
- package/dist/workflows/authoring/authoring.js +17 -8
- package/dist/workflows/exec/child-invocation.js +34 -0
- package/dist/workflows/exec/child-workflow.js +370 -0
- package/dist/workflows/exec/exec-unit.js +50 -170
- package/dist/workflows/exec/frozen-judge.js +19 -2
- package/dist/workflows/exec/native-executor.js +49 -27
- package/dist/workflows/exec/param-secrets.js +12 -0
- package/dist/workflows/exec/run-workflow.js +48 -59
- package/dist/workflows/exec/step-work.js +222 -80
- package/dist/workflows/exec/unit-dispatch.js +72 -0
- package/dist/workflows/freeze/child-output-references.js +94 -0
- package/dist/workflows/freeze/environment.js +174 -0
- package/dist/workflows/freeze/identity.js +22 -0
- package/dist/workflows/freeze/resolve-steps.js +78 -0
- package/dist/workflows/freeze/source-freeze.js +57 -0
- package/dist/workflows/freeze/step-values.js +68 -0
- package/dist/workflows/freeze/targets/child-workflow.js +206 -0
- package/dist/workflows/freeze/targets/command.js +81 -0
- package/dist/workflows/freeze/targets/script.js +57 -0
- package/dist/workflows/freeze/targets/shell.js +31 -0
- package/dist/workflows/freeze/targets/task.js +179 -0
- package/dist/workflows/freeze/task-bindings.js +180 -0
- package/dist/workflows/ir/compile.js +59 -11
- package/dist/workflows/ir/environment-v4.js +3 -3
- package/dist/workflows/ir/freeze-v4.js +41 -7
- package/dist/workflows/ir/params.js +58 -131
- package/dist/workflows/ir/plan-hash.js +3 -3
- package/dist/workflows/ir/schema-v4.js +246 -17
- package/dist/workflows/parser.js +74 -2
- package/dist/workflows/program/schema.js +5 -2
- package/dist/workflows/resource-limits.js +20 -0
- package/dist/workflows/runtime/plan-classifier.js +24 -7
- package/dist/workflows/runtime/run-outputs.js +103 -0
- package/dist/workflows/runtime/runs.js +114 -9
- package/dist/workflows/runtime/workflow-asset-loader.js +14 -6
- package/dist/workflows/source-files.js +5 -5
- package/dist/workflows/source-ir/compare.js +17 -0
- package/dist/workflows/source-ir/compile.js +7 -3
- package/dist/workflows/source-ir/github-yaml.js +64 -17
- package/dist/workflows/source-ir/schema.js +69 -21
- package/dist/workflows/source-ir/semantics.js +7 -25
- package/dist/workflows/source-ir/triggers.js +79 -0
- package/dist/workflows/source-ir/uses.js +33 -7
- package/docs/migration/README.md +1 -1
- package/docs/migration/release-notes/0.9.2.md +87 -11
- package/docs/migration/release-notes/README.md +3 -2
- package/docs/migration/v0.8-to-v0.9.md +13 -11
- package/docs/migration/v0.9.0-troubleshooting.md +20 -13
- package/docs/migration/v0.9.1-to-v0.9.2.md +598 -49
- package/docs/reference/README.md +1 -1
- package/docs/reference/cli.md +140 -46
- package/docs/reference/configuration.md +6 -5
- package/docs/reference/supported-formats.md +9 -5
- package/docs/reference/tasks.md +338 -75
- package/docs/reference/workflow-schema.md +290 -16
- package/docs/reference/workflows.md +57 -7
- package/package.json +1 -1
- package/schemas/akm-task.json +173 -118
- package/schemas/akm-workflow.json +28 -0
- package/dist/tasks/runner.js +0 -941
- package/dist/tasks/runtime-v3.js +0 -281
- package/dist/workflows/ir/source-freeze-v4.js +0 -506
- package/dist/workflows/source-ir/ordering.js +0 -38
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { prepareCommandInvocation } from "../../../commands/command/command-execution.js";
|
|
6
|
+
import { captureFrozenDirectoryIdentity } from "../../../execution/directory-identity.js";
|
|
7
|
+
import { freezeExecutableIdentity } from "../../../execution/executable-identity.js";
|
|
8
|
+
import { canonicalResolvedExecutionRequest, } from "../../../execution/resolved-request.js";
|
|
9
|
+
import { fallbackAnnouncement } from "../../../integrations/agent/engine-fallback.js";
|
|
10
|
+
import { requireAuthorizedExecutionPlan } from "../../../integrations/agent/execution-cascade.js";
|
|
11
|
+
import { lowerResolvedExecutionRequest } from "../../../integrations/agent/execution-lowering.js";
|
|
12
|
+
import { prepareInlineExecution } from "../../../integrations/agent/inline-execution.js";
|
|
13
|
+
import { freezeEnvironment, guardedExecutionSource } from "../environment.js";
|
|
14
|
+
import { gitIdentity } from "../identity.js";
|
|
15
|
+
import { durableRequest, executionUnitValues, executionValues, targetConcurrency, } from "../step-values.js";
|
|
16
|
+
export async function commandDispatch(source, baseUnit, action, context) {
|
|
17
|
+
const prepared = await prepareCommandInvocation({
|
|
18
|
+
action,
|
|
19
|
+
config: context.config,
|
|
20
|
+
invocationKind: "workflow",
|
|
21
|
+
...(context.sourceIr.defaults
|
|
22
|
+
? { invocationDefaults: executionUnitValues(context.sourceIr.defaults, context.asset.sourcePath) }
|
|
23
|
+
: {}),
|
|
24
|
+
...(source.commandMode === "literal" ? { inlineContentMode: "literal" } : {}),
|
|
25
|
+
current: executionValues(source, context.asset.sourcePath),
|
|
26
|
+
sourceLoader: (ref, kind) => guardedExecutionSource(ref, kind, context),
|
|
27
|
+
});
|
|
28
|
+
return commandResult(source, baseUnit, prepared, context);
|
|
29
|
+
}
|
|
30
|
+
export function inlineDispatch(source, baseUnit, context) {
|
|
31
|
+
const content = source.instructions ?? `Execute workflow step ${source.id}.`;
|
|
32
|
+
const prepared = prepareInlineExecution({
|
|
33
|
+
content,
|
|
34
|
+
config: context.config,
|
|
35
|
+
invocationKind: "workflow",
|
|
36
|
+
...(context.sourceIr.defaults
|
|
37
|
+
? { invocationDefaults: executionUnitValues(context.sourceIr.defaults, context.asset.sourcePath) }
|
|
38
|
+
: {}),
|
|
39
|
+
current: executionValues(source, context.asset.sourcePath),
|
|
40
|
+
});
|
|
41
|
+
return commandResult(source, baseUnit, prepared, context);
|
|
42
|
+
}
|
|
43
|
+
export function commandResult(source, baseUnit, prepared, context, literals = []) {
|
|
44
|
+
const request = durableRequest(requireAuthorizedExecutionPlan(prepared.plan));
|
|
45
|
+
const lowered = lowerResolvedExecutionRequest(request, prepared.config);
|
|
46
|
+
const cwdIdentity = captureFrozenDirectoryIdentity(context.asset.sourcePath);
|
|
47
|
+
let runner = lowered.runner;
|
|
48
|
+
let executable;
|
|
49
|
+
if (runner.kind === "agent") {
|
|
50
|
+
executable = freezeExecutableIdentity(runner.profile.bin, { cwd: cwdIdentity.realCwd });
|
|
51
|
+
runner = Object.freeze({ ...runner, profile: Object.freeze({ ...runner.profile, bin: executable.absolutePath }) });
|
|
52
|
+
}
|
|
53
|
+
const unit = {
|
|
54
|
+
...baseUnit,
|
|
55
|
+
engine: request.engine.name,
|
|
56
|
+
...(request.model ? { model: request.model.resolved } : {}),
|
|
57
|
+
...(Object.hasOwn(request.runtime, "timeoutMs") ? { timeoutMs: request.runtime.timeoutMs } : {}),
|
|
58
|
+
...(request.inference ? { llm: request.inference } : {}),
|
|
59
|
+
...(request.outputSchema ? { output: request.outputSchema } : {}),
|
|
60
|
+
};
|
|
61
|
+
const environment = Object.freeze([...literals, ...freezeEnvironment(source, undefined, context)]);
|
|
62
|
+
const target = Object.freeze({
|
|
63
|
+
kind: "command",
|
|
64
|
+
ref: request.command.source?.ref ?? null,
|
|
65
|
+
contentHash: createHash("sha256").update(request.command.content).digest("hex"),
|
|
66
|
+
request: JSON.parse(canonicalResolvedExecutionRequest(request)),
|
|
67
|
+
runner,
|
|
68
|
+
...(targetConcurrency(runner, context.config) ? { concurrency: targetConcurrency(runner, context.config) } : {}),
|
|
69
|
+
cwdIdentity,
|
|
70
|
+
...(executable ? { executable } : {}),
|
|
71
|
+
...gitIdentity(baseUnit, cwdIdentity.realRoot),
|
|
72
|
+
});
|
|
73
|
+
const engineAnnouncement = fallbackAnnouncement(prepared.fallbackEngineName, request.engine.name);
|
|
74
|
+
return {
|
|
75
|
+
target,
|
|
76
|
+
environment,
|
|
77
|
+
unit,
|
|
78
|
+
instructions: request.command.content,
|
|
79
|
+
...(engineAnnouncement ? { engineAnnouncement } : {}),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
import { freezeExecutableIdentity } from "../../../execution/executable-identity.js";
|
|
5
|
+
import { prepareScriptTarget } from "../../../tasks/prepare/prepare-script-target.js";
|
|
6
|
+
import { captureOwned, freezeEnvironment, resolveOwnedAsset } from "../environment.js";
|
|
7
|
+
import { gitIdentity, scriptExecutable } from "../identity.js";
|
|
8
|
+
import { freezeExecSpec } from "../step-values.js";
|
|
9
|
+
export async function directScript(source, baseUnit, refInput, context) {
|
|
10
|
+
const owned = await resolveOwnedAsset(refInput, "script", context);
|
|
11
|
+
captureOwned(owned, context.collector);
|
|
12
|
+
// Typed preparer (P1b spec §4.3) — no synthetic task YAML, no parseTaskV3Yaml
|
|
13
|
+
// call, no fabricated schedule/filePath/taskId/taskRef. The script's own
|
|
14
|
+
// owned identity (ref/file/bundleRoot) is all prepareScriptTarget needs.
|
|
15
|
+
const captured = prepareScriptTarget({
|
|
16
|
+
ref: owned.ref,
|
|
17
|
+
file: owned.file,
|
|
18
|
+
bundleRoot: owned.root,
|
|
19
|
+
readFile: () => context.collector.readBytes(owned.file, owned.root),
|
|
20
|
+
});
|
|
21
|
+
return scriptResult(source, baseUnit, {
|
|
22
|
+
sourceRef: captured.ref,
|
|
23
|
+
interpreter: captured.interpreter,
|
|
24
|
+
extension: captured.extension,
|
|
25
|
+
bytesBase64: captured.bytesBase64,
|
|
26
|
+
byteLength: captured.byteLength,
|
|
27
|
+
sha256: captured.sha256,
|
|
28
|
+
cwdIdentity: captured.cwdIdentity,
|
|
29
|
+
}, context, []);
|
|
30
|
+
}
|
|
31
|
+
export function scriptResult(source, baseUnit, prepared, context, literals) {
|
|
32
|
+
const requestedExecutable = scriptExecutable(prepared.interpreter);
|
|
33
|
+
const executable = freezeExecutableIdentity(requestedExecutable, { cwd: prepared.cwdIdentity.realCwd });
|
|
34
|
+
const authoredExec = { command: [executable.absolutePath, "<frozen-script>"] };
|
|
35
|
+
const exec = freezeExecSpec(source, authoredExec, context);
|
|
36
|
+
const environment = Object.freeze([...literals, ...freezeEnvironment(source, authoredExec, context)]);
|
|
37
|
+
const target = Object.freeze({
|
|
38
|
+
kind: "script",
|
|
39
|
+
ref: prepared.sourceRef,
|
|
40
|
+
contentHash: prepared.sha256,
|
|
41
|
+
exec,
|
|
42
|
+
interpreter: prepared.interpreter,
|
|
43
|
+
extension: prepared.extension,
|
|
44
|
+
bytesBase64: prepared.bytesBase64,
|
|
45
|
+
byteLength: prepared.byteLength,
|
|
46
|
+
cwdIdentity: prepared.cwdIdentity,
|
|
47
|
+
materialization: "ephemeral-0700-delete",
|
|
48
|
+
executable,
|
|
49
|
+
...gitIdentity(baseUnit, prepared.cwdIdentity.realRoot),
|
|
50
|
+
});
|
|
51
|
+
return {
|
|
52
|
+
target,
|
|
53
|
+
environment,
|
|
54
|
+
unit: { ...baseUnit, exec: authoredExec },
|
|
55
|
+
instructions: source.instructions ?? `Run script ${prepared.sourceRef}.`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
import { captureFrozenDirectoryIdentity } from "../../../execution/directory-identity.js";
|
|
5
|
+
import { freezeExecutableIdentity } from "../../../execution/executable-identity.js";
|
|
6
|
+
import { freezeEnvironment } from "../environment.js";
|
|
7
|
+
import { gitIdentity } from "../identity.js";
|
|
8
|
+
import { freezeExecSpec } from "../step-values.js";
|
|
9
|
+
export function directShell(source, baseUnit, context) {
|
|
10
|
+
const authoredExec = baseUnit.exec;
|
|
11
|
+
if (!authoredExec)
|
|
12
|
+
throw new Error(`workflow shell step ${source.id} lost its source-IR execution spec`);
|
|
13
|
+
const exec = freezeExecSpec(source, authoredExec, context);
|
|
14
|
+
const cwdIdentity = captureFrozenDirectoryIdentity(context.asset.sourcePath, authoredExec.cwd);
|
|
15
|
+
const executable = freezeExecutableIdentity(authoredExec.command[0], { cwd: cwdIdentity.realCwd });
|
|
16
|
+
const environment = Object.freeze(freezeEnvironment(source, authoredExec, context));
|
|
17
|
+
const target = Object.freeze({
|
|
18
|
+
kind: "shell",
|
|
19
|
+
contentHash: "",
|
|
20
|
+
exec,
|
|
21
|
+
cwdIdentity,
|
|
22
|
+
executable,
|
|
23
|
+
...gitIdentity(baseUnit, cwdIdentity.realRoot),
|
|
24
|
+
});
|
|
25
|
+
return {
|
|
26
|
+
target,
|
|
27
|
+
environment,
|
|
28
|
+
unit: { ...baseUnit, exec },
|
|
29
|
+
instructions: source.instructions ?? `Run ${source.run ?? authoredExec.command.join(" ")}.`,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { parseBundleRef } from "../../../core/asset/asset-ref.js";
|
|
6
|
+
import { UsageError } from "../../../core/errors.js";
|
|
7
|
+
import { freezeExecutableIdentity } from "../../../execution/executable-identity.js";
|
|
8
|
+
import { prepareTaskV3Execution } from "../../../tasks/prepare/prepare.js";
|
|
9
|
+
import { parseTaskSource } from "../../../tasks/source/parse-task-source.js";
|
|
10
|
+
import { projectTaskSourceV4 } from "../../../tasks/source/project-v4.js";
|
|
11
|
+
import { workflowShellCommand } from "../../source-ir/program.js";
|
|
12
|
+
import { captureOwned, freezeEnvironment, guardedExecutionSource, resolveOwnedAsset } from "../environment.js";
|
|
13
|
+
import { gitIdentity } from "../identity.js";
|
|
14
|
+
import { declaredParamNames, earlierStepIds, freezeExecSpec, } from "../step-values.js";
|
|
15
|
+
import { freezeTaskInputBindings } from "../task-bindings.js";
|
|
16
|
+
import { childWorkflowDispatch } from "./child-workflow.js";
|
|
17
|
+
import { commandResult } from "./command.js";
|
|
18
|
+
import { scriptResult } from "./script.js";
|
|
19
|
+
/** The one message shared by every "cannot prove this is a valid binding surface" rejection (A-N5). */
|
|
20
|
+
function noDeclaredInputsError(stepId, ref) {
|
|
21
|
+
return new UsageError(`Workflow step ${stepId} cannot pass with: to task target ${ref}; ${ref} declares no inputs.`, "COMPOSITION_INVALID");
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A-N6 (spec docs/plans/specs/p2b-input-bindings.md §1.7): LC-N1's
|
|
25
|
+
* peek-and-throw (p2a §1.5) is GONE — a version: 4 target now composes.
|
|
26
|
+
* `parseTaskSource` parses the YAML ONCE and routes on the SAME {root,
|
|
27
|
+
* lineAt} the v3 arm always used, so a v3 composition stays byte-identical
|
|
28
|
+
* (B-07, B-08). `contract` is undefined for a v3 task (which can never
|
|
29
|
+
* declare `inputs:`, P2a §1.2 D2) or a v4 task with no `inputs:` key at all —
|
|
30
|
+
* either way, "no declared inputs" (A-N5).
|
|
31
|
+
*
|
|
32
|
+
* RECORDED TENSION (spec §0, Review log): A-N5's "no declared inputs"
|
|
33
|
+
* rejection is reasoned from the target's PARSED `inputs:` contract, which
|
|
34
|
+
* needs the target to resolve — yet `tests/workflows/with-rejection.test.ts`
|
|
35
|
+
* B-02b pins `COMPOSITION_INVALID` (not an asset-resolution error) for an
|
|
36
|
+
* UNRESOLVABLE task ref with an authored `with:`. Reconciled here: an
|
|
37
|
+
* authored `with:` whose target cannot even be resolved/parsed "cannot be
|
|
38
|
+
* proven a valid binding surface", so it is refused the same
|
|
39
|
+
* no-declared-inputs way — a `with:`-free step's resolution failure is
|
|
40
|
+
* untouched and propagates as before.
|
|
41
|
+
*
|
|
42
|
+
* Code-review finding: that reconciliation must stay scoped to ASSET
|
|
43
|
+
* resolution (`resolveOwnedAsset`/`captureOwned`) only. Once the target
|
|
44
|
+
* resolves to a real file, `parseTaskSource`/`projectTaskSourceV4` reports a
|
|
45
|
+
* genuine, path-and-line-anchored defect in the composed task's OWN source
|
|
46
|
+
* (e.g. `TASK_SOURCE_INVALID` for an unsupported `inputs.<name>` field) —
|
|
47
|
+
* that is never "cannot be proven a valid binding surface" and must not be
|
|
48
|
+
* repainted as `noDeclaredInputsError`. Parsing therefore happens OUTSIDE
|
|
49
|
+
* the try/catch below, so its errors propagate unchanged regardless of
|
|
50
|
+
* whether this step authored a `with:`.
|
|
51
|
+
*/
|
|
52
|
+
async function resolveTaskForComposition(source, refInput, context) {
|
|
53
|
+
const { owned, retained } = await resolveAndCaptureTaskAsset(source, refInput, context);
|
|
54
|
+
const parsed = parseTaskSource({ yaml: retained.content, filePath: owned.file, workspaceRoot: owned.root });
|
|
55
|
+
const contract = parsed.v4.inputs;
|
|
56
|
+
const task = projectTaskSourceV4(parsed.v4);
|
|
57
|
+
return { owned, task, contract };
|
|
58
|
+
}
|
|
59
|
+
/** The asset-resolution-only half of {@link resolveTaskForComposition} — the sole failure mode an authored `with:` on an unresolvable ref may repaint as `noDeclaredInputsError` (A-N5's `with-rejection.test.ts` B-02b). */
|
|
60
|
+
async function resolveAndCaptureTaskAsset(source, refInput, context) {
|
|
61
|
+
try {
|
|
62
|
+
const owned = await resolveOwnedAsset(refInput, "task", context);
|
|
63
|
+
const retained = captureOwned(owned, context.collector);
|
|
64
|
+
return { owned, retained };
|
|
65
|
+
}
|
|
66
|
+
catch (cause) {
|
|
67
|
+
if (source.with !== undefined)
|
|
68
|
+
throw noDeclaredInputsError(source.id, refInput);
|
|
69
|
+
throw cause;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export async function taskDispatch(source, baseUnit, refInput, context) {
|
|
73
|
+
const { owned, task, contract } = await resolveTaskForComposition(source, refInput, context);
|
|
74
|
+
// A-N5: a with: on a target that declares no inputs: at all is
|
|
75
|
+
// COMPOSITION_INVALID. Fires on ANY authored with: shape, including `{}`
|
|
76
|
+
// (the check is `!== undefined`, not "non-empty").
|
|
77
|
+
if (source.with !== undefined && contract === undefined) {
|
|
78
|
+
throw noDeclaredInputsError(source.id, refInput);
|
|
79
|
+
}
|
|
80
|
+
// P3a (docs/plans/specs/p3a-plan-v5-child-freeze.md §4.2, row B-12): a task
|
|
81
|
+
// whose OWN target is `uses: workflows/<ref>` used to fast-fail here,
|
|
82
|
+
// before ever calling prepareTaskV3Execution. That rejection is REMOVED —
|
|
83
|
+
// flow continues to the bindings computation and the prepare call exactly
|
|
84
|
+
// as for any other task target, and `prepared.kind === "workflow"` below
|
|
85
|
+
// routes to the ONE child-workflow resolver instead.
|
|
86
|
+
// Lane A2 (§3.2-§3.5): normalize THIS step's own with: against THIS task's
|
|
87
|
+
// OWN declared inputs — no merge across a composition chain (B-29). A task
|
|
88
|
+
// with no inputs: has contract === undefined, so an empty {} contract is
|
|
89
|
+
// used; freezeTaskInputBindings then produces no bindings for it (there is
|
|
90
|
+
// nothing to bind, and the COMPOSITION_INVALID check above already fired
|
|
91
|
+
// for any authored with:).
|
|
92
|
+
const bindings = freezeTaskInputBindings({
|
|
93
|
+
stepId: source.id,
|
|
94
|
+
targetRef: refInput,
|
|
95
|
+
with: source.with,
|
|
96
|
+
contract: contract ?? {},
|
|
97
|
+
earlierStepIds: earlierStepIds(context.sourceIr, source.id),
|
|
98
|
+
declaredParamNames: declaredParamNames(context.sourceIr),
|
|
99
|
+
});
|
|
100
|
+
const prepared = await prepareTaskV3Execution(task, {
|
|
101
|
+
taskId: parseBundleRef(owned.ref).conceptId.slice("tasks/".length),
|
|
102
|
+
taskRef: owned.ref,
|
|
103
|
+
bundleName: owned.bundle,
|
|
104
|
+
bundleRoot: owned.root,
|
|
105
|
+
config: context.config,
|
|
106
|
+
commandSourceLoader: (ref, kind) => guardedExecutionSource(ref, kind, context),
|
|
107
|
+
resolveAsset: async ({ ref, type }) => {
|
|
108
|
+
const target = await resolveOwnedAsset(ref, type, context);
|
|
109
|
+
captureOwned(target, context.collector);
|
|
110
|
+
return { file: target.file, bundleRoot: target.root };
|
|
111
|
+
},
|
|
112
|
+
readFile: (file, root = owned.root) => context.collector.readBytes(file, root),
|
|
113
|
+
});
|
|
114
|
+
if (prepared.kind === "workflow") {
|
|
115
|
+
// P3a (spec §4.2, rows B-12/B-13): routes to the ONE child-workflow
|
|
116
|
+
// resolver instead of rejecting. `prepared.ref`/`prepared.taskRef` are
|
|
117
|
+
// already qualified (§4.2 step 1 re-resolves and re-canonicalizes them
|
|
118
|
+
// regardless). Always hands over the bindings just computed above — this
|
|
119
|
+
// task's OWN effective inputs, already classified once against its own
|
|
120
|
+
// contract — to be RE-bound against the child's declared `params:`
|
|
121
|
+
// (never round-tripped through the `with:` grammar: doing so would let a
|
|
122
|
+
// literal value shaped like `{from: ...}` be silently reinterpreted as a
|
|
123
|
+
// reference, code-review finding). Task source v4 rejects `with:` on any
|
|
124
|
+
// target but `uses: akm/command` (row B-28), so a workflow-target task
|
|
125
|
+
// can no longer author `with:` at all — the `{kind:"with"}` arm this
|
|
126
|
+
// ternary used to reach for a v3 task, or a v4 task with no `inputs:`,
|
|
127
|
+
// is unreachable from here and deleted (P4 §3.2.7, row B-30).
|
|
128
|
+
// `AuthoredChildInputs`'s `{kind:"with"}` member itself stays — the
|
|
129
|
+
// DIRECT composition path (`resolve-steps.ts`) still produces it.
|
|
130
|
+
return childWorkflowDispatch({
|
|
131
|
+
source,
|
|
132
|
+
baseUnit,
|
|
133
|
+
childRefInput: prepared.ref,
|
|
134
|
+
context,
|
|
135
|
+
via: "task",
|
|
136
|
+
taskRef: prepared.taskRef,
|
|
137
|
+
authoredInputs: { kind: "bindings", value: bindings },
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
const taskLiterals = Object.entries(prepared.environment).map(([name, value]) => Object.freeze({ kind: "literal", name, value }));
|
|
141
|
+
if (prepared.kind === "command") {
|
|
142
|
+
return withInputBindings(commandResult(source, baseUnit, prepared.invocation, context, taskLiterals), bindings);
|
|
143
|
+
}
|
|
144
|
+
if (prepared.kind === "shell") {
|
|
145
|
+
const authoredExec = {
|
|
146
|
+
command: workflowShellCommand(prepared.shell, prepared.command),
|
|
147
|
+
...(prepared.cwdIdentity.realCwd !== prepared.cwdIdentity.realRoot
|
|
148
|
+
? { cwd: path.relative(prepared.cwdIdentity.realRoot, prepared.cwdIdentity.realCwd) }
|
|
149
|
+
: {}),
|
|
150
|
+
};
|
|
151
|
+
const exec = freezeExecSpec(source, authoredExec, context);
|
|
152
|
+
const environment = Object.freeze([...taskLiterals, ...freezeEnvironment(source, authoredExec, context)]);
|
|
153
|
+
const executable = freezeExecutableIdentity(exec.command[0], { cwd: prepared.cwdIdentity.realCwd });
|
|
154
|
+
const target = Object.freeze({
|
|
155
|
+
kind: "shell",
|
|
156
|
+
contentHash: "",
|
|
157
|
+
exec,
|
|
158
|
+
cwdIdentity: prepared.cwdIdentity,
|
|
159
|
+
executable,
|
|
160
|
+
...gitIdentity(baseUnit, prepared.cwdIdentity.realRoot),
|
|
161
|
+
});
|
|
162
|
+
return withInputBindings({
|
|
163
|
+
target,
|
|
164
|
+
environment,
|
|
165
|
+
unit: { ...baseUnit, exec: authoredExec },
|
|
166
|
+
instructions: source.instructions ?? `Run task ${owned.ref}.`,
|
|
167
|
+
}, bindings);
|
|
168
|
+
}
|
|
169
|
+
return withInputBindings(scriptResult(source, baseUnit, prepared, context, taskLiterals), bindings);
|
|
170
|
+
}
|
|
171
|
+
/** Attach the frozen inputBindings (A-N7) to whichever target shape taskDispatch produced. Absent, never [], when empty. */
|
|
172
|
+
function withInputBindings(resolved, bindings) {
|
|
173
|
+
if (bindings.length === 0)
|
|
174
|
+
return resolved;
|
|
175
|
+
return {
|
|
176
|
+
...resolved,
|
|
177
|
+
target: Object.freeze({ ...resolved.target, inputBindings: bindings }),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* The pure `with:` -> `TaskInputBinding[]` normalizer, plus the pure
|
|
6
|
+
* re-binder a recursive composition needs on top of it.
|
|
7
|
+
* {@link freezeTaskInputBindings} classifies a genuinely AUTHORED `with:`
|
|
8
|
+
* record against a contract (called by `freeze/targets/task.ts`'s
|
|
9
|
+
* `taskDispatch`, this step's own `with:` against the composed task's own
|
|
10
|
+
* contract only — no merge across a composition chain).
|
|
11
|
+
* {@link rebindTaskInputBindings} instead re-binds an ALREADY-classified
|
|
12
|
+
* `TaskInputBinding[]` by name against a DIFFERENT contract, trusting each
|
|
13
|
+
* entry's existing `kind` rather than re-deriving it from the value's shape
|
|
14
|
+
* (`freeze/targets/child-workflow.ts`'s only caller). Everything decidable
|
|
15
|
+
* at FREEZE time is decided here; a reference's *resolved* value is
|
|
16
|
+
* validated PRE-ATTEMPT instead (`exec/step-work.ts`). Pure function: no IO,
|
|
17
|
+
* no config reads.
|
|
18
|
+
*
|
|
19
|
+
* See docs/architecture/decisions/0008-task-binding-normalization.md for the
|
|
20
|
+
* full design history, including why re-binding cannot reuse the
|
|
21
|
+
* shape-driven normalizer (a code-review finding).
|
|
22
|
+
*/
|
|
23
|
+
import { UsageError } from "../../core/errors.js";
|
|
24
|
+
import { validateInputs } from "../../execution/input-contract.js";
|
|
25
|
+
import { parseReference } from "../program/expressions.js";
|
|
26
|
+
function inputBindingInvalid(message) {
|
|
27
|
+
return new UsageError(message, "INPUT_BINDING_INVALID");
|
|
28
|
+
}
|
|
29
|
+
function isPlainObject(value) {
|
|
30
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
31
|
+
}
|
|
32
|
+
function unknownBindingNameError(stepId, targetRef, name, declaredNames) {
|
|
33
|
+
return inputBindingInvalid(`Workflow step ${stepId} targets ${targetRef} with.${name}, which is not a declared input. ` +
|
|
34
|
+
`Declared inputs: ${declaredNames.length > 0 ? declaredNames.join(", ") : "(none)"}.`);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Normalize the workflow source front end's authored `with:` record into the
|
|
38
|
+
* `TaskInputBinding[]` a task-composing step's frozen target carries (spec
|
|
39
|
+
* §3.3). Throws `UsageError`/`INPUT_BINDING_INVALID` for the first violation
|
|
40
|
+
* found — per authored entry first (B-11, B-15, B-16, B-17, B-18), then over
|
|
41
|
+
* the whole contract (B-12, B-13). The result is sorted by name; an entry
|
|
42
|
+
* exists only for a declared input with an effective value (authored
|
|
43
|
+
* literal, authored reference, or an applied default) — never for an
|
|
44
|
+
* unsupplied optional input with no default (B-20).
|
|
45
|
+
*/
|
|
46
|
+
export function freezeTaskInputBindings(input) {
|
|
47
|
+
const { stepId, targetRef, contract, earlierStepIds, declaredParamNames } = input;
|
|
48
|
+
const authored = input.with ?? {};
|
|
49
|
+
const declaredNames = Object.keys(contract).sort();
|
|
50
|
+
const byName = new Map();
|
|
51
|
+
for (const [name, value] of Object.entries(authored)) {
|
|
52
|
+
if (!Object.hasOwn(contract, name))
|
|
53
|
+
throw unknownBindingNameError(stepId, targetRef, name, declaredNames);
|
|
54
|
+
const declaration = contract[name];
|
|
55
|
+
if (!declaration)
|
|
56
|
+
throw inputBindingInvalid(`Workflow step ${stepId} targets ${targetRef} with.${name} is invalid.`);
|
|
57
|
+
byName.set(name, normalizeOneEntry(stepId, targetRef, name, value, declaration.schema, earlierStepIds, declaredParamNames));
|
|
58
|
+
}
|
|
59
|
+
return finalizeBindings(stepId, targetRef, contract, byName);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Re-bind an ALREADY-NORMALIZED `TaskInputBinding[]` — a v4 task's own
|
|
63
|
+
* effective inputs, classified once against the TASK's own declared
|
|
64
|
+
* `inputs:` contract by {@link freezeTaskInputBindings} — against a
|
|
65
|
+
* DIFFERENT contract (the child workflow's declared `params:`) by NAME,
|
|
66
|
+
* without re-deriving each entry's literal/reference classification from
|
|
67
|
+
* its value's shape (a code-review finding — see
|
|
68
|
+
* docs/architecture/decisions/0008-task-binding-normalization.md for why a
|
|
69
|
+
* shape-shifting round-trip through {@link normalizeOneEntry} would
|
|
70
|
+
* silently misclassify a literal value shaped like a reference).
|
|
71
|
+
*
|
|
72
|
+
* Per-entry rules, otherwise identical to {@link freezeTaskInputBindings}:
|
|
73
|
+
* an entry naming a key the new `contract` does not declare is
|
|
74
|
+
* `INPUT_BINDING_INVALID` (same message shape); a `kind: "literal"` entry
|
|
75
|
+
* keeps its value verbatim and is validated against the NEW contract's
|
|
76
|
+
* declared schema for that name; a `kind: "reference"` entry keeps its
|
|
77
|
+
* `from` verbatim (the reference target — an earlier step or declared param
|
|
78
|
+
* of the composing workflow — does not change with which contract it is
|
|
79
|
+
* bound against) and its `schema` is re-derived from the NEW contract,
|
|
80
|
+
* matching {@link normalizeOneEntry}'s existing rule that a reference
|
|
81
|
+
* binding's schema always comes from the contract it is bound against; a
|
|
82
|
+
* contract key absent from `bindings` is defaulted or required exactly as
|
|
83
|
+
* {@link freezeTaskInputBindings} does.
|
|
84
|
+
*/
|
|
85
|
+
export function rebindTaskInputBindings(input) {
|
|
86
|
+
const { stepId, targetRef, contract } = input;
|
|
87
|
+
const declaredNames = Object.keys(contract).sort();
|
|
88
|
+
const byName = new Map();
|
|
89
|
+
for (const binding of input.bindings ?? []) {
|
|
90
|
+
if (!Object.hasOwn(contract, binding.name)) {
|
|
91
|
+
throw unknownBindingNameError(stepId, targetRef, binding.name, declaredNames);
|
|
92
|
+
}
|
|
93
|
+
const declaration = contract[binding.name];
|
|
94
|
+
if (!declaration)
|
|
95
|
+
throw inputBindingInvalid(`Workflow step ${stepId} targets ${targetRef} with.${binding.name} is invalid.`);
|
|
96
|
+
byName.set(binding.name, binding.kind === "literal"
|
|
97
|
+
? binding
|
|
98
|
+
: Object.freeze({ kind: "reference", name: binding.name, from: binding.from, schema: declaration.schema }));
|
|
99
|
+
}
|
|
100
|
+
return finalizeBindings(stepId, targetRef, contract, byName);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The tail shared by {@link freezeTaskInputBindings} and
|
|
104
|
+
* {@link rebindTaskInputBindings} once `byName` holds one classified entry
|
|
105
|
+
* per AUTHORED/bound name: apply declared defaults for every remaining
|
|
106
|
+
* contract key (or throw for a required one with none), schema-validate
|
|
107
|
+
* every literal (authored, re-bound, or defaulted) against the contract, and
|
|
108
|
+
* return the result sorted by name.
|
|
109
|
+
*/
|
|
110
|
+
function finalizeBindings(stepId, targetRef, contract, byName) {
|
|
111
|
+
for (const [name, declaration] of Object.entries(contract)) {
|
|
112
|
+
if (byName.has(name))
|
|
113
|
+
continue;
|
|
114
|
+
if (Object.hasOwn(declaration, "default")) {
|
|
115
|
+
byName.set(name, Object.freeze({ kind: "literal", name, value: declaration.default }));
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (declaration.required) {
|
|
119
|
+
throw inputBindingInvalid(`Workflow step ${stepId} targets ${targetRef}, which declares required input "${name}" with no default; ` +
|
|
120
|
+
`supply it with with.${name}.`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Every LITERAL value (authored, re-bound, or defaulted) is validated
|
|
124
|
+
// against its own declared schema — a contract NARROWED to just the
|
|
125
|
+
// literal-bound names, so a required input bound via REFERENCE (whose
|
|
126
|
+
// value is not known until pre-attempt, §3.6) is never wrongly flagged
|
|
127
|
+
// "missing" here.
|
|
128
|
+
const literalContract = {};
|
|
129
|
+
const literalValues = {};
|
|
130
|
+
for (const binding of byName.values()) {
|
|
131
|
+
if (binding.kind !== "literal")
|
|
132
|
+
continue;
|
|
133
|
+
const declaration = contract[binding.name];
|
|
134
|
+
if (declaration)
|
|
135
|
+
literalContract[binding.name] = declaration;
|
|
136
|
+
literalValues[binding.name] = binding.value;
|
|
137
|
+
}
|
|
138
|
+
const schemaErrors = validateInputs(literalContract, literalValues, { pathRoot: "with" });
|
|
139
|
+
if (schemaErrors.length > 0) {
|
|
140
|
+
throw inputBindingInvalid(`Workflow step ${stepId} targets ${targetRef}: ${schemaErrors.join("; ")}`);
|
|
141
|
+
}
|
|
142
|
+
const sorted = [...byName.values()].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
143
|
+
return Object.freeze(sorted);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Classify and validate ONE authored `with:` entry whose key is already
|
|
147
|
+
* known to be a declared input name. §3.3 point 2/3: a value is a
|
|
148
|
+
* `{kind:"reference"}` binding IFF it is a non-null, non-array plain object
|
|
149
|
+
* whose OWN key set is exactly `["from"]` and whose `from` is a string
|
|
150
|
+
* `parseReference` accepts — the hard-fail band (B-15, B-16) means any OTHER
|
|
151
|
+
* shape carrying an own `from` key is `INPUT_BINDING_INVALID`, never
|
|
152
|
+
* reinterpreted as a literal.
|
|
153
|
+
*/
|
|
154
|
+
function normalizeOneEntry(stepId, targetRef, name, value, schema, earlierStepIds, declaredParamNames) {
|
|
155
|
+
if (!isPlainObject(value) || !Object.hasOwn(value, "from")) {
|
|
156
|
+
return Object.freeze({ kind: "literal", name, value });
|
|
157
|
+
}
|
|
158
|
+
const keys = Object.keys(value);
|
|
159
|
+
if (keys.length !== 1 || typeof value.from !== "string") {
|
|
160
|
+
throw inputBindingInvalid(`Workflow step ${stepId} targets ${targetRef} with.${name} looks like a reference binding ({from: ...}) ` +
|
|
161
|
+
`but is not one: it must have exactly one key, "from", whose value is a reference string.`);
|
|
162
|
+
}
|
|
163
|
+
const parsed = parseReference(value.from);
|
|
164
|
+
if (!parsed.ok) {
|
|
165
|
+
throw inputBindingInvalid(`Workflow step ${stepId} targets ${targetRef} with.${name} reference ${JSON.stringify(value.from)} is ` +
|
|
166
|
+
`invalid: ${parsed.message}`);
|
|
167
|
+
}
|
|
168
|
+
if (parsed.expr.kind === "stepOutput") {
|
|
169
|
+
if (!earlierStepIds.has(parsed.expr.stepId)) {
|
|
170
|
+
throw inputBindingInvalid(`Workflow step ${stepId} targets ${targetRef} with.${name} reference ${value.from} does not name an ` +
|
|
171
|
+
`earlier step of this workflow.`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
else if (!declaredParamNames.has(parsed.expr.name)) {
|
|
175
|
+
const sortedParams = [...declaredParamNames].sort();
|
|
176
|
+
throw inputBindingInvalid(`Workflow step ${stepId} targets ${targetRef} with.${name} reference ${value.from} does not name a ` +
|
|
177
|
+
`declared workflow param; declared params: ${sortedParams.length > 0 ? sortedParams.join(", ") : "(none)"}.`);
|
|
178
|
+
}
|
|
179
|
+
return Object.freeze({ kind: "reference", name, from: value.from, schema });
|
|
180
|
+
}
|
|
@@ -38,17 +38,9 @@ import { decodeWorkflowSourceIrV1, } from "../source-ir/schema.js";
|
|
|
38
38
|
export function compileWorkflowPlan(input, title, resolvedUnits = new Map()) {
|
|
39
39
|
const sourceIr = decodeWorkflowSourceIrV1(input);
|
|
40
40
|
const errors = [];
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
errors: [
|
|
45
|
-
{
|
|
46
|
-
line: sourceIr.jobs[1]?.source.start ?? sourceIr.source.start,
|
|
47
|
-
message: "Current workflow execution requires exactly one source-IR job.",
|
|
48
|
-
},
|
|
49
|
-
],
|
|
50
|
-
};
|
|
51
|
-
}
|
|
41
|
+
// The decoder guarantees exactly one job (P4 §3.3, docs/plans/specs/
|
|
42
|
+
// p4-deletions-closeout.md, row B-43) — a 2+-job document is rejected at
|
|
43
|
+
// the adapter boundary and never reaches this compiler.
|
|
52
44
|
const sourceSteps = sourceIr.jobs[0]?.steps ?? [];
|
|
53
45
|
const allStepIds = new Set(sourceSteps.map((step) => step.id));
|
|
54
46
|
const earlierStepIds = new Set();
|
|
@@ -75,6 +67,13 @@ export function compileWorkflowPlan(input, title, resolvedUnits = new Map()) {
|
|
|
75
67
|
steps.push(compileStep(step, sequenceIndex, sourceIr.defaults, resolvedUnits.get(step.id)));
|
|
76
68
|
earlierStepIds.add(step.id);
|
|
77
69
|
});
|
|
70
|
+
// P3b (spec §4.2, rows B-06/B-07): `outputs:` references are validated
|
|
71
|
+
// against the FULL step id set (never `earlierStepIds` — resolution happens
|
|
72
|
+
// at RUN COMPLETION, after every step has run, so an output may legitimately
|
|
73
|
+
// name any declared step regardless of its position).
|
|
74
|
+
for (const [name, declaration] of Object.entries(sourceIr.outputs ?? {})) {
|
|
75
|
+
checkOutputReference(name, declaration.from, { errors, allStepIds, line: sourceIr.source.start });
|
|
76
|
+
}
|
|
78
77
|
if (errors.length > 0)
|
|
79
78
|
return { ok: false, errors };
|
|
80
79
|
const paramNames = sourceIr.params ? Object.keys(sourceIr.params) : [];
|
|
@@ -87,6 +86,7 @@ export function compileWorkflowPlan(input, title, resolvedUnits = new Map()) {
|
|
|
87
86
|
...(sourceIr.params && paramNames.length > 0
|
|
88
87
|
? { paramSchemas: sourceIr.params }
|
|
89
88
|
: {}),
|
|
89
|
+
...(sourceIr.outputs ? { outputs: sortedOutputs(sourceIr.outputs) } : {}),
|
|
90
90
|
...(sourceIr.budget
|
|
91
91
|
? {
|
|
92
92
|
budget: {
|
|
@@ -99,6 +99,24 @@ export function compileWorkflowPlan(input, title, resolvedUnits = new Map()) {
|
|
|
99
99
|
},
|
|
100
100
|
};
|
|
101
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Re-key `outputs:` into canonical wire order (code-point-ascending by name)
|
|
104
|
+
* — the order {@link decodeWorkflowOutputs} in `ir/schema-v4.ts` requires,
|
|
105
|
+
* mirroring how `freeze/task-bindings.ts`'s `finalizeBindings` sorts
|
|
106
|
+
* `inputBindings` before freezing. The source parser preserves AUTHOR order
|
|
107
|
+
* (P3b spec §4.2 places no ordering requirement on authoring), so this is the
|
|
108
|
+
* one place that must impose it.
|
|
109
|
+
*/
|
|
110
|
+
function sortedOutputs(outputs) {
|
|
111
|
+
const sorted = {};
|
|
112
|
+
for (const name of Object.keys(outputs).sort(compareCodePoints)) {
|
|
113
|
+
sorted[name] = outputs[name];
|
|
114
|
+
}
|
|
115
|
+
return sorted;
|
|
116
|
+
}
|
|
117
|
+
function compareCodePoints(left, right) {
|
|
118
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
119
|
+
}
|
|
102
120
|
function compileStep(step, sequenceIndex, defaults, resolved) {
|
|
103
121
|
const gate = {
|
|
104
122
|
kind: "gate",
|
|
@@ -213,6 +231,36 @@ function checkInputReference(text, index, check) {
|
|
|
213
231
|
});
|
|
214
232
|
}
|
|
215
233
|
}
|
|
234
|
+
/**
|
|
235
|
+
* Validate one `outputs.<name>.from` reference (P3b, spec §4.2, rows
|
|
236
|
+
* B-06/B-07): it must parse, it must name a STEP output — never a param (an
|
|
237
|
+
* output projects a step artifact; a param is already on the run row) — and
|
|
238
|
+
* that step must be DECLARED somewhere in the document. Unlike
|
|
239
|
+
* {@link checkInputReference}, the named step need not be EARLIER: an output
|
|
240
|
+
* resolves at run completion, after every step has already run.
|
|
241
|
+
*/
|
|
242
|
+
function checkOutputReference(name, text, check) {
|
|
243
|
+
const parsed = parseReference(text);
|
|
244
|
+
if (!parsed.ok) {
|
|
245
|
+
check.errors.push({ line: check.line, message: `Output "${name}" from: ${parsed.message}` });
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (parsed.expr.kind === "param") {
|
|
249
|
+
check.errors.push({
|
|
250
|
+
line: check.line,
|
|
251
|
+
message: `Output "${name}" from: "${formatReference(parsed.expr)}" names a param, not a step output — an output ` +
|
|
252
|
+
`projects a STEP artifact, never a param. "outputs:" only names step outputs (steps.<id>.output...).`,
|
|
253
|
+
});
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (!check.allStepIds.has(parsed.expr.stepId)) {
|
|
257
|
+
check.errors.push({
|
|
258
|
+
line: check.line,
|
|
259
|
+
message: `Output "${name}" from: "${formatReference(parsed.expr)}" cannot be resolved — "${parsed.expr.stepId}" is ` +
|
|
260
|
+
`not a step in this workflow.`,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
216
264
|
// ── Non-fatal warnings ───────────────────────────────────────────────────────
|
|
217
265
|
/**
|
|
218
266
|
* Collect the document's non-fatal WARNINGS — advisories that never fail
|
|
@@ -30,10 +30,10 @@ export function freezeWorkflowEnvironment(refs, options) {
|
|
|
30
30
|
const resolved = options.resolveRef(inputRef);
|
|
31
31
|
const match = QUALIFIED_ENV_REF_RE.exec(resolved.ref);
|
|
32
32
|
if (!match || match[1] !== resolved.bundle) {
|
|
33
|
-
throw new UsageError(`Workflow env ref ${JSON.stringify(inputRef)} did not resolve to a canonical fully-qualified owner.`, "
|
|
33
|
+
throw new UsageError(`Workflow env ref ${JSON.stringify(inputRef)} did not resolve to a canonical fully-qualified owner.`, "WORKFLOW_SOURCE_INVALID");
|
|
34
34
|
}
|
|
35
35
|
if (logical.has(resolved.ref)) {
|
|
36
|
-
throw new UsageError(`Workflow environment contains duplicate ref ${resolved.ref}.`, "
|
|
36
|
+
throw new UsageError(`Workflow environment contains duplicate ref ${resolved.ref}.`, "WORKFLOW_SOURCE_INVALID");
|
|
37
37
|
}
|
|
38
38
|
logical.add(resolved.ref);
|
|
39
39
|
trackParentDirectories(collector, resolved.root, resolved.path);
|
|
@@ -324,5 +324,5 @@ function physicalIdentity(realPath, stat) {
|
|
|
324
324
|
return stat.ino === 0n ? `path:${realPath}` : `inode:${stat.dev}:${stat.ino}`;
|
|
325
325
|
}
|
|
326
326
|
function invalid(message) {
|
|
327
|
-
throw new UsageError(`Invalid frozen workflow environment: ${message}.`, "
|
|
327
|
+
throw new UsageError(`Invalid frozen workflow environment: ${message}.`, "WORKFLOW_SOURCE_INVALID");
|
|
328
328
|
}
|