akm-cli 0.9.0-rc.0 → 0.9.0-rc.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 +339 -2
- package/SECURITY.md +23 -24
- package/dist/assets/help/help-improve.md +10 -10
- package/dist/assets/hints/cli-hints-full.md +44 -10
- package/dist/assets/hints/cli-hints-short.md +6 -2
- package/dist/assets/{profiles → improve-strategies}/default.json +1 -0
- package/dist/assets/{profiles → improve-strategies}/graph-refresh.json +1 -1
- package/dist/assets/{profiles → improve-strategies}/proactive-maintenance.json +2 -3
- package/dist/assets/{profiles → improve-strategies}/reflect-distill.json +3 -4
- package/dist/assets/prompts/workflow-unit-preamble.md +26 -0
- package/dist/assets/stash-skeleton/README.md +28 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +12 -1
- package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +11 -1
- package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +9 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +7 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +7 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +98 -0
- package/dist/assets/stash-skeleton/facts/conventions/domains.md +63 -0
- package/dist/assets/stash-skeleton/facts/conventions/organization.md +127 -0
- package/dist/assets/tasks/core/backup.yml +1 -0
- package/dist/assets/tasks/core/extract.yml +1 -0
- package/dist/assets/tasks/core/improve.yml +1 -0
- package/dist/assets/tasks/core/index-refresh.yml +1 -0
- package/dist/assets/tasks/core/sync.yml +1 -0
- package/dist/assets/tasks/core/version-check.yml +1 -0
- package/dist/assets/tasks/graph-refresh-weekly.yml +4 -4
- package/dist/assets/templates/html/health.html +5 -1
- package/dist/cli/config-migrate.js +31 -138
- package/dist/cli/config-validate.js +10 -8
- package/dist/cli.js +48 -14
- package/dist/commands/agent/agent-dispatch.js +17 -16
- package/dist/commands/agent/agent-support.js +0 -24
- package/dist/commands/agent/contribute-cli.js +5 -15
- package/dist/commands/backup-cli.js +54 -0
- package/dist/commands/config-cli.js +45 -159
- package/dist/commands/env/env-binding.js +95 -0
- package/dist/commands/env/env-cli.js +8 -65
- package/dist/commands/env/secret.js +8 -5
- package/dist/commands/health/checks.js +130 -83
- package/dist/commands/health/html-report.js +4 -0
- package/dist/commands/health/improve-metrics.js +30 -32
- package/dist/commands/health/llm-usage.js +19 -19
- package/dist/commands/health/md-report.js +4 -0
- package/dist/commands/health/metrics.js +2 -1
- package/dist/commands/health/surfaces.js +5 -4
- package/dist/commands/health.js +1 -1
- package/dist/commands/improve/consolidate/chunking.js +2 -2
- package/dist/commands/improve/consolidate.js +28 -25
- package/dist/commands/improve/distill/promote-memory.js +5 -12
- package/dist/commands/improve/distill/quality-gate.js +5 -7
- package/dist/commands/improve/distill.js +16 -5
- package/dist/commands/improve/eligibility.js +22 -12
- package/dist/commands/improve/extract-cli.js +47 -19
- package/dist/commands/improve/extract.js +110 -62
- package/dist/commands/improve/improve-cli.js +38 -16
- package/dist/commands/improve/improve-result-file.js +30 -24
- package/dist/commands/improve/improve-strategies.js +137 -0
- package/dist/commands/improve/improve.js +60 -30
- package/dist/commands/improve/locks.js +66 -45
- package/dist/commands/improve/loop-stages.js +75 -33
- package/dist/commands/improve/memory/memory-belief.js +79 -7
- package/dist/commands/improve/memory/memory-contradiction-detect.js +12 -4
- package/dist/commands/improve/preparation.js +71 -73
- package/dist/commands/improve/procedural.js +3 -2
- package/dist/commands/improve/recombine.js +2 -1
- package/dist/commands/improve/reflect.js +119 -214
- package/dist/commands/improve/shared.js +11 -5
- package/dist/commands/lint/base-linter.js +152 -42
- package/dist/commands/mv-cli.js +809 -0
- package/dist/commands/proposal/proposal-cli.js +18 -8
- package/dist/commands/proposal/propose.js +64 -69
- package/dist/commands/read/knowledge.js +436 -4
- package/dist/commands/read/remember-cli.js +39 -2
- package/dist/commands/read/search-cli.js +6 -1
- package/dist/commands/registry-cli.js +29 -14
- package/dist/commands/remember.js +2 -0
- package/dist/commands/sources/init.js +13 -14
- package/dist/commands/sources/migration-help.js +7 -4
- package/dist/commands/sources/schema-repair.js +2 -4
- package/dist/commands/sources/source-add.js +62 -73
- package/dist/commands/sources/source-manage.js +50 -46
- package/dist/commands/sources/stash-cli.js +41 -4
- package/dist/commands/tasks/default-tasks.js +12 -12
- package/dist/commands/tasks/tasks-cli.js +7 -3
- package/dist/commands/tasks/tasks.js +113 -18
- package/dist/commands/wiki-cli.js +9 -10
- package/dist/commands/workflow-cli.js +276 -12
- package/dist/core/asset/asset-spec.js +58 -1
- package/dist/core/asset/frontmatter.js +12 -2
- package/dist/core/common.js +5 -3
- package/dist/core/config/config-io.js +28 -17
- package/dist/core/config/config-schema.js +379 -66
- package/dist/core/config/config-types.js +3 -3
- package/dist/core/config/config-version.js +29 -0
- package/dist/core/config/config-walker.js +98 -27
- package/dist/core/config/config.js +132 -266
- package/dist/core/config/deep-merge.js +41 -0
- package/dist/core/config/engine-semantics.js +32 -0
- package/dist/core/errors.js +2 -2
- package/dist/core/extra-params.js +61 -0
- package/dist/core/file-lock.js +201 -56
- package/dist/core/improve-result.js +178 -0
- package/dist/core/json-schema.js +142 -0
- package/dist/core/maintenance-barrier.js +119 -0
- package/dist/core/migration-backup.js +416 -0
- package/dist/core/paths.js +3 -0
- package/dist/core/redaction.js +358 -0
- package/dist/core/state/migrations.js +17 -2
- package/dist/core/state-db.js +44 -1
- package/dist/indexer/db/db.js +118 -2
- package/dist/indexer/graph/graph-extraction.js +28 -16
- package/dist/indexer/index-writer-lock.js +31 -24
- package/dist/indexer/index-written-assets.js +15 -6
- package/dist/indexer/indexer.js +47 -2
- package/dist/indexer/passes/memory-inference.js +10 -6
- package/dist/indexer/passes/metadata.js +250 -0
- package/dist/indexer/search/db-search.js +111 -44
- package/dist/indexer/search/fts-query.js +41 -0
- package/dist/indexer/search/ranking-contributors.js +48 -0
- package/dist/indexer/search/ranking.js +36 -23
- package/dist/indexer/search/search-fields.js +11 -1
- package/dist/indexer/walk/matchers.js +39 -0
- package/dist/integrations/agent/builder-shared.js +7 -0
- package/dist/integrations/agent/builders.js +5 -50
- package/dist/integrations/agent/config.js +3 -143
- package/dist/integrations/agent/detect.js +17 -2
- package/dist/integrations/agent/engine-resolution.js +202 -0
- package/dist/integrations/agent/index.js +1 -2
- package/dist/integrations/agent/model-aliases.js +16 -2
- package/dist/integrations/agent/profiles.js +36 -62
- package/dist/integrations/agent/runner-dispatch.js +91 -4
- package/dist/integrations/agent/runner.js +76 -207
- package/dist/integrations/agent/spawn.js +141 -20
- package/dist/integrations/harnesses/aider/agent-builder.js +112 -0
- package/dist/integrations/harnesses/aider/index.js +57 -0
- package/dist/integrations/harnesses/aider/result-extractor.js +53 -0
- package/dist/integrations/harnesses/amazonq/agent-builder.js +152 -0
- package/dist/integrations/harnesses/amazonq/index.js +58 -0
- package/dist/integrations/harnesses/amazonq/result-extractor.js +48 -0
- package/dist/integrations/harnesses/claude/agent-builder.js +46 -8
- package/dist/integrations/harnesses/claude/index.js +25 -25
- package/dist/integrations/harnesses/claude/result-extractor.js +52 -0
- package/dist/integrations/harnesses/codex/agent-builder.js +136 -0
- package/dist/integrations/harnesses/codex/index.js +62 -0
- package/dist/integrations/harnesses/codex/result-extractor.js +73 -0
- package/dist/integrations/harnesses/copilot/agent-builder.js +121 -0
- package/dist/integrations/harnesses/copilot/index.js +59 -0
- package/dist/integrations/harnesses/copilot/result-extractor.js +151 -0
- package/dist/integrations/harnesses/gemini/agent-builder.js +120 -0
- package/dist/integrations/harnesses/gemini/index.js +59 -0
- package/dist/integrations/harnesses/gemini/result-extractor.js +121 -0
- package/dist/integrations/harnesses/index.js +27 -28
- package/dist/integrations/harnesses/opencode/agent-builder.js +2 -3
- package/dist/integrations/harnesses/opencode/index.js +15 -22
- package/dist/integrations/harnesses/opencode-sdk/harness.js +60 -0
- package/dist/integrations/harnesses/opencode-sdk/index.js +8 -32
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +681 -108
- package/dist/integrations/harnesses/openhands/agent-builder.js +125 -0
- package/dist/integrations/harnesses/openhands/index.js +57 -0
- package/dist/integrations/harnesses/openhands/result-extractor.js +103 -0
- package/dist/integrations/harnesses/pi/agent-builder.js +103 -0
- package/dist/integrations/harnesses/pi/index.js +57 -0
- package/dist/integrations/harnesses/pi/result-extractor.js +135 -0
- package/dist/integrations/harnesses/types.js +8 -32
- package/dist/integrations/lockfile.js +32 -21
- package/dist/integrations/session-logs/index.js +24 -11
- package/dist/llm/client.js +48 -14
- package/dist/llm/feature-gate.js +15 -47
- package/dist/llm/graph-extract.js +1 -1
- package/dist/llm/index-passes.js +8 -42
- package/dist/llm/memory-infer-impl.js +1 -1
- package/dist/llm/usage-persist.js +4 -0
- package/dist/llm/usage-telemetry.js +35 -5
- package/dist/output/renderers.js +3 -2
- package/dist/output/shapes/helpers.js +2 -1
- package/dist/output/shapes/passthrough.js +6 -0
- package/dist/output/text/helpers.js +215 -2
- package/dist/output/text/workflow.js +3 -1
- package/dist/schemas/akm-config.json +16638 -0
- package/dist/schemas/akm-task.json +87 -0
- package/dist/schemas/akm-workflow.json +372 -0
- package/dist/scripts/migrate-storage.js +10944 -8801
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9247 -350
- package/dist/setup/detected-engines.js +142 -0
- package/dist/setup/engine-config.js +89 -0
- package/dist/setup/setup.js +236 -132
- package/dist/setup/steps/connection.js +61 -32
- package/dist/setup/steps/platforms.js +4 -4
- package/dist/setup/steps.js +3 -2
- package/dist/storage/database.js +13 -1
- package/dist/storage/engines/sqlite-migrations.js +1 -0
- package/dist/storage/repositories/improve-runs-repository.js +5 -5
- package/dist/storage/repositories/task-history-repository.js +78 -0
- package/dist/storage/repositories/workflow-runs-repository.js +190 -1
- package/dist/tasks/parser.js +138 -52
- package/dist/tasks/runner.js +71 -75
- package/dist/tasks/schema.js +1 -1
- package/dist/tasks/validator.js +11 -6
- package/dist/text-import-hook.mjs +1 -1
- package/dist/wiki/wiki.js +9 -8
- package/dist/workflows/authoring/authoring.js +123 -10
- package/dist/workflows/authoring/workflow-program-template.yaml +31 -0
- package/dist/workflows/cli.js +4 -0
- package/dist/workflows/concurrency-policy.js +15 -0
- package/dist/workflows/db.js +200 -13
- package/dist/workflows/exec/brief.js +478 -0
- package/dist/workflows/exec/frozen-judge.js +47 -0
- package/dist/workflows/exec/native-executor.js +1034 -0
- package/dist/workflows/exec/param-secrets.js +115 -0
- package/dist/workflows/exec/report.js +1355 -0
- package/dist/workflows/exec/run-workflow.js +609 -0
- package/dist/workflows/exec/scheduler.js +71 -0
- package/dist/workflows/exec/step-work.js +1212 -0
- package/dist/workflows/exec/unit-writer.js +23 -0
- package/dist/workflows/exec/watch.js +116 -0
- package/dist/workflows/exec/worktree.js +171 -0
- package/dist/workflows/ir/compile.js +375 -0
- package/dist/workflows/ir/freeze.js +243 -0
- package/dist/workflows/ir/params.js +54 -0
- package/dist/workflows/ir/plan-hash.js +68 -0
- package/dist/workflows/ir/schema.js +545 -0
- package/dist/workflows/parser.js +10 -1
- package/dist/workflows/program/expressions.js +369 -0
- package/dist/workflows/program/parser.js +869 -0
- package/dist/workflows/program/project.js +104 -0
- package/dist/workflows/program/schema.js +54 -0
- package/dist/workflows/renderer.js +82 -5
- package/dist/workflows/resource-limits.js +20 -0
- package/dist/workflows/runtime/agent-identity.js +59 -14
- package/dist/workflows/runtime/plan-classifier.js +187 -0
- package/dist/workflows/runtime/runs.js +246 -69
- package/dist/workflows/runtime/unit-checkin.js +45 -0
- package/dist/workflows/runtime/workflow-asset-loader.js +42 -1
- package/dist/workflows/validate-summary.js +24 -3
- package/dist/workflows/validator.js +26 -1
- package/docs/data-and-telemetry.md +4 -3
- package/docs/migration/release-notes/0.6.0.md +1 -1
- package/docs/migration/release-notes/0.7.0.md +5 -4
- package/docs/migration/release-notes/0.9.0-beta.60.md +19 -0
- package/docs/migration/v0.8-to-v0.9.md +401 -0
- package/package.json +4 -2
- package/schemas/akm-config.json +16638 -0
- package/schemas/akm-task.json +87 -0
- package/schemas/akm-workflow.json +372 -0
- package/dist/commands/improve/improve-profiles.js +0 -168
- package/dist/core/config/config-migration.js +0 -602
- package/dist/core/deep-merge.js +0 -38
- package/dist/llm/call-ai.js +0 -62
- package/dist/setup/legacy-config.js +0 -106
- package/docs/README.md +0 -104
- /package/dist/assets/{profiles → improve-strategies}/catchup.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/consolidate.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/frequent.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/memory-focus.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/quick.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/recombine-only.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/synthesize.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/thorough.json +0 -0
|
@@ -0,0 +1,369 @@
|
|
|
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 deterministic expression language for YAML orchestration programs
|
|
6
|
+
* (R1 redesign addendum). `${{ ... }}` delimits references; everything else
|
|
7
|
+
* is literal text that passes through byte-exact.
|
|
8
|
+
*
|
|
9
|
+
* The grammar is CLOSED — exactly four roots, nothing else parses:
|
|
10
|
+
*
|
|
11
|
+
* params.<ident>
|
|
12
|
+
* steps.<ident>.output( .<ident> | [<non-negative int>] )*
|
|
13
|
+
* item
|
|
14
|
+
* item_index
|
|
15
|
+
*
|
|
16
|
+
* where <ident> is `[A-Za-z_][A-Za-z0-9_-]*`. No functions, no clock, no
|
|
17
|
+
* randomness, no ambient lookup — orchestration decisions stay pure
|
|
18
|
+
* functions of (frozen plan, params, journaled unit results).
|
|
19
|
+
*
|
|
20
|
+
* Templates are parsed ONCE into literal/reference segments; resolution is a
|
|
21
|
+
* single pass over that AST. Substituted content is data, never re-scanned,
|
|
22
|
+
* so a value containing `${{ params.x }}` is inserted literally — the P1
|
|
23
|
+
* re-scan injection bug class is structurally impossible.
|
|
24
|
+
*
|
|
25
|
+
* There is deliberately NO escape syntax in v1: a literal `${{` cannot
|
|
26
|
+
* appear in instructions. Authors who write one get a parse error from the
|
|
27
|
+
* validator (unterminated or invalid reference) telling them so.
|
|
28
|
+
*
|
|
29
|
+
* Pure module: no IO, no engine imports.
|
|
30
|
+
*/
|
|
31
|
+
const OPEN = "${{";
|
|
32
|
+
const CLOSE = "}}";
|
|
33
|
+
// ── Template parsing ─────────────────────────────────────────────────────────
|
|
34
|
+
/**
|
|
35
|
+
* Split a template into literal and reference segments. Literal text passes
|
|
36
|
+
* through byte-exact, including `$`, `{`, `}`, `${`, `{{`, and `}}` sequences
|
|
37
|
+
* that do not form the exact `${{` opener. Errors are returned, not thrown.
|
|
38
|
+
*/
|
|
39
|
+
export function parseTemplate(template) {
|
|
40
|
+
const segments = [];
|
|
41
|
+
const errors = [];
|
|
42
|
+
let cursor = 0;
|
|
43
|
+
while (cursor < template.length) {
|
|
44
|
+
const open = template.indexOf(OPEN, cursor);
|
|
45
|
+
if (open === -1)
|
|
46
|
+
break;
|
|
47
|
+
if (open > cursor)
|
|
48
|
+
segments.push({ kind: "literal", text: template.slice(cursor, open) });
|
|
49
|
+
const close = template.indexOf(CLOSE, open + OPEN.length);
|
|
50
|
+
const nested = template.indexOf(OPEN, open + OPEN.length);
|
|
51
|
+
if (close === -1) {
|
|
52
|
+
errors.push({
|
|
53
|
+
index: open,
|
|
54
|
+
message: `Unterminated ${OPEN} reference (no matching ${CLOSE}). ` +
|
|
55
|
+
`Note: there is no escape syntax — a literal ${OPEN} cannot appear in this text.`,
|
|
56
|
+
});
|
|
57
|
+
cursor = template.length;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
if (nested !== -1 && nested < close) {
|
|
61
|
+
errors.push({
|
|
62
|
+
index: nested,
|
|
63
|
+
message: `Nested ${OPEN} inside a reference — references cannot contain other references.`,
|
|
64
|
+
});
|
|
65
|
+
cursor = close + CLOSE.length;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const inner = template.slice(open + OPEN.length, close);
|
|
69
|
+
const parsed = parseExpression(inner);
|
|
70
|
+
if (parsed.ok) {
|
|
71
|
+
segments.push({
|
|
72
|
+
kind: "reference",
|
|
73
|
+
expr: parsed.expr,
|
|
74
|
+
raw: template.slice(open, close + CLOSE.length),
|
|
75
|
+
index: open,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
errors.push({ index: open, message: parsed.message });
|
|
80
|
+
}
|
|
81
|
+
cursor = close + CLOSE.length;
|
|
82
|
+
}
|
|
83
|
+
if (cursor < template.length)
|
|
84
|
+
segments.push({ kind: "literal", text: template.slice(cursor) });
|
|
85
|
+
return errors.length > 0 ? { ok: false, errors } : { ok: true, segments };
|
|
86
|
+
}
|
|
87
|
+
const GRAMMAR_HINT = "allowed forms: params.<name>, steps.<id>.output[...], item, item_index";
|
|
88
|
+
/**
|
|
89
|
+
* Parse a single expression (the text between `${{` and `}}`, or a bare
|
|
90
|
+
* whole-value field such as `map.over` after delimiter stripping).
|
|
91
|
+
*/
|
|
92
|
+
export function parseExpression(source) {
|
|
93
|
+
const text = source.trim();
|
|
94
|
+
if (text === "")
|
|
95
|
+
return { ok: false, message: `Empty expression inside ${OPEN} ${CLOSE}; ${GRAMMAR_HINT}.` };
|
|
96
|
+
const root = readIdent(text, 0);
|
|
97
|
+
if (!root) {
|
|
98
|
+
return { ok: false, message: `Invalid expression "${text}" — must start with an identifier; ${GRAMMAR_HINT}.` };
|
|
99
|
+
}
|
|
100
|
+
switch (root.name) {
|
|
101
|
+
case "item":
|
|
102
|
+
case "item_index": {
|
|
103
|
+
if (root.end !== text.length) {
|
|
104
|
+
return { ok: false, message: `"${root.name}" takes no path — found trailing "${text.slice(root.end)}".` };
|
|
105
|
+
}
|
|
106
|
+
return { ok: true, expr: root.name === "item" ? { kind: "item" } : { kind: "itemIndex" } };
|
|
107
|
+
}
|
|
108
|
+
case "params": {
|
|
109
|
+
if (text[root.end] !== ".") {
|
|
110
|
+
return { ok: false, message: `"params" requires a name: params.<name> (got "${text}").` };
|
|
111
|
+
}
|
|
112
|
+
const name = readIdent(text, root.end + 1);
|
|
113
|
+
if (!name) {
|
|
114
|
+
return { ok: false, message: `Invalid param name after "params." in "${text}".` };
|
|
115
|
+
}
|
|
116
|
+
if (name.end !== text.length) {
|
|
117
|
+
return {
|
|
118
|
+
ok: false,
|
|
119
|
+
message: `"params.${name.name}" takes exactly one name — found trailing "${text.slice(name.end)}".`,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return { ok: true, expr: { kind: "param", name: name.name } };
|
|
123
|
+
}
|
|
124
|
+
case "steps": {
|
|
125
|
+
if (text[root.end] !== ".") {
|
|
126
|
+
return { ok: false, message: `"steps" requires a step id: steps.<id>.output (got "${text}").` };
|
|
127
|
+
}
|
|
128
|
+
const stepId = readIdent(text, root.end + 1);
|
|
129
|
+
if (!stepId) {
|
|
130
|
+
return { ok: false, message: `Invalid step id after "steps." in "${text}".` };
|
|
131
|
+
}
|
|
132
|
+
if (text[stepId.end] !== ".") {
|
|
133
|
+
return { ok: false, message: `"steps.${stepId.name}" must be followed by ".output" (got "${text}").` };
|
|
134
|
+
}
|
|
135
|
+
const output = readIdent(text, stepId.end + 1);
|
|
136
|
+
if (!output || output.name !== "output") {
|
|
137
|
+
return { ok: false, message: `Expected ".output" after "steps.${stepId.name}" in "${text}".` };
|
|
138
|
+
}
|
|
139
|
+
const path = parsePath(text, output.end);
|
|
140
|
+
if (!path.ok)
|
|
141
|
+
return { ok: false, message: path.message };
|
|
142
|
+
return { ok: true, expr: { kind: "stepOutput", stepId: stepId.name, path: path.path } };
|
|
143
|
+
}
|
|
144
|
+
default:
|
|
145
|
+
return { ok: false, message: `Unknown root "${root.name}" in "${text}"; ${GRAMMAR_HINT}.` };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/** `<ident>` is `[A-Za-z_][A-Za-z0-9_-]*`. Returns null when no ident starts at `start`. */
|
|
149
|
+
function readIdent(text, start) {
|
|
150
|
+
if (start >= text.length || !/[A-Za-z_]/.test(text[start]))
|
|
151
|
+
return null;
|
|
152
|
+
let end = start + 1;
|
|
153
|
+
while (end < text.length && /[A-Za-z0-9_-]/.test(text[end]))
|
|
154
|
+
end++;
|
|
155
|
+
return { name: text.slice(start, end), end };
|
|
156
|
+
}
|
|
157
|
+
/** Parse `( .<ident> | [<non-negative int>] )*` from `start` to end of text. */
|
|
158
|
+
function parsePath(text, start) {
|
|
159
|
+
const path = [];
|
|
160
|
+
let i = start;
|
|
161
|
+
while (i < text.length) {
|
|
162
|
+
const char = text[i];
|
|
163
|
+
if (char === ".") {
|
|
164
|
+
const ident = readIdent(text, i + 1);
|
|
165
|
+
if (!ident) {
|
|
166
|
+
return { ok: false, message: `Invalid path segment after "." at position ${i} in "${text}".` };
|
|
167
|
+
}
|
|
168
|
+
path.push(ident.name);
|
|
169
|
+
i = ident.end;
|
|
170
|
+
}
|
|
171
|
+
else if (char === "[") {
|
|
172
|
+
let j = i + 1;
|
|
173
|
+
while (j < text.length && /[0-9]/.test(text[j]))
|
|
174
|
+
j++;
|
|
175
|
+
if (j === i + 1 || text[j] !== "]") {
|
|
176
|
+
return {
|
|
177
|
+
ok: false,
|
|
178
|
+
message: `Invalid indexer at position ${i} in "${text}" — expected [<non-negative integer>].`,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
path.push(Number.parseInt(text.slice(i + 1, j), 10));
|
|
182
|
+
i = j + 1;
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
return { ok: false, message: `Unexpected character "${char}" at position ${i} in "${text}".` };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return { ok: true, path };
|
|
189
|
+
}
|
|
190
|
+
// ── Resolution ───────────────────────────────────────────────────────────────
|
|
191
|
+
/**
|
|
192
|
+
* Resolve parsed segments against a scope in a SINGLE pass — resolved values
|
|
193
|
+
* are concatenated as data and never re-scanned for further references.
|
|
194
|
+
* Strings insert verbatim; numbers/booleans via String(); objects/arrays as
|
|
195
|
+
* canonical JSON (recursively sorted keys); null/undefined/missing paths are
|
|
196
|
+
* resolution errors naming the reference.
|
|
197
|
+
*/
|
|
198
|
+
export function resolveTemplate(segments, scope) {
|
|
199
|
+
const parts = [];
|
|
200
|
+
const errors = [];
|
|
201
|
+
for (const segment of segments) {
|
|
202
|
+
if (segment.kind === "literal") {
|
|
203
|
+
parts.push(segment.text);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const resolved = resolveReference(segment.expr, scope);
|
|
207
|
+
if (!resolved.ok) {
|
|
208
|
+
errors.push(resolved.error);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const value = resolved.value;
|
|
212
|
+
if (typeof value === "string") {
|
|
213
|
+
parts.push(value);
|
|
214
|
+
}
|
|
215
|
+
else if (typeof value === "number" || typeof value === "boolean") {
|
|
216
|
+
parts.push(String(value));
|
|
217
|
+
}
|
|
218
|
+
else if (typeof value === "object" && value !== null) {
|
|
219
|
+
parts.push(canonicalJson(value));
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
errors.push({
|
|
223
|
+
reference: formatReference(segment.expr),
|
|
224
|
+
message: `${formatReference(segment.expr)} resolved to an unsupported value type (${typeof value}).`,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return errors.length > 0 ? { ok: false, errors } : { ok: true, text: parts.join("") };
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Resolve a whole-value field (`map.over`, `route.input`) from its RAW
|
|
232
|
+
* template string: the text must be exactly one `${{ … }}` reference with no
|
|
233
|
+
* surrounding literal text (the compiler enforces this for YAML programs;
|
|
234
|
+
* frozen plans are re-checked here so a tampered/legacy plan fails loudly
|
|
235
|
+
* instead of string-splicing). The reference resolves to its RAW value —
|
|
236
|
+
* arrays stay arrays, objects stay objects.
|
|
237
|
+
*/
|
|
238
|
+
export function resolveWholeValue(template, scope) {
|
|
239
|
+
const parsed = parseTemplate(template);
|
|
240
|
+
if (!parsed.ok) {
|
|
241
|
+
return {
|
|
242
|
+
ok: false,
|
|
243
|
+
error: { reference: template, message: parsed.errors.map((e) => e.message).join(" ") },
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
const [first] = parsed.segments;
|
|
247
|
+
if (parsed.segments.length !== 1 || first?.kind !== "reference") {
|
|
248
|
+
return {
|
|
249
|
+
ok: false,
|
|
250
|
+
error: {
|
|
251
|
+
reference: template,
|
|
252
|
+
message: `expected a single whole-value \${{ … }} reference with no surrounding text ` +
|
|
253
|
+
`(e.g. "\${{ steps.discover.output.files }}"), got ${JSON.stringify(template)}.`,
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
return resolveReference(first.expr, scope);
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Resolve a single reference to its RAW value for whole-value contexts
|
|
261
|
+
* (`map.over`, `route.input`): arrays stay arrays, objects stay objects.
|
|
262
|
+
* null/undefined values and missing paths are errors, same as in templates.
|
|
263
|
+
*/
|
|
264
|
+
export function resolveReference(expr, scope) {
|
|
265
|
+
const reference = formatReference(expr);
|
|
266
|
+
const fail = (message) => ({ ok: false, error: { reference, message } });
|
|
267
|
+
switch (expr.kind) {
|
|
268
|
+
case "param": {
|
|
269
|
+
if (!Object.hasOwn(scope.params, expr.name)) {
|
|
270
|
+
return fail(`${reference} is not defined in the run's params.`);
|
|
271
|
+
}
|
|
272
|
+
return finish(scope.params[expr.name], reference, fail);
|
|
273
|
+
}
|
|
274
|
+
case "item": {
|
|
275
|
+
if (scope.item === undefined)
|
|
276
|
+
return fail("item is only available inside a map unit.");
|
|
277
|
+
return finish(scope.item, reference, fail);
|
|
278
|
+
}
|
|
279
|
+
case "itemIndex": {
|
|
280
|
+
if (typeof scope.itemIndex !== "number")
|
|
281
|
+
return fail("item_index is only available inside a map unit.");
|
|
282
|
+
return { ok: true, value: scope.itemIndex };
|
|
283
|
+
}
|
|
284
|
+
case "stepOutput": {
|
|
285
|
+
if (!Object.hasOwn(scope.stepOutputs, expr.stepId)) {
|
|
286
|
+
return fail(`steps.${expr.stepId}.output is not available — step "${expr.stepId}" has no recorded output.`);
|
|
287
|
+
}
|
|
288
|
+
let current = scope.stepOutputs[expr.stepId];
|
|
289
|
+
let walked = `steps.${expr.stepId}.output`;
|
|
290
|
+
for (const segment of expr.path) {
|
|
291
|
+
if (typeof segment === "number") {
|
|
292
|
+
if (!Array.isArray(current)) {
|
|
293
|
+
return fail(`${walked} is not an array — cannot resolve index [${segment}].`);
|
|
294
|
+
}
|
|
295
|
+
if (segment >= current.length) {
|
|
296
|
+
return fail(`${walked}[${segment}] is out of bounds (array length ${current.length}).`);
|
|
297
|
+
}
|
|
298
|
+
current = current[segment];
|
|
299
|
+
walked += `[${segment}]`;
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
if (typeof current !== "object" || current === null || Array.isArray(current)) {
|
|
303
|
+
return fail(`${walked} is not an object — cannot resolve property "${segment}".`);
|
|
304
|
+
}
|
|
305
|
+
if (!Object.hasOwn(current, segment)) {
|
|
306
|
+
return fail(`${walked}.${segment} is missing (no such property, resolving ${reference}).`);
|
|
307
|
+
}
|
|
308
|
+
current = current[segment];
|
|
309
|
+
walked += `.${segment}`;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return finish(current, reference, fail);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function finish(value, reference, fail) {
|
|
317
|
+
if (value === undefined)
|
|
318
|
+
return fail(`${reference} resolved to undefined.`);
|
|
319
|
+
if (value === null)
|
|
320
|
+
return fail(`${reference} resolved to null.`);
|
|
321
|
+
return { ok: true, value };
|
|
322
|
+
}
|
|
323
|
+
// ── Introspection helpers ────────────────────────────────────────────────────
|
|
324
|
+
/** The reference ASTs of a parsed template, in document order — for validator edge checking. */
|
|
325
|
+
export function listReferences(segments) {
|
|
326
|
+
const references = [];
|
|
327
|
+
for (const segment of segments) {
|
|
328
|
+
if (segment.kind === "reference")
|
|
329
|
+
references.push(segment.expr);
|
|
330
|
+
}
|
|
331
|
+
return references;
|
|
332
|
+
}
|
|
333
|
+
/** Canonical source spelling of a reference, e.g. `steps.review.output.files[2].name`. */
|
|
334
|
+
export function formatReference(expr) {
|
|
335
|
+
switch (expr.kind) {
|
|
336
|
+
case "param":
|
|
337
|
+
return `params.${expr.name}`;
|
|
338
|
+
case "item":
|
|
339
|
+
return "item";
|
|
340
|
+
case "itemIndex":
|
|
341
|
+
return "item_index";
|
|
342
|
+
case "stepOutput": {
|
|
343
|
+
let text = `steps.${expr.stepId}.output`;
|
|
344
|
+
for (const segment of expr.path) {
|
|
345
|
+
text += typeof segment === "number" ? `[${segment}]` : `.${segment}`;
|
|
346
|
+
}
|
|
347
|
+
return text;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
// ── Canonical JSON ───────────────────────────────────────────────────────────
|
|
352
|
+
/**
|
|
353
|
+
* Stable stringify with recursively sorted object keys, so equal values
|
|
354
|
+
* render identically regardless of key insertion order. Same pattern as the
|
|
355
|
+
* module-private helper in exec/native-executor.ts (not exported there).
|
|
356
|
+
*/
|
|
357
|
+
function canonicalJson(value) {
|
|
358
|
+
return JSON.stringify(sortKeys(value));
|
|
359
|
+
}
|
|
360
|
+
function sortKeys(value) {
|
|
361
|
+
if (Array.isArray(value))
|
|
362
|
+
return value.map(sortKeys);
|
|
363
|
+
if (value && typeof value === "object") {
|
|
364
|
+
return Object.fromEntries(Object.entries(value)
|
|
365
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
366
|
+
.map(([k, v]) => [k, sortKeys(v)]));
|
|
367
|
+
}
|
|
368
|
+
return value;
|
|
369
|
+
}
|