akm-cli 0.9.2-alpha.4 → 0.9.2-alpha.5
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 +463 -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 +233 -45
- 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 +55 -12
- 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 +19 -5
- 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 +2 -2
- package/docs/migration/v0.8-to-v0.9.md +9 -7
- package/docs/migration/v0.9.0-troubleshooting.md +14 -7
- 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 +3 -3
- package/docs/reference/supported-formats.md +9 -5
- package/docs/reference/tasks.md +338 -75
- package/docs/reference/workflow-schema.md +281 -8
- 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,455 @@
|
|
|
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 bounded-document front end and field helpers both task source grammars
|
|
6
|
+
* share (spec docs/plans/specs/p2a-task-source-v4.md §1.5 D2-N4, §3.1).
|
|
7
|
+
*
|
|
8
|
+
* This is the actual D2-N4 move, not a parallel reimplementation: every
|
|
9
|
+
* helper below — including the `TASK_V3_MAX_*` resource bounds and
|
|
10
|
+
* `assertBoundedTaskYamlDocument` — is now OWNED here, body-intact from
|
|
11
|
+
* `src/tasks/source-v3.ts`. `source-v3.ts` imports every one of them (and
|
|
12
|
+
* re-exports `assertBoundedTaskYamlDocument` / the `TASK_V3_MAX_*` constants
|
|
13
|
+
* at their existing names, since those were already part of its public
|
|
14
|
+
* surface) instead of declaring its own copies. §9's acceptance criterion is
|
|
15
|
+
* structural: "src/tasks/source/bounded-document.ts exists and owns the
|
|
16
|
+
* D2-N4 helpers; src/tasks/source-v3.ts imports them and contains no copy of
|
|
17
|
+
* any of them" — `tests/tasks/bounded-document.test.ts`'s last describe
|
|
18
|
+
* block pins this with an AST scan of `source-v3.ts`, not just a runtime
|
|
19
|
+
* behavior check (a copy-instead-of-move implementation would pass every
|
|
20
|
+
* OTHER test in that file and in `tests/tasks/source-v3.test.ts`, because
|
|
21
|
+
* both files would still call functions they have in scope either way).
|
|
22
|
+
*
|
|
23
|
+
* This module deliberately imports NOTHING from `../source-v3` (not even a
|
|
24
|
+
* type) — `tests/architecture/import-cycle-ratchet.test.ts` is an ABSOLUTE
|
|
25
|
+
* no-cycle gate over all of `src/**` (its baseline emptied at chunk-8 and
|
|
26
|
+
* counts type-only imports as real graph edges), and `source-v3.ts` now
|
|
27
|
+
* imports from this file, so a reverse edge here would close a cycle. Where
|
|
28
|
+
* a moved helper's signature referenced a v3-only type
|
|
29
|
+
* (`TaskV3AkmOptions["tools"]`, `TaskV3Environment`) the return type is
|
|
30
|
+
* inlined structurally instead — invisible at runtime, and TypeScript's
|
|
31
|
+
* structural typing makes the inlined shape and the named alias
|
|
32
|
+
* interchangeable at every call site.
|
|
33
|
+
*
|
|
34
|
+
* `parseTimeout` and `parseTools` stay v3-only in the sense that they
|
|
35
|
+
* hardcode the `["akm", …]` field path (byte-identical to v3's existing
|
|
36
|
+
* behavior — that hardcoding is exactly what D2-N4's binding resolution asks
|
|
37
|
+
* to preserve). Task source v4's top-level `timeout:`/`tools:` fields have
|
|
38
|
+
* their own siblings in `src/tasks/source/task-source-v4.ts`
|
|
39
|
+
* (`parseTimeoutTopLevel`/`parseToolsTopLevel`) with the same accept/reject
|
|
40
|
+
* semantics at a different, un-prefixed field path — v3 imports the two
|
|
41
|
+
* below rather than declaring them itself, purely because D2-N4 homes every
|
|
42
|
+
* one of these named helpers here. `nullableSelector` is the same kind of
|
|
43
|
+
* `["akm", …]`-hardcoding helper but is NOT in D2-N4's named list and stays
|
|
44
|
+
* declared directly in `source-v3.ts`.
|
|
45
|
+
*/
|
|
46
|
+
import fs from "node:fs";
|
|
47
|
+
import path from "node:path";
|
|
48
|
+
import { types as utilTypes } from "node:util";
|
|
49
|
+
import { isAlias, isMap, isScalar, isSeq, LineCounter, parseDocument } from "yaml";
|
|
50
|
+
import { UsageError } from "../../core/errors.js";
|
|
51
|
+
import { DURATION_UNITS, parseDuration } from "../../core/time.js";
|
|
52
|
+
import { EXECUTION_MAX_TIMEOUT_MS } from "../../execution/limits.js";
|
|
53
|
+
import { snapshotStrictRecord } from "../../execution/record.js";
|
|
54
|
+
import { WORKFLOW_ENV_VAR_NAME_PATTERN } from "../../workflows/resource-limits.js";
|
|
55
|
+
// ── Resource bounds (D2-N4: owned here, re-exported by source-v3.ts) ───────
|
|
56
|
+
export const TASK_V3_MAX_SOURCE_BYTES = 1024 * 1024;
|
|
57
|
+
export const TASK_V3_MAX_JSON_DEPTH = 64;
|
|
58
|
+
export const TASK_V3_MAX_JSON_NODES = 10_000;
|
|
59
|
+
export const TASK_V3_MAX_COLLECTION_ITEMS = 1024;
|
|
60
|
+
export const TASK_V3_MAX_OBJECT_KEYS = 256;
|
|
61
|
+
export const TASK_V3_MAX_STRING_BYTES = 256 * 1024;
|
|
62
|
+
export const TASK_V3_MAX_SCHEDULES = 64;
|
|
63
|
+
export function own(value, key) {
|
|
64
|
+
return Object.hasOwn(value, key);
|
|
65
|
+
}
|
|
66
|
+
export function utf8Bytes(value) {
|
|
67
|
+
return new TextEncoder().encode(value).byteLength;
|
|
68
|
+
}
|
|
69
|
+
export function wellFormedUnicode(value) {
|
|
70
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
71
|
+
const code = value.charCodeAt(index);
|
|
72
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
73
|
+
const next = value.charCodeAt(index + 1);
|
|
74
|
+
if (!(next >= 0xdc00 && next <= 0xdfff))
|
|
75
|
+
return false;
|
|
76
|
+
index += 1;
|
|
77
|
+
}
|
|
78
|
+
else if (code >= 0xdc00 && code <= 0xdfff)
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
/** The one per-field error funnel both grammars render through, distinguished only by `ctx.sourceLabel` (D2-N4). */
|
|
84
|
+
export function sourceError(ctx, fieldPath, detail) {
|
|
85
|
+
const dotted = fieldPath.length === 0
|
|
86
|
+
? "$"
|
|
87
|
+
: fieldPath.reduce((display, segment) => typeof segment === "number"
|
|
88
|
+
? `${display}[${segment}]`
|
|
89
|
+
: display.length > 0
|
|
90
|
+
? `${display}.${segment}`
|
|
91
|
+
: segment, "");
|
|
92
|
+
const line = ctx.lineAt?.(fieldPath);
|
|
93
|
+
const location = `${ctx.filePath}${line === undefined ? "" : `:${line}`}`;
|
|
94
|
+
throw new UsageError(`Invalid ${ctx.sourceLabel} at ${location}: ${dotted} ${detail}`, "TASK_SOURCE_INVALID");
|
|
95
|
+
}
|
|
96
|
+
export function cloneBoundedJson(value, ctx, fieldPath, state, depth = 0, ancestors = new Set()) {
|
|
97
|
+
state.nodes += 1;
|
|
98
|
+
if (state.nodes > TASK_V3_MAX_JSON_NODES)
|
|
99
|
+
sourceError(ctx, fieldPath, `exceeds the ${TASK_V3_MAX_JSON_NODES}-node limit.`);
|
|
100
|
+
if (depth > TASK_V3_MAX_JSON_DEPTH)
|
|
101
|
+
sourceError(ctx, fieldPath, `exceeds the nesting depth of ${TASK_V3_MAX_JSON_DEPTH}.`);
|
|
102
|
+
if (value === null || typeof value === "boolean")
|
|
103
|
+
return value;
|
|
104
|
+
if (typeof value === "number") {
|
|
105
|
+
if (!Number.isFinite(value))
|
|
106
|
+
sourceError(ctx, fieldPath, "must be a finite JSON number.");
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
if (typeof value === "string") {
|
|
110
|
+
if (!wellFormedUnicode(value))
|
|
111
|
+
sourceError(ctx, fieldPath, "must contain well-formed Unicode.");
|
|
112
|
+
if (utf8Bytes(value) > TASK_V3_MAX_STRING_BYTES) {
|
|
113
|
+
sourceError(ctx, fieldPath, `exceeds the ${TASK_V3_MAX_STRING_BYTES}-byte string limit.`);
|
|
114
|
+
}
|
|
115
|
+
return value;
|
|
116
|
+
}
|
|
117
|
+
if (value === undefined)
|
|
118
|
+
sourceError(ctx, fieldPath, "must be omitted instead of set to undefined.");
|
|
119
|
+
if (typeof value !== "object")
|
|
120
|
+
sourceError(ctx, fieldPath, "must be JSON-safe.");
|
|
121
|
+
if (utilTypes.isProxy(value))
|
|
122
|
+
sourceError(ctx, fieldPath, "must not be a Proxy object.");
|
|
123
|
+
if (ancestors.has(value))
|
|
124
|
+
sourceError(ctx, fieldPath, "must not contain a cycle.");
|
|
125
|
+
const nextAncestors = new Set(ancestors).add(value);
|
|
126
|
+
if (Array.isArray(value)) {
|
|
127
|
+
if (Object.getPrototypeOf(value) !== Array.prototype)
|
|
128
|
+
sourceError(ctx, fieldPath, "array must use the standard prototype.");
|
|
129
|
+
const rawLength = Reflect.getOwnPropertyDescriptor(value, "length")?.value;
|
|
130
|
+
if (typeof rawLength !== "number" ||
|
|
131
|
+
!Number.isInteger(rawLength) ||
|
|
132
|
+
rawLength < 0 ||
|
|
133
|
+
rawLength > TASK_V3_MAX_COLLECTION_ITEMS) {
|
|
134
|
+
sourceError(ctx, fieldPath, `array exceeds the ${TASK_V3_MAX_COLLECTION_ITEMS}-item limit.`);
|
|
135
|
+
}
|
|
136
|
+
const length = rawLength;
|
|
137
|
+
const keys = Reflect.ownKeys(value);
|
|
138
|
+
if (keys.length !== length + 1)
|
|
139
|
+
sourceError(ctx, fieldPath, "array must be dense and contain no extra fields.");
|
|
140
|
+
const result = [];
|
|
141
|
+
for (let index = 0; index < length; index += 1) {
|
|
142
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(value, String(index));
|
|
143
|
+
if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) {
|
|
144
|
+
sourceError(ctx, [...fieldPath, index], "array item must be an enumerable data property in a dense array.");
|
|
145
|
+
}
|
|
146
|
+
result.push(cloneBoundedJson(descriptor.value, ctx, [...fieldPath, index], state, depth + 1, nextAncestors));
|
|
147
|
+
}
|
|
148
|
+
return Object.freeze(result);
|
|
149
|
+
}
|
|
150
|
+
let snapshot;
|
|
151
|
+
try {
|
|
152
|
+
snapshot = snapshotStrictRecord(value, fieldPath.map(String).join(".") || "task source");
|
|
153
|
+
}
|
|
154
|
+
catch (cause) {
|
|
155
|
+
sourceError(ctx, fieldPath, cause instanceof Error ? cause.message : String(cause));
|
|
156
|
+
}
|
|
157
|
+
const entries = Object.entries(snapshot);
|
|
158
|
+
if (entries.length > TASK_V3_MAX_OBJECT_KEYS) {
|
|
159
|
+
sourceError(ctx, fieldPath, `mapping exceeds the ${TASK_V3_MAX_OBJECT_KEYS}-key limit.`);
|
|
160
|
+
}
|
|
161
|
+
const result = Object.create(null);
|
|
162
|
+
for (const [key, child] of entries) {
|
|
163
|
+
if (!wellFormedUnicode(key))
|
|
164
|
+
sourceError(ctx, fieldPath, "contains a mapping key with malformed Unicode.");
|
|
165
|
+
if (utf8Bytes(key) > TASK_V3_MAX_STRING_BYTES) {
|
|
166
|
+
sourceError(ctx, fieldPath, `contains a mapping key exceeding the ${TASK_V3_MAX_STRING_BYTES}-byte string limit.`);
|
|
167
|
+
}
|
|
168
|
+
Object.defineProperty(result, key, {
|
|
169
|
+
value: cloneBoundedJson(child, ctx, [...fieldPath, key], state, depth + 1, nextAncestors),
|
|
170
|
+
enumerable: true,
|
|
171
|
+
configurable: false,
|
|
172
|
+
writable: false,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
return Object.freeze(result);
|
|
176
|
+
}
|
|
177
|
+
export function asRecord(value, ctx, fieldPath) {
|
|
178
|
+
if (value === null || Array.isArray(value) || typeof value !== "object")
|
|
179
|
+
sourceError(ctx, fieldPath, "must be a mapping.");
|
|
180
|
+
return value;
|
|
181
|
+
}
|
|
182
|
+
export function checkKeys(value, allowed, ctx, fieldPath) {
|
|
183
|
+
const allow = new Set(allowed);
|
|
184
|
+
const firstUnknown = Object.keys(value).find((key) => !allow.has(key));
|
|
185
|
+
if (firstUnknown !== undefined)
|
|
186
|
+
sourceError(ctx, [...fieldPath, firstUnknown], "is an unsupported field.");
|
|
187
|
+
}
|
|
188
|
+
export function presentJsonValue(value, ctx, fieldPath) {
|
|
189
|
+
if (value === undefined)
|
|
190
|
+
sourceError(ctx, fieldPath, "must be omitted instead of set to undefined.");
|
|
191
|
+
return value;
|
|
192
|
+
}
|
|
193
|
+
export function stringField(value, ctx, fieldPath, options = {}) {
|
|
194
|
+
if (value === null && options.nullable)
|
|
195
|
+
return null;
|
|
196
|
+
if (typeof value !== "string")
|
|
197
|
+
sourceError(ctx, fieldPath, options.nullable ? "must be a string or null." : "must be a string.");
|
|
198
|
+
if (options.nonempty && value.trim().length === 0)
|
|
199
|
+
sourceError(ctx, fieldPath, "must be a non-empty string.");
|
|
200
|
+
return value;
|
|
201
|
+
}
|
|
202
|
+
export function noGithubExpression(value, ctx, fieldPath) {
|
|
203
|
+
if (value.includes("${{"))
|
|
204
|
+
sourceError(ctx, fieldPath, "contains an unsupported GitHub expression.");
|
|
205
|
+
}
|
|
206
|
+
/** `env:` is already a top-level field in both grammars — reused body-intact, field path unchanged. */
|
|
207
|
+
export function parseEnvironment(value, ctx) {
|
|
208
|
+
const environment = asRecord(value, ctx, ["env"]);
|
|
209
|
+
for (const [key, child] of Object.entries(environment)) {
|
|
210
|
+
if (!WORKFLOW_ENV_VAR_NAME_PATTERN.test(key))
|
|
211
|
+
sourceError(ctx, ["env", key], "has an invalid environment variable name.");
|
|
212
|
+
if (typeof child !== "string" && typeof child !== "number" && typeof child !== "boolean") {
|
|
213
|
+
sourceError(ctx, ["env", key], "must be a string, finite number, or boolean.");
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return environment;
|
|
217
|
+
}
|
|
218
|
+
export function parseStringArray(value, ctx, fieldPath, options = {}) {
|
|
219
|
+
if (!Array.isArray(value))
|
|
220
|
+
sourceError(ctx, fieldPath, "must be an array of strings.");
|
|
221
|
+
if (options.max !== undefined && value.length > options.max)
|
|
222
|
+
sourceError(ctx, fieldPath, `accepts at most ${options.max} items.`);
|
|
223
|
+
const strings = [];
|
|
224
|
+
for (const [index, entry] of value.entries()) {
|
|
225
|
+
if (typeof entry !== "string" || entry.length === 0)
|
|
226
|
+
sourceError(ctx, [...fieldPath, index], "must be a non-empty string.");
|
|
227
|
+
if (options.pattern && !options.pattern.test(entry))
|
|
228
|
+
sourceError(ctx, [...fieldPath, index], "has an invalid value.");
|
|
229
|
+
strings.push(entry);
|
|
230
|
+
}
|
|
231
|
+
return Object.freeze(strings);
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* `akm.timeout` — v3-only (hardcodes the `["akm", "timeout"]` field path, see
|
|
235
|
+
* this module's header). Body-intact from `source-v3.ts`.
|
|
236
|
+
*/
|
|
237
|
+
export function parseTimeout(value, ctx) {
|
|
238
|
+
if (value === null)
|
|
239
|
+
return null;
|
|
240
|
+
if (typeof value === "string" && value.trim() !== value) {
|
|
241
|
+
sourceError(ctx, ["akm", "timeout"], "must not contain surrounding whitespace.");
|
|
242
|
+
}
|
|
243
|
+
const milliseconds = typeof value === "string" ? parseDuration(value, DURATION_UNITS) : value;
|
|
244
|
+
if (milliseconds === null ||
|
|
245
|
+
typeof milliseconds !== "number" ||
|
|
246
|
+
!Number.isSafeInteger(milliseconds) ||
|
|
247
|
+
milliseconds < 0 ||
|
|
248
|
+
milliseconds > EXECUTION_MAX_TIMEOUT_MS) {
|
|
249
|
+
sourceError(ctx, ["akm", "timeout"], `must be null, 0 through ${EXECUTION_MAX_TIMEOUT_MS} milliseconds, or a common duration such as 20m.`);
|
|
250
|
+
}
|
|
251
|
+
return value;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* `akm.tools` — v3-only (hardcodes the `["akm", "tools"]` field path, see
|
|
255
|
+
* this module's header). Body-intact from `source-v3.ts`; the return type is
|
|
256
|
+
* inlined (was `TaskV3AkmOptions["tools"]`) so this module imports nothing
|
|
257
|
+
* from `../source-v3` (see header — the import-cycle ratchet is absolute).
|
|
258
|
+
*/
|
|
259
|
+
export function parseTools(value, ctx) {
|
|
260
|
+
if (value === null || typeof value === "string")
|
|
261
|
+
return value;
|
|
262
|
+
if (Array.isArray(value)) {
|
|
263
|
+
if (value.some((entry) => typeof entry !== "string"))
|
|
264
|
+
sourceError(ctx, ["akm", "tools"], "array values must be strings.");
|
|
265
|
+
return value;
|
|
266
|
+
}
|
|
267
|
+
if (typeof value === "object")
|
|
268
|
+
return value;
|
|
269
|
+
sourceError(ctx, ["akm", "tools"], "must be a string, string array, mapping, or null.");
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* `working-directory:` is already a top-level field in both grammars —
|
|
273
|
+
* reused body-intact, field path unchanged (`source-v3.ts:677-714`).
|
|
274
|
+
*/
|
|
275
|
+
export function validateWorkingDirectory(value, ctx) {
|
|
276
|
+
if (value.trim().length === 0 ||
|
|
277
|
+
value.includes("\0") ||
|
|
278
|
+
path.posix.isAbsolute(value.replaceAll("\\", "/")) ||
|
|
279
|
+
/^[A-Za-z]:[\\/]/.test(value) ||
|
|
280
|
+
value.startsWith("\\\\")) {
|
|
281
|
+
sourceError(ctx, ["working-directory"], "must be a non-empty relative path contained by the workspace root.");
|
|
282
|
+
}
|
|
283
|
+
const segments = value.replaceAll("\\", "/").split("/");
|
|
284
|
+
if (segments.some((segment) => segment === ".." || segment.length === 0)) {
|
|
285
|
+
sourceError(ctx, ["working-directory"], "must not contain empty or escaping path segments.");
|
|
286
|
+
}
|
|
287
|
+
if (!ctx.workspaceRoot) {
|
|
288
|
+
sourceError(ctx, ["working-directory"], "requires a workspace root so physical containment can be verified.");
|
|
289
|
+
}
|
|
290
|
+
let realRoot;
|
|
291
|
+
let realCandidate;
|
|
292
|
+
try {
|
|
293
|
+
realRoot = fs.realpathSync(ctx.workspaceRoot);
|
|
294
|
+
const candidate = path.resolve(realRoot, value);
|
|
295
|
+
const stat = fs.statSync(candidate);
|
|
296
|
+
if (!stat.isDirectory())
|
|
297
|
+
sourceError(ctx, ["working-directory"], "must resolve to a directory.");
|
|
298
|
+
realCandidate = fs.realpathSync(candidate);
|
|
299
|
+
}
|
|
300
|
+
catch (cause) {
|
|
301
|
+
if (cause instanceof UsageError)
|
|
302
|
+
throw cause;
|
|
303
|
+
sourceError(ctx, ["working-directory"], `cannot be physically verified: ${cause instanceof Error ? cause.message : String(cause)}.`);
|
|
304
|
+
}
|
|
305
|
+
const relative = path.relative(realRoot, realCandidate);
|
|
306
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
307
|
+
sourceError(ctx, ["working-directory"], "resolves outside the workspace root and is not physically contained.");
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
export function yamlProblem(message) {
|
|
311
|
+
return message.split("\n")[0]?.trim() || "invalid YAML";
|
|
312
|
+
}
|
|
313
|
+
export function yamlAstError(options, node, detail) {
|
|
314
|
+
const range = node?.range;
|
|
315
|
+
const line = range && options.lineCounter ? options.lineCounter.linePos(range[0] ?? 0).line : undefined;
|
|
316
|
+
throw new UsageError(`Invalid ${options.sourceLabel} at ${options.filePath}${line === undefined ? "" : `:${line}`}: ${detail}`, "TASK_SOURCE_INVALID");
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Bound and close the YAML AST before `toJS` can allocate or recurse through
|
|
320
|
+
* it. This is shared by the v3 parser, task source v4, and the explicit v2
|
|
321
|
+
* migration reader.
|
|
322
|
+
*/
|
|
323
|
+
export function assertBoundedTaskYamlDocument(document, options) {
|
|
324
|
+
const stack = [{ node: document.contents, depth: 0 }];
|
|
325
|
+
let nodes = 0;
|
|
326
|
+
while (stack.length > 0) {
|
|
327
|
+
const current = stack.pop();
|
|
328
|
+
if (!current)
|
|
329
|
+
break;
|
|
330
|
+
const node = current.node;
|
|
331
|
+
if (node === null || node === undefined)
|
|
332
|
+
continue;
|
|
333
|
+
nodes += 1;
|
|
334
|
+
if (nodes > TASK_V3_MAX_JSON_NODES) {
|
|
335
|
+
yamlAstError(options, node, `YAML exceeds the ${TASK_V3_MAX_JSON_NODES}-node limit.`);
|
|
336
|
+
}
|
|
337
|
+
if (current.depth > TASK_V3_MAX_JSON_DEPTH) {
|
|
338
|
+
yamlAstError(options, node, `YAML exceeds the nesting depth of ${TASK_V3_MAX_JSON_DEPTH}.`);
|
|
339
|
+
}
|
|
340
|
+
if (isAlias(node))
|
|
341
|
+
yamlAstError(options, node, "YAML aliases are unsupported.");
|
|
342
|
+
if (node.anchor !== undefined) {
|
|
343
|
+
yamlAstError(options, node, "YAML anchors are unsupported.");
|
|
344
|
+
}
|
|
345
|
+
if (node.tag) {
|
|
346
|
+
yamlAstError(options, node, "custom or explicit YAML tags are unsupported.");
|
|
347
|
+
}
|
|
348
|
+
if (isScalar(node))
|
|
349
|
+
continue;
|
|
350
|
+
if (isSeq(node)) {
|
|
351
|
+
if (node.items.length > TASK_V3_MAX_COLLECTION_ITEMS) {
|
|
352
|
+
yamlAstError(options, node, `YAML sequence exceeds the ${TASK_V3_MAX_COLLECTION_ITEMS}-item limit.`);
|
|
353
|
+
}
|
|
354
|
+
for (let index = node.items.length - 1; index >= 0; index -= 1) {
|
|
355
|
+
stack.push({ node: node.items[index], depth: current.depth + 1 });
|
|
356
|
+
}
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
if (isMap(node)) {
|
|
360
|
+
if (node.items.length > TASK_V3_MAX_OBJECT_KEYS) {
|
|
361
|
+
yamlAstError(options, node, `YAML mapping exceeds the ${TASK_V3_MAX_OBJECT_KEYS}-key limit.`);
|
|
362
|
+
}
|
|
363
|
+
for (let index = node.items.length - 1; index >= 0; index -= 1) {
|
|
364
|
+
const pair = node.items[index];
|
|
365
|
+
if (!pair)
|
|
366
|
+
yamlAstError(options, node, "sparse YAML mappings are unsupported.");
|
|
367
|
+
if (!isScalar(pair.key) || typeof pair.key.value !== "string") {
|
|
368
|
+
yamlAstError(options, pair.key, "non-string YAML mapping keys are unsupported.");
|
|
369
|
+
}
|
|
370
|
+
if (!wellFormedUnicode(pair.key.value)) {
|
|
371
|
+
yamlAstError(options, pair.key, "YAML mapping key must contain well-formed Unicode.");
|
|
372
|
+
}
|
|
373
|
+
if (utf8Bytes(pair.key.value) > TASK_V3_MAX_STRING_BYTES) {
|
|
374
|
+
yamlAstError(options, pair.key, `YAML mapping key exceeds the ${TASK_V3_MAX_STRING_BYTES}-byte string limit.`);
|
|
375
|
+
}
|
|
376
|
+
if (pair.key.value === "<<")
|
|
377
|
+
yamlAstError(options, pair.key, "YAML merge keys are unsupported.");
|
|
378
|
+
stack.push({ node: pair.value, depth: current.depth + 1 });
|
|
379
|
+
stack.push({ node: pair.key, depth: current.depth + 1 });
|
|
380
|
+
}
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
yamlAstError(options, node, "unsupported YAML node kind.");
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Parse hostile YAML without aliases/tags/merges, then hand back the bounded
|
|
388
|
+
* `{root, lineAt}` pair both grammars that read a task document consume —
|
|
389
|
+
* originally lifted from the now-deleted `parseTaskV3Yaml`
|
|
390
|
+
* (`source-v3.ts:894-952`), generalized only by `sourceLabel`.
|
|
391
|
+
*
|
|
392
|
+
* P4 (docs/plans/specs/p4-deletions-closeout.md §3.2) deleted task v3
|
|
393
|
+
* acceptance from `src` entirely — `parseTaskV3Yaml` and its `"task v3
|
|
394
|
+
* source"` label no longer exist here (the grammar survives only in the
|
|
395
|
+
* vendored, frozen `scripts/akm-migrate/migrate/task-source-v3-frozen.ts`
|
|
396
|
+
* copy, which does not call this function). The two live `src` callers
|
|
397
|
+
* today: `parseTaskSourceV4`'s standalone YAML-string entry
|
|
398
|
+
* (`task-source-v4.ts:790`) passes `sourceLabel: "task source v4"`; the
|
|
399
|
+
* version router (`parse-task-source.ts:61`) calls this directly with
|
|
400
|
+
* `sourceLabel: "task source"` — not "task v3 source" — because its front
|
|
401
|
+
* end runs before `root.version` is even read, so it cannot yet know which
|
|
402
|
+
* schema version the document will turn out to be.
|
|
403
|
+
*/
|
|
404
|
+
export function readBoundedTaskSourceYaml(input, options) {
|
|
405
|
+
const sourceLabel = options.sourceLabel;
|
|
406
|
+
// P4 (docs/plans/specs/p4-deletions-closeout.md §5.2, row R-R8): these six
|
|
407
|
+
// throws used to omit their `code` argument and rely on the constructor's
|
|
408
|
+
// `INVALID_FLAG_VALUE` default (a pre-P4 ratchet-gaming trick that kept the
|
|
409
|
+
// literal string out of the grep-style count while the effective code
|
|
410
|
+
// stayed generic). §5.2's target table closes that gap: every pre-version
|
|
411
|
+
// failure in this bounded YAML front end (not a string, too large, YAML
|
|
412
|
+
// parse/expansion) is a task-source defect, not a flag-parsing one, so each
|
|
413
|
+
// now carries `TASK_SOURCE_INVALID` explicitly.
|
|
414
|
+
if (typeof input.yaml !== "string") {
|
|
415
|
+
throw new UsageError(`Invalid ${sourceLabel} at ${input.filePath}: source must be a string.`, "TASK_SOURCE_INVALID");
|
|
416
|
+
}
|
|
417
|
+
if (utf8Bytes(input.yaml) > TASK_V3_MAX_SOURCE_BYTES) {
|
|
418
|
+
throw new UsageError(`Invalid ${sourceLabel} at ${input.filePath}: source exceeds the 1 MiB (${TASK_V3_MAX_SOURCE_BYTES}-byte) resource limit.`, "TASK_SOURCE_INVALID");
|
|
419
|
+
}
|
|
420
|
+
const lineCounter = new LineCounter();
|
|
421
|
+
let document;
|
|
422
|
+
try {
|
|
423
|
+
document = parseDocument(input.yaml, { lineCounter, uniqueKeys: true });
|
|
424
|
+
}
|
|
425
|
+
catch (cause) {
|
|
426
|
+
throw new UsageError(`Invalid ${sourceLabel} at ${input.filePath}: YAML parsing failed: ${cause instanceof Error ? cause.message : String(cause)}`, "TASK_SOURCE_INVALID");
|
|
427
|
+
}
|
|
428
|
+
const [problem] = document.errors;
|
|
429
|
+
if (problem) {
|
|
430
|
+
const offset = Array.isArray(problem.pos) ? problem.pos[0] : 0;
|
|
431
|
+
throw new UsageError(`Invalid ${sourceLabel} at ${input.filePath}:${lineCounter.linePos(offset).line}: ${yamlProblem(problem.message)}`, "TASK_SOURCE_INVALID");
|
|
432
|
+
}
|
|
433
|
+
const [warning] = document.warnings;
|
|
434
|
+
if (warning) {
|
|
435
|
+
throw new UsageError(`Invalid ${sourceLabel} at ${input.filePath}: unsupported YAML construct: ${yamlProblem(warning.message)}`, "TASK_SOURCE_INVALID");
|
|
436
|
+
}
|
|
437
|
+
assertBoundedTaskYamlDocument(document, { filePath: input.filePath, sourceLabel, lineCounter });
|
|
438
|
+
let root;
|
|
439
|
+
try {
|
|
440
|
+
root = document.toJS({ maxAliasCount: 0 });
|
|
441
|
+
}
|
|
442
|
+
catch (cause) {
|
|
443
|
+
throw new UsageError(`Invalid ${sourceLabel} at ${input.filePath}: YAML expansion failed: ${cause instanceof Error ? cause.message : String(cause)}`, "TASK_SOURCE_INVALID");
|
|
444
|
+
}
|
|
445
|
+
const lineAt = (fieldPath) => {
|
|
446
|
+
for (let depth = fieldPath.length; depth >= 0; depth -= 1) {
|
|
447
|
+
const node = depth === 0 ? document.contents : document.getIn(fieldPath.slice(0, depth), true);
|
|
448
|
+
const range = node?.range;
|
|
449
|
+
if (range)
|
|
450
|
+
return lineCounter.linePos(range[0]).line;
|
|
451
|
+
}
|
|
452
|
+
return undefined;
|
|
453
|
+
};
|
|
454
|
+
return { root, lineAt };
|
|
455
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
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 task source version router (spec docs/plans/specs/p4-deletions-closeout.md
|
|
6
|
+
* §3.2.2).
|
|
7
|
+
*
|
|
8
|
+
* Runs the bounded YAML front end ONCE (`readBoundedTaskSourceYaml`), reads
|
|
9
|
+
* `root.version`, and either dispatches into `parseTaskSourceV4Document` or
|
|
10
|
+
* fails closed — no second parse, no re-serialization, no synthetic document
|
|
11
|
+
* (the P1b §4.3 invariant this phase carries forward).
|
|
12
|
+
*
|
|
13
|
+
* The terminal routing table:
|
|
14
|
+
*
|
|
15
|
+
* | root `version` | outcome |
|
|
16
|
+
* |------------------------|-----------------------------------------------------------------|
|
|
17
|
+
* | `4` | `parseTaskSourceV4Document` — the new grammar (row B-13) |
|
|
18
|
+
* | any other number | `TASK_SCHEMA_VERSION_UNSUPPORTED`, naming the migrator (B-14/B-15) |
|
|
19
|
+
* | absent / not a number | `parseTaskSourceV4Document` — its own `TASK_SOURCE_INVALID` "version is required and must be 4" / "must be exactly 4" wording (row B-16) |
|
|
20
|
+
*
|
|
21
|
+
* A missing or non-numeric `version:` is a MALFORMED v4 document, not a
|
|
22
|
+
* legacy one — it routes into the v4 parser so the field error names the
|
|
23
|
+
* one grammar `src` still accepts, rather than a generic "unsupported"
|
|
24
|
+
* message that would send the user to the migrator for a document that was
|
|
25
|
+
* never task v2 or v3 in the first place.
|
|
26
|
+
*
|
|
27
|
+
* task v2 and task v3 sources are no longer read by `src` at all — the only
|
|
28
|
+
* surviving reader is the frozen, vendored copy in
|
|
29
|
+
* `scripts/akm-migrate/migrate/task-source-v3-frozen.ts`, reachable only
|
|
30
|
+
* through `akm migrate apply` / `akm-migrate`. The front end's own
|
|
31
|
+
* pre-version failures (source not a string, source too large, YAML
|
|
32
|
+
* parse/warning/expansion) render with the label `task source` (row B-17,
|
|
33
|
+
* closing the "task v3 source" label wart P2a's §3.4 recorded).
|
|
34
|
+
*/
|
|
35
|
+
import { UsageError } from "../../core/errors.js";
|
|
36
|
+
import { readBoundedTaskSourceYaml } from "./bounded-document.js";
|
|
37
|
+
import { parseTaskSourceV4Document, TASK_SOURCE_V4_VERSION } from "./task-source-v4.js";
|
|
38
|
+
/** Read the root `version` field without over-accepting non-number values (e.g. the string `"4"`). */
|
|
39
|
+
export function peekTaskSourceVersion(root) {
|
|
40
|
+
if (root === null || typeof root !== "object" || Array.isArray(root))
|
|
41
|
+
return undefined;
|
|
42
|
+
const value = root.version;
|
|
43
|
+
return typeof value === "number" ? value : undefined;
|
|
44
|
+
}
|
|
45
|
+
const TASK_MIGRATE_HINT = "Run `akm migrate apply --dry-run` to preview the task-v3 to task-source-v4 conversion, then run `akm migrate apply`.";
|
|
46
|
+
/** Parse task source YAML, routing per the terminal table above. */
|
|
47
|
+
export function parseTaskSource(input) {
|
|
48
|
+
const { root, lineAt } = readBoundedTaskSourceYaml(input, { sourceLabel: "task source" });
|
|
49
|
+
const version = peekTaskSourceVersion(root);
|
|
50
|
+
if (version !== undefined && version !== TASK_SOURCE_V4_VERSION) {
|
|
51
|
+
throw new UsageError(`TASK_SCHEMA_VERSION_UNSUPPORTED: Task at ${input.filePath} uses task schema version ${version}, which this release does not accept.`, "TASK_SCHEMA_VERSION_UNSUPPORTED", TASK_MIGRATE_HINT);
|
|
52
|
+
}
|
|
53
|
+
const documentOptions = {
|
|
54
|
+
filePath: input.filePath,
|
|
55
|
+
...(input.workspaceRoot ? { workspaceRoot: input.workspaceRoot } : {}),
|
|
56
|
+
lineAt,
|
|
57
|
+
};
|
|
58
|
+
return Object.freeze({ version: 4, v4: parseTaskSourceV4Document(root, documentOptions) });
|
|
59
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
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
|
+
* Map every top-level task source v4 execution control and D2-N7 survivor
|
|
6
|
+
* into v3's `akm.*` shape, one field at a time (never a whole-object copy,
|
|
7
|
+
* so a field task source v4 does not represent — `inputs`, per-binding
|
|
8
|
+
* `enabled` — can never leak in by accident). Returns `undefined` when
|
|
9
|
+
* nothing maps, matching v3's own
|
|
10
|
+
* convention of omitting the `akm` key entirely rather than emitting an
|
|
11
|
+
* always-present empty object (`source-v3.ts:789`).
|
|
12
|
+
*/
|
|
13
|
+
function projectAkm(document) {
|
|
14
|
+
const out = {};
|
|
15
|
+
if (document.description !== undefined)
|
|
16
|
+
out.description = document.description;
|
|
17
|
+
if (document.when_to_use !== undefined)
|
|
18
|
+
out.when_to_use = document.when_to_use;
|
|
19
|
+
if (document.tags !== undefined)
|
|
20
|
+
out.tags = document.tags;
|
|
21
|
+
const execution = document.execution;
|
|
22
|
+
if (execution.agent !== undefined)
|
|
23
|
+
out.agent = execution.agent;
|
|
24
|
+
if (execution.engine !== undefined)
|
|
25
|
+
out.engine = execution.engine;
|
|
26
|
+
if (execution.model !== undefined)
|
|
27
|
+
out.model = execution.model;
|
|
28
|
+
if (execution.inference !== undefined)
|
|
29
|
+
out.inference = execution.inference;
|
|
30
|
+
if (execution.tools !== undefined)
|
|
31
|
+
out.tools = execution.tools;
|
|
32
|
+
if (execution.timeout !== undefined)
|
|
33
|
+
out.timeout = execution.timeout;
|
|
34
|
+
if (execution.redact !== undefined)
|
|
35
|
+
out.redact = execution.redact;
|
|
36
|
+
if (execution.maxSteps !== undefined)
|
|
37
|
+
out.maxSteps = execution.maxSteps;
|
|
38
|
+
if (execution.maxRetries !== undefined)
|
|
39
|
+
out.maxRetries = execution.maxRetries;
|
|
40
|
+
if (document.output !== undefined)
|
|
41
|
+
out.outputSchema = document.output;
|
|
42
|
+
return Object.keys(out).length > 0 ? Object.freeze(out) : undefined;
|
|
43
|
+
}
|
|
44
|
+
/** Project one parsed task source v4 document into the prepare seam's existing input shape (spec §3.5). */
|
|
45
|
+
export function projectTaskSourceV4(document) {
|
|
46
|
+
const akm = projectAkm(document);
|
|
47
|
+
const schedules = Object.freeze(document.schedule.map((entry) => Object.freeze({ cron: entry.cron, source: entry.source, ordinal: entry.ordinal })));
|
|
48
|
+
return Object.freeze({
|
|
49
|
+
// The literal `3` is the prepare seam's own discriminant
|
|
50
|
+
// (PreparableTaskDocument = TaskV3SourceDocument), not an assertion that
|
|
51
|
+
// the source this was projected from was task v3 — `TASK_V3_SCHEMA_VERSION`
|
|
52
|
+
// is gone (P4 deleted task v3 acceptance from src), and this projection
|
|
53
|
+
// only ever runs on a task source v4 document (P2a §3.5's recorded wart).
|
|
54
|
+
version: 3,
|
|
55
|
+
...(document.name !== undefined ? { name: document.name } : {}),
|
|
56
|
+
target: document.target,
|
|
57
|
+
...(document.env !== undefined ? { env: document.env } : {}),
|
|
58
|
+
...(akm !== undefined ? { akm } : {}),
|
|
59
|
+
triggers: Object.freeze({ manual: document.manualOnly, schedules }),
|
|
60
|
+
source: Object.freeze({ path: document.source.path }),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
* `TASK_INPUT_DIAGNOSTICS` — the task-side message/code vocabulary for
|
|
6
|
+
* `materializeInputFlags` (spec docs/plans/specs/p2a-task-source-v4.md §1.5
|
|
7
|
+
* D3-N3, §4.2, §5.1).
|
|
8
|
+
*
|
|
9
|
+
* `materializeInputFlags` (`src/execution/input-contract.ts`) contains no
|
|
10
|
+
* literal user-facing string of its own — every message/code it raises comes
|
|
11
|
+
* from a caller-supplied `InputFlagDiagnostics`. `src/workflows/ir/params.ts`
|
|
12
|
+
* supplies `WORKFLOW_PARAMETER_DIAGNOSTICS` for workflow params; this module
|
|
13
|
+
* is that same seam's task-side sibling, consumed by `akm task run`'s Stage 2
|
|
14
|
+
* (`src/tasks/run/load-task.ts`). Mirrors `WORKFLOW_PARAMETER_DIAGNOSTICS`'s
|
|
15
|
+
* shape exactly (`unknownFlag`'s declared-name enumeration in the SEPARATE
|
|
16
|
+
* `hint`, not the message — `params.ts:65-72`) with task-flavored nouns and
|
|
17
|
+
* the codes §5.1 binds: `UNKNOWN_FLAG` for an undeclared name,
|
|
18
|
+
* `INPUT_BINDING_INVALID` for every value/contract failure.
|
|
19
|
+
*/
|
|
20
|
+
import { UsageError } from "../../core/errors.js";
|
|
21
|
+
function invalidTaskInput(name, detail) {
|
|
22
|
+
return new UsageError(`Task input "--${name}" ${detail}.`, "INPUT_BINDING_INVALID");
|
|
23
|
+
}
|
|
24
|
+
/** The task-input message/code vocabulary for `materializeInputFlags` (D3-N3). */
|
|
25
|
+
export const TASK_INPUT_DIAGNOSTICS = {
|
|
26
|
+
unknownFlag: (name, declared) => {
|
|
27
|
+
const available = declared.map((n) => `--${n}`).join(", ");
|
|
28
|
+
return new UsageError(`Unknown task input "--${name}". Input flags must exactly match a declared task input.`, "UNKNOWN_FLAG", available ? `Declared inputs: ${available}.` : "This task declares no inputs.");
|
|
29
|
+
},
|
|
30
|
+
invalidValue: (name, detail) => invalidTaskInput(name, detail),
|
|
31
|
+
contractViolation: (errors) => new UsageError(`Task input flags do not satisfy the task's declared input schemas:\n${errors
|
|
32
|
+
.map((error) => ` - ${error.replace(/^\$/, "inputs")}`)
|
|
33
|
+
.join("\n")}`, "INPUT_BINDING_INVALID"),
|
|
34
|
+
duplicateNonArray: (name) => invalidTaskInput(name, "was provided more than once but is not declared as an array"),
|
|
35
|
+
malformedJson: (name) => invalidTaskInput(name, "must contain valid JSON"),
|
|
36
|
+
};
|