@intentius/chant 0.49.0 → 0.51.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audit/catalog.d.ts +13 -3
- package/dist/audit/catalog.d.ts.map +1 -1
- package/dist/audit/core.d.ts +9 -0
- package/dist/audit/core.d.ts.map +1 -1
- package/dist/audit/discover.d.ts +6 -0
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/audit/fetch.d.ts.map +1 -1
- package/dist/audit/report-html.d.ts.map +1 -1
- package/dist/audit/report-model.d.ts +6 -0
- package/dist/audit/report-model.d.ts.map +1 -1
- package/dist/audit/report.d.ts.map +1 -1
- package/dist/audit/rules-doc.d.ts.map +1 -1
- package/dist/audit/secrets.d.ts +95 -0
- package/dist/audit/secrets.d.ts.map +1 -0
- package/dist/audit/wrangler.d.ts +33 -0
- package/dist/audit/wrangler.d.ts.map +1 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/cli/commands/audit.d.ts +7 -0
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts +23 -0
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/components.d.ts +31 -0
- package/dist/cli/handlers/components.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts +11 -0
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/op-progress.d.ts +57 -0
- package/dist/cli/handlers/op-progress.d.ts.map +1 -0
- package/dist/cli/handlers/operator.d.ts +32 -0
- package/dist/cli/handlers/operator.d.ts.map +1 -0
- package/dist/cli/handlers/run-client.d.ts +21 -1
- package/dist/cli/handlers/run-client.d.ts.map +1 -1
- package/dist/cli/handlers/run-report.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/scenario.d.ts +39 -0
- package/dist/cli/handlers/scenario.d.ts.map +1 -0
- package/dist/cli/handlers/search.d.ts +22 -0
- package/dist/cli/handlers/search.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/op-tools.d.ts.map +1 -1
- package/dist/cli/mcp/resource-handlers.d.ts.map +1 -1
- package/dist/cli/mcp/server.d.ts +35 -2
- package/dist/cli/mcp/server.d.ts.map +1 -1
- package/dist/cli/mcp/types.d.ts +29 -1
- package/dist/cli/mcp/types.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +47 -3
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/codegen/docs-rule-scanning.d.ts.map +1 -1
- package/dist/components/capability.d.ts +17 -2
- package/dist/components/capability.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +7 -0
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/component.d.ts +15 -0
- package/dist/components/component.d.ts.map +1 -1
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/run-progress.d.ts +7 -5
- package/dist/components/run-progress.d.ts.map +1 -1
- package/dist/components/verbs/index.d.ts +6 -1
- package/dist/components/verbs/index.d.ts.map +1 -1
- package/dist/components/verbs/run-agent.d.ts +499 -0
- package/dist/components/verbs/run-agent.d.ts.map +1 -0
- package/dist/components/verbs/sign.d.ts +30 -0
- package/dist/components/verbs/sign.d.ts.map +1 -1
- package/dist/composite.d.ts +6 -1
- package/dist/composite.d.ts.map +1 -1
- package/dist/discovery/collect.d.ts.map +1 -1
- package/dist/discovery/fold-import.d.ts +15 -1
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/fold-rank.d.ts +66 -0
- package/dist/discovery/fold-rank.d.ts.map +1 -0
- package/dist/discovery/index.d.ts +15 -0
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/discovery/param-deps.d.ts +17 -0
- package/dist/discovery/param-deps.d.ts.map +1 -0
- package/dist/fold/fold.d.ts +55 -2
- package/dist/fold/fold.d.ts.map +1 -1
- package/dist/fold/subset.d.ts +21 -14
- package/dist/fold/subset.d.ts.map +1 -1
- package/dist/lexicon-schema.d.ts +2 -0
- package/dist/lexicon-schema.d.ts.map +1 -1
- package/dist/lexicon.d.ts +134 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/assert-live.d.ts +77 -0
- package/dist/lifecycle/assert-live.d.ts.map +1 -0
- package/dist/lifecycle/change-set.d.ts +17 -0
- package/dist/lifecycle/change-set.d.ts.map +1 -1
- package/dist/lifecycle/converge-ledger.d.ts +90 -0
- package/dist/lifecycle/converge-ledger.d.ts.map +1 -0
- package/dist/lifecycle/deep-diff.d.ts +18 -0
- package/dist/lifecycle/deep-diff.d.ts.map +1 -1
- package/dist/lifecycle/deep-observe.d.ts +9 -1
- package/dist/lifecycle/deep-observe.d.ts.map +1 -1
- package/dist/lifecycle/disruption.d.ts +96 -0
- package/dist/lifecycle/disruption.d.ts.map +1 -0
- package/dist/lifecycle/gate-ledger.d.ts +33 -0
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -0
- package/dist/lifecycle/git.d.ts +145 -21
- package/dist/lifecycle/git.d.ts.map +1 -1
- package/dist/lifecycle/index.d.ts +6 -0
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/lease.d.ts +113 -0
- package/dist/lifecycle/lease.d.ts.map +1 -0
- package/dist/lifecycle/replay.d.ts +2 -0
- package/dist/lifecycle/replay.d.ts.map +1 -1
- package/dist/lifecycle/scenario-eval.d.ts +42 -0
- package/dist/lifecycle/scenario-eval.d.ts.map +1 -0
- package/dist/lifecycle/scenario.d.ts +163 -0
- package/dist/lifecycle/scenario.d.ts.map +1 -0
- package/dist/lifecycle/symptoms.d.ts +63 -0
- package/dist/lifecycle/symptoms.d.ts.map +1 -0
- package/dist/lint/output-docs.d.ts +94 -0
- package/dist/lint/output-docs.d.ts.map +1 -0
- package/dist/lint/post-synth.d.ts +29 -0
- package/dist/lint/post-synth.d.ts.map +1 -1
- package/dist/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.d.ts +11 -0
- package/dist/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.d.ts.map +1 -0
- package/dist/lsp/lexicon-providers.d.ts +7 -0
- package/dist/lsp/lexicon-providers.d.ts.map +1 -1
- package/dist/op/activity-contract.d.ts +139 -0
- package/dist/op/activity-contract.d.ts.map +1 -0
- package/dist/op/builders.d.ts +42 -2
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/converge-rule.d.ts +161 -0
- package/dist/op/converge-rule.d.ts.map +1 -0
- package/dist/op/generate-pipeline.d.ts +39 -0
- package/dist/op/generate-pipeline.d.ts.map +1 -0
- package/dist/op/index.d.ts +14 -0
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/local-executor.d.ts +7 -1
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/op-verb-class.d.ts +42 -0
- package/dist/op/op-verb-class.d.ts.map +1 -0
- package/dist/op/operator.d.ts +128 -0
- package/dist/op/operator.d.ts.map +1 -0
- package/dist/op/step-output-ref.d.ts +187 -0
- package/dist/op/step-output-ref.d.ts.map +1 -0
- package/dist/op/types.d.ts +18 -1
- package/dist/op/types.d.ts.map +1 -1
- package/dist/provenance.d.ts +73 -3
- package/dist/provenance.d.ts.map +1 -1
- package/dist/runtime-adapter.d.ts +7 -1
- package/dist/runtime-adapter.d.ts.map +1 -1
- package/dist/serializer.d.ts +18 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/testing.d.ts +23 -2
- package/dist/testing.d.ts.map +1 -1
- package/dist/toml.d.ts +40 -5
- package/dist/toml.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/audit/catalog.test.ts +1 -1
- package/src/audit/catalog.ts +75 -3
- package/src/audit/core.ts +9 -0
- package/src/audit/discover.ts +29 -2
- package/src/audit/fetch.test.ts +216 -3
- package/src/audit/fetch.ts +270 -59
- package/src/audit/report-html.ts +5 -2
- package/src/audit/report-model.ts +9 -0
- package/src/audit/report.test.ts +22 -0
- package/src/audit/report.ts +3 -2
- package/src/audit/rules-doc.ts +2 -0
- package/src/audit/secrets.test.ts +303 -0
- package/src/audit/secrets.ts +406 -0
- package/src/audit/wrangler.test.ts +230 -0
- package/src/audit/wrangler.ts +290 -0
- package/src/build.ts +8 -3
- package/src/cli/command-group.ts +1 -1
- package/src/cli/commands/__fixtures__/schemas/sarif-2.1.0.schema.json +2882 -0
- package/src/cli/commands/audit.test.ts +215 -1
- package/src/cli/commands/audit.ts +86 -17
- package/src/cli/commands/build.test.ts +167 -2
- package/src/cli/commands/build.ts +114 -23
- package/src/cli/handlers/build.ts +2 -0
- package/src/cli/handlers/components.test.ts +199 -1
- package/src/cli/handlers/components.ts +160 -3
- package/src/cli/handlers/graph.test.ts +20 -0
- package/src/cli/handlers/graph.ts +10 -1
- package/src/cli/handlers/lifecycle.test.ts +90 -0
- package/src/cli/handlers/lifecycle.ts +30 -5
- package/src/cli/handlers/op-progress.test.ts +202 -0
- package/src/cli/handlers/op-progress.ts +192 -0
- package/src/cli/handlers/operator.test.ts +255 -0
- package/src/cli/handlers/operator.ts +240 -0
- package/src/cli/handlers/run-client.test.ts +82 -0
- package/src/cli/handlers/run-client.ts +85 -2
- package/src/cli/handlers/run-report.test.ts +62 -0
- package/src/cli/handlers/run-report.ts +20 -58
- package/src/cli/handlers/run.test.ts +144 -0
- package/src/cli/handlers/run.ts +40 -18
- package/src/cli/handlers/scenario.test.ts +456 -0
- package/src/cli/handlers/scenario.ts +330 -0
- package/src/cli/handlers/search-drift.test.ts +263 -0
- package/src/cli/handlers/search.ts +150 -1
- package/src/cli/main.test.ts +23 -0
- package/src/cli/main.ts +81 -1
- package/src/cli/mcp/op-tools.ts +17 -6
- package/src/cli/mcp/resource-handlers.ts +13 -5
- package/src/cli/mcp/server.test.ts +265 -2
- package/src/cli/mcp/server.ts +84 -7
- package/src/cli/mcp/types.ts +27 -1
- package/src/cli/registry.ts +47 -3
- package/src/codegen/docs-rule-scanning.test.ts +42 -0
- package/src/codegen/docs-rule-scanning.ts +25 -2
- package/src/components/README.md +7 -0
- package/src/components/capability.ts +17 -2
- package/src/components/cli-support.test.ts +17 -0
- package/src/components/cli-support.ts +13 -1
- package/src/components/component-schema.test.ts +32 -0
- package/src/components/component.schema.json +6 -0
- package/src/components/component.test.ts +21 -0
- package/src/components/component.ts +15 -0
- package/src/components/driver.ts +12 -4
- package/src/components/run-progress.ts +9 -5
- package/src/components/verbs/index.ts +6 -1
- package/src/components/verbs/run-agent.test.ts +683 -0
- package/src/components/verbs/run-agent.ts +786 -0
- package/src/components/verbs/sign.test.ts +19 -0
- package/src/components/verbs/sign.ts +34 -2
- package/src/composite.ts +31 -2
- package/src/discovery/collect.ts +11 -2
- package/src/discovery/fold-import.test.ts +54 -0
- package/src/discovery/fold-import.ts +178 -38
- package/src/discovery/fold-rank.test.ts +197 -0
- package/src/discovery/fold-rank.ts +346 -0
- package/src/discovery/index.ts +16 -1
- package/src/discovery/param-deps.test.ts +118 -0
- package/src/discovery/param-deps.ts +170 -0
- package/src/fold/fold.test.ts +6 -2
- package/src/fold/fold.ts +184 -3
- package/src/fold/subset.test.ts +82 -19
- package/src/fold/subset.ts +79 -41
- package/src/lexicon-schema.ts +3 -0
- package/src/lexicon.ts +154 -2
- package/src/lifecycle/assert-live.test.ts +125 -0
- package/src/lifecycle/assert-live.ts +154 -0
- package/src/lifecycle/change-set.ts +35 -3
- package/src/lifecycle/converge-ledger.test.ts +199 -0
- package/src/lifecycle/converge-ledger.ts +179 -0
- package/src/lifecycle/deep-diff.test.ts +79 -1
- package/src/lifecycle/deep-diff.ts +23 -0
- package/src/lifecycle/deep-observe.ts +13 -2
- package/src/lifecycle/disruption.test.ts +186 -0
- package/src/lifecycle/disruption.ts +224 -0
- package/src/lifecycle/gate-ledger.test.ts +103 -0
- package/src/lifecycle/gate-ledger.ts +140 -0
- package/src/lifecycle/git.test.ts +430 -0
- package/src/lifecycle/git.ts +446 -84
- package/src/lifecycle/index.ts +6 -0
- package/src/lifecycle/lease.test.ts +343 -0
- package/src/lifecycle/lease.ts +270 -0
- package/src/lifecycle/replay.test.ts +25 -0
- package/src/lifecycle/replay.ts +11 -3
- package/src/lifecycle/scenario-eval.test.ts +199 -0
- package/src/lifecycle/scenario-eval.ts +158 -0
- package/src/lifecycle/scenario.test.ts +195 -0
- package/src/lifecycle/scenario.ts +321 -0
- package/src/lifecycle/symptoms.test.ts +116 -0
- package/src/lifecycle/symptoms.ts +126 -0
- package/src/lint/output-docs.test.ts +220 -0
- package/src/lint/output-docs.ts +204 -0
- package/src/lint/post-synth.test.ts +97 -0
- package/src/lint/post-synth.ts +45 -0
- package/src/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.ts +26 -0
- package/src/lint/rules/comp/comp.test.ts +49 -1
- package/src/lint/rules/evl001-non-literal-expression.test.ts +8 -3
- package/src/lint/rules/evl001-non-literal-expression.ts +6 -6
- package/src/lsp/lexicon-providers.test.ts +44 -0
- package/src/lsp/lexicon-providers.ts +11 -1
- package/src/op/activity-contract.test.ts +180 -0
- package/src/op/activity-contract.ts +278 -0
- package/src/op/builders-exports.test.ts +17 -1
- package/src/op/builders.ts +59 -5
- package/src/op/converge-rule.test.ts +179 -0
- package/src/op/converge-rule.ts +311 -0
- package/src/op/generate-pipeline.test.ts +53 -0
- package/src/op/generate-pipeline.ts +99 -0
- package/src/op/index.ts +30 -0
- package/src/op/local-executor.test.ts +92 -0
- package/src/op/local-executor.ts +52 -10
- package/src/op/local-output.ts +1 -1
- package/src/op/op-verb-class.test.ts +126 -0
- package/src/op/op-verb-class.ts +115 -0
- package/src/op/operator.test.ts +346 -0
- package/src/op/operator.ts +213 -0
- package/src/op/step-output-ref.test.ts +334 -0
- package/src/op/step-output-ref.ts +453 -0
- package/src/op/types.ts +18 -1
- package/src/provenance.test.ts +151 -4
- package/src/provenance.ts +118 -4
- package/src/runtime-adapter.ts +31 -10
- package/src/serializer.ts +18 -0
- package/src/testing.test.ts +89 -2
- package/src/testing.ts +63 -3
- package/src/toml.test.ts +157 -384
- package/src/toml.ts +371 -5
|
@@ -23,11 +23,14 @@ const checks = loadComponentChecks();
|
|
|
23
23
|
/**
|
|
24
24
|
* Rollback dispositions the COMP003 fixtures reference, standing in for what
|
|
25
25
|
* `chant lint` derives from the project's registry (`cfn-deploy` has a native
|
|
26
|
-
* `rollback`; `run-migration` declares `needs-opt-out` in the aws lexicon
|
|
26
|
+
* `rollback`; `run-migration` declares `needs-opt-out` in the aws lexicon;
|
|
27
|
+
* `run-agent` declares `native` from phase 1 (#1941) — see
|
|
28
|
+
* `../../../components/verbs/run-agent.ts`'s `rollbackPolicy: "native"`).
|
|
27
29
|
*/
|
|
28
30
|
const FIXTURE_ROLLBACK_POLICIES = new Map<string, RollbackPolicy>([
|
|
29
31
|
["cfn-deploy", "native"],
|
|
30
32
|
["run-migration", "needs-opt-out"],
|
|
33
|
+
["run-agent", "native"],
|
|
31
34
|
]);
|
|
32
35
|
|
|
33
36
|
async function lintFixture(
|
|
@@ -124,6 +127,16 @@ describe("COMP003: mutating-no-rollback", () => {
|
|
|
124
127
|
expect(diagnostics.filter((d) => d.checkId === "COMP003")).toHaveLength(0);
|
|
125
128
|
});
|
|
126
129
|
|
|
130
|
+
it("does not flag a bare run-agent step — its registry-declared rollbackPolicy is \"native\" (#1941), never needing a noRollback opt-out (#1944)", async () => {
|
|
131
|
+
// Regression test: proves the registry's rollbackPolicy for "run-agent"
|
|
132
|
+
// is wired correctly into ctx.rollbackPolicies (the same seam COMP005
|
|
133
|
+
// uses for ctx.knownKinds), not just that COMP003 happens to stay quiet.
|
|
134
|
+
// See ../../../components/verbs/run-agent.ts's "rollbackPolicy: 'native'".
|
|
135
|
+
const diagnostics = await lintFixture("comp003", "pass", { rollbackPolicies: FIXTURE_ROLLBACK_POLICIES });
|
|
136
|
+
const hits = diagnostics.filter((d) => d.checkId === "COMP003" && d.component === "agent-turn");
|
|
137
|
+
expect(hits).toHaveLength(0);
|
|
138
|
+
});
|
|
139
|
+
|
|
127
140
|
it("flags a needs-opt-out step nested inside a fan-out phase, not just top-level phases", () => {
|
|
128
141
|
// Regression test: the rule used to iterate component.deploy directly and
|
|
129
142
|
// never recursed into nested Phase entries (a fan-out unit, e.g. the
|
|
@@ -163,6 +176,41 @@ describe("COMP003: mutating-no-rollback", () => {
|
|
|
163
176
|
expect(diagnostics[0].message).toContain("run-migration");
|
|
164
177
|
});
|
|
165
178
|
|
|
179
|
+
it("DOES flag a \"run-agent\"-kind step when a synthetic registry declares it needs-opt-out — proves the rule is conditional on the registry, not a hard-coded allowance for this one verb name (#1944)", () => {
|
|
180
|
+
// Companion negative case to the "does not flag a bare run-agent step"
|
|
181
|
+
// test above: same step kind, same shape, but with a synthetic
|
|
182
|
+
// rollbackPolicies map that declares "run-agent" as "needs-opt-out"
|
|
183
|
+
// instead of the real registry's "native". If this rule silently
|
|
184
|
+
// special-cased "run-agent" by name, this would incorrectly pass; it must
|
|
185
|
+
// flag exactly like any other needs-opt-out verb.
|
|
186
|
+
const [comp003] = checks.filter((c) => c.id === "COMP003");
|
|
187
|
+
const ctx = {
|
|
188
|
+
rollbackPolicies: new Map<string, RollbackPolicy>([["run-agent", "needs-opt-out"]]),
|
|
189
|
+
components: new Map([
|
|
190
|
+
[
|
|
191
|
+
"agent-turn",
|
|
192
|
+
{
|
|
193
|
+
component: {
|
|
194
|
+
name: "agent-turn",
|
|
195
|
+
dependsOn: [],
|
|
196
|
+
deploy: [
|
|
197
|
+
{
|
|
198
|
+
phase: "Run",
|
|
199
|
+
steps: [{ kind: "run-agent", agent: "claude", task: { prompt: "..." }, workspace: {} }],
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
},
|
|
203
|
+
filePath: "agent-turn.component.ts",
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
]),
|
|
207
|
+
};
|
|
208
|
+
const diagnostics = comp003.check(ctx as never);
|
|
209
|
+
expect(diagnostics).toHaveLength(1);
|
|
210
|
+
expect(diagnostics[0].component).toBe("agent-turn");
|
|
211
|
+
expect(diagnostics[0].message).toContain("run-agent");
|
|
212
|
+
});
|
|
213
|
+
|
|
166
214
|
it("a compensation sibling only counts within the same nested phase, not a same-named sibling phase elsewhere in the component", () => {
|
|
167
215
|
// Two different "Node" phases both contain a run-migration step; only the
|
|
168
216
|
// second one has a rollback-previous sibling. The first must still be
|
|
@@ -90,11 +90,16 @@ describe("EVL001: non-literal-expression", () => {
|
|
|
90
90
|
expect(diags[0].message).toContain("statically evaluable");
|
|
91
91
|
});
|
|
92
92
|
|
|
93
|
-
|
|
93
|
+
// chant #1966 — `fold()` widened to fold a method call when its receiver
|
|
94
|
+
// resolves (`github.actor.toString()`, `[...].join(",")`); `findSubsetViolation`
|
|
95
|
+
// (./subset.ts) accepts the SHAPE unconditionally, the same way it already
|
|
96
|
+
// does for `.step` and a nested `new` — whether the receiver actually
|
|
97
|
+
// resolves is resolution-dependent and out of reach here, so a method call
|
|
98
|
+
// no longer trips this rule on shape alone.
|
|
99
|
+
test("does not flag a method call — resolution-dependent, out of scope for a syntax-only rule (chant #1966)", () => {
|
|
94
100
|
const ctx = createContext(`new Bucket({ name: config.getName() });`);
|
|
95
101
|
const diags = evl001NonLiteralExpressionRule.check(ctx);
|
|
96
|
-
expect(diags).toHaveLength(
|
|
97
|
-
expect(diags[0].ruleId).toBe("EVL001");
|
|
102
|
+
expect(diags).toHaveLength(0);
|
|
98
103
|
});
|
|
99
104
|
|
|
100
105
|
test("flags await expression", () => {
|
|
@@ -31,14 +31,14 @@ function checkNode(node: ts.Node, context: LintContext, diagnostics: LintDiagnos
|
|
|
31
31
|
const firstArg = node.arguments[0];
|
|
32
32
|
if (ts.isObjectLiteralExpression(firstArg)) {
|
|
33
33
|
for (const prop of firstArg.properties) {
|
|
34
|
-
// chant #1544 — `allowCompositeStepAccess: true` opts EVL001 (and
|
|
35
|
-
// only EVL001; `fold()` never sets this) into treating
|
|
36
34
|
// `Checkout({...}).step` — the single-action Composite()-wrapper
|
|
37
35
|
// idiom every lexicon's own docs/examples embed inline inside a
|
|
38
|
-
// Job's `steps:` array —
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
|
|
36
|
+
// Job's `steps:` array — is shape-valid unconditionally (chant
|
|
37
|
+
// #1544 introduced this as an EVL-only opt-in while `fold()`
|
|
38
|
+
// still rejected the shape; chant #1174 closed that divergence by
|
|
39
|
+
// making `fold()` accept it too, so `findSubsetViolation` no
|
|
40
|
+
// longer needs a flag to say so).
|
|
41
|
+
const violation = checkObjectMember(prop, context.intrinsics);
|
|
42
42
|
if (violation) {
|
|
43
43
|
const { line, character } = context.sourceFile.getLineAndCharacterOfPosition(
|
|
44
44
|
violation.node.getStart(context.sourceFile),
|
|
@@ -53,3 +53,47 @@ describe("lexiconCompletions resource ranking (#600)", () => {
|
|
|
53
53
|
expect(items.length).toBe(100);
|
|
54
54
|
});
|
|
55
55
|
});
|
|
56
|
+
|
|
57
|
+
describe("lexiconCompletions deprecation marking (#1701)", () => {
|
|
58
|
+
/** One resource with a declared deprecation and an inferred one. */
|
|
59
|
+
function bucketIndex(): LexiconIndex {
|
|
60
|
+
return new LexiconIndex({
|
|
61
|
+
Bucket: {
|
|
62
|
+
resourceType: "Test::S3::Bucket",
|
|
63
|
+
kind: "resource",
|
|
64
|
+
lexicon: "test",
|
|
65
|
+
createOnly: ["AccessControl", "Runtime", "BucketName"],
|
|
66
|
+
deprecatedProperties: ["AccessControl", "Runtime"],
|
|
67
|
+
inferredDeprecations: ["Runtime"],
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const propCtx = (): CompletionContext =>
|
|
73
|
+
({
|
|
74
|
+
uri: "file:///t.ts",
|
|
75
|
+
content: "const b = new Bucket({\n ",
|
|
76
|
+
linePrefix: " ",
|
|
77
|
+
wordAtCursor: "",
|
|
78
|
+
position: { line: 1, character: 2 },
|
|
79
|
+
}) as unknown as CompletionContext;
|
|
80
|
+
|
|
81
|
+
test("a declared deprecation is marked deprecated", () => {
|
|
82
|
+
const items = lexiconCompletions(propCtx(), bucketIndex(), "Test resource");
|
|
83
|
+
const access = items.find((i) => i.label === "AccessControl");
|
|
84
|
+
expect(access?.deprecated).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("an inferred deprecation is not marked deprecated", () => {
|
|
88
|
+
const items = lexiconCompletions(propCtx(), bucketIndex(), "Test resource");
|
|
89
|
+
const runtime = items.find((i) => i.label === "Runtime");
|
|
90
|
+
expect(runtime).toBeDefined();
|
|
91
|
+
expect(runtime?.deprecated).toBeUndefined();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("a property with no deprecation signal is not marked", () => {
|
|
95
|
+
const items = lexiconCompletions(propCtx(), bucketIndex(), "Test resource");
|
|
96
|
+
const name = items.find((i) => i.label === "BucketName");
|
|
97
|
+
expect(name?.deprecated).toBeUndefined();
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -20,6 +20,13 @@ export interface LexiconEntry {
|
|
|
20
20
|
writeOnly?: string[];
|
|
21
21
|
primaryIdentifier?: string[];
|
|
22
22
|
deprecatedProperties?: string[];
|
|
23
|
+
/**
|
|
24
|
+
* Subset of `deprecatedProperties` that only prose supports — a lexicon
|
|
25
|
+
* generator matched the property description against a deprecation regex
|
|
26
|
+
* rather than reading an upstream declaration (#1701). Editors do not strike
|
|
27
|
+
* these through; the basis is too weak to mark a property dead.
|
|
28
|
+
*/
|
|
29
|
+
inferredDeprecations?: string[];
|
|
23
30
|
conditionalCreateOnly?: string[];
|
|
24
31
|
replacementStrategy?: "delete_then_create" | "create_then_delete";
|
|
25
32
|
tagging?: { taggable: boolean; tagOnCreate: boolean; tagUpdatable: boolean };
|
|
@@ -127,7 +134,10 @@ export function lexiconCompletions(
|
|
|
127
134
|
const entry = index.getEntry(className);
|
|
128
135
|
const props = index.getPropertyNames(className);
|
|
129
136
|
if (props.length > 0) {
|
|
130
|
-
const
|
|
137
|
+
const inferred = new Set(entry?.inferredDeprecations ?? []);
|
|
138
|
+
const deprecatedSet = new Set(
|
|
139
|
+
(entry?.deprecatedProperties ?? []).filter((p) => !inferred.has(p)),
|
|
140
|
+
);
|
|
131
141
|
const filtered = wordAtCursor
|
|
132
142
|
? props.filter((p) => p.toLowerCase().startsWith(wordAtCursor.toLowerCase()))
|
|
133
143
|
: props;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Activity contract tests (chant #1288 Stage 1).
|
|
3
|
+
*
|
|
4
|
+
* Exercises the four failure classes named in the issue, using a small
|
|
5
|
+
* lifecycleDiff-shaped contract so the test doesn't need a real lexicon.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, it } from "vitest";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { activity, phase, effect } from "./builders";
|
|
11
|
+
import type { OpConfig } from "./types";
|
|
12
|
+
import {
|
|
13
|
+
activityContract,
|
|
14
|
+
isActivityContract,
|
|
15
|
+
collectActivityContracts,
|
|
16
|
+
validateActivitySteps,
|
|
17
|
+
type ActivityContract,
|
|
18
|
+
} from "./activity-contract";
|
|
19
|
+
import { stepOutput } from "./step-output-ref";
|
|
20
|
+
import { EffectReceipt } from "../effect-receipt";
|
|
21
|
+
|
|
22
|
+
// ── activityContract() / isActivityContract() ──────────────────────────────
|
|
23
|
+
|
|
24
|
+
describe("activityContract()", () => {
|
|
25
|
+
it("brands its result so isActivityContract recognizes it", () => {
|
|
26
|
+
const contract = activityContract("noop", z.strictObject({}));
|
|
27
|
+
expect(isActivityContract(contract)).toBe(true);
|
|
28
|
+
expect(contract.name).toBe("noop");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("isActivityContract rejects a plain object with the same shape", () => {
|
|
32
|
+
expect(isActivityContract({ name: "noop", args: z.strictObject({}) })).toBe(false);
|
|
33
|
+
expect(isActivityContract(null)).toBe(false);
|
|
34
|
+
expect(isActivityContract(undefined)).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("collectActivityContracts()", () => {
|
|
39
|
+
it("collects every exported contract, keyed by its declared name — ignores non-contract exports", () => {
|
|
40
|
+
const mod = {
|
|
41
|
+
shellCmdContract: activityContract("shellCmd", z.strictObject({ cmd: z.string() })),
|
|
42
|
+
httpCheckContract: activityContract("httpCheck", z.strictObject({ url: z.string() })),
|
|
43
|
+
unrelatedExport: "not a contract",
|
|
44
|
+
SOME_CONSTANT: 42,
|
|
45
|
+
};
|
|
46
|
+
const into = new Map<string, ActivityContract>();
|
|
47
|
+
collectActivityContracts(mod, into);
|
|
48
|
+
expect([...into.keys()].sort()).toEqual(["httpCheck", "shellCmd"]);
|
|
49
|
+
expect(into.get("shellCmd")!.name).toBe("shellCmd");
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// ── validateActivitySteps() ─────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
const lifecycleDiffContract = activityContract(
|
|
56
|
+
"lifecycleDiff",
|
|
57
|
+
z.strictObject({ env: z.string(), live: z.boolean().optional() }),
|
|
58
|
+
z.object({ output: z.string(), exitCode: z.number(), drifted: z.boolean() }),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const helmInstallContract = activityContract(
|
|
62
|
+
"helmInstall",
|
|
63
|
+
z.strictObject({ name: z.string(), chart: z.string().optional(), namespace: z.string().optional() }),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
function opWith(config: Partial<OpConfig> & Pick<OpConfig, "phases">): Pick<OpConfig, "name" | "phases" | "onFailure"> {
|
|
67
|
+
return { name: "test-op", ...config };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
describe("validateActivitySteps() — passing Ops", () => {
|
|
71
|
+
it("returns no issues for a step matching its contract exactly", () => {
|
|
72
|
+
const config = opWith({ phases: [phase("Diff", [activity("lifecycleDiff", { env: "prod", live: true })])] });
|
|
73
|
+
const issues = validateActivitySteps(config, new Map([["lifecycleDiff", lifecycleDiffContract]]));
|
|
74
|
+
expect(issues).toEqual([]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("skips a step whose fn has no registered contract — non-breaking by design", () => {
|
|
78
|
+
const config = opWith({ phases: [phase("Deploy", [activity("somethingUnregistered", { anything: "goes" })])] });
|
|
79
|
+
const issues = validateActivitySteps(config, new Map([["lifecycleDiff", lifecycleDiffContract]]));
|
|
80
|
+
expect(issues).toEqual([]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("a valid outcomeAttribute.from path against the declared return schema passes", () => {
|
|
84
|
+
const step = activity("lifecycleDiff", { env: "prod" });
|
|
85
|
+
step.outcomeAttribute = { name: "Drift", from: "drifted" };
|
|
86
|
+
const config = opWith({ phases: [phase("Diff", [step])] });
|
|
87
|
+
const issues = validateActivitySteps(config, new Map([["lifecycleDiff", lifecycleDiffContract]]));
|
|
88
|
+
expect(issues).toEqual([]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("a step-output reference standing in for a typed arg does not trip a false-positive type mismatch (#1290)", () => {
|
|
92
|
+
// args.env is z.string() — a StepOutputRef placeholder there is not a
|
|
93
|
+
// string, but it's not a real error either; TMP013 validates it against
|
|
94
|
+
// the producer's return schema separately.
|
|
95
|
+
const step = activity("lifecycleDiff", { env: stepOutput("diff", "name") as unknown as string });
|
|
96
|
+
const config = opWith({ phases: [phase("Diff", [step])] });
|
|
97
|
+
const issues = validateActivitySteps(config, new Map([["lifecycleDiff", lifecycleDiffContract]]));
|
|
98
|
+
expect(issues).toEqual([]);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("a step-output reference elsewhere in args doesn't mask a genuinely unrecognized key", () => {
|
|
102
|
+
const step = activity("lifecycleDiff", { env: stepOutput("diff", "name") as unknown as string, typo: "oops" });
|
|
103
|
+
const config = opWith({ phases: [phase("Diff", [step])] });
|
|
104
|
+
const issues = validateActivitySteps(config, new Map([["lifecycleDiff", lifecycleDiffContract]]));
|
|
105
|
+
expect(issues.some((i) => i.message.includes("Unrecognized key") && i.message.includes("typo"))).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe("validateActivitySteps() — the four failure classes from #1288", () => {
|
|
110
|
+
it("flags an unknown profile — e.g. kubectlApply(..., { profile: \"longInfa\" })", () => {
|
|
111
|
+
const step = activity("kubectlApply", { manifest: "dist/k8s.yaml" }, "longInfa" as never);
|
|
112
|
+
const config = opWith({ phases: [phase("Deploy", [step])] });
|
|
113
|
+
const issues = validateActivitySteps(config, new Map());
|
|
114
|
+
expect(issues).toHaveLength(1);
|
|
115
|
+
expect(issues[0].message).toContain('unknown profile "longInfa"');
|
|
116
|
+
expect(issues[0].message).toContain("fastIdempotent");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("flags an arg key the schema doesn't recognize — e.g. helmInstall(..., { nameSpace: \"prod\" })", () => {
|
|
120
|
+
const step = activity("helmInstall", { name: "api", chart: "./chart", nameSpace: "prod" });
|
|
121
|
+
const config = opWith({ phases: [phase("Deploy", [step])] });
|
|
122
|
+
const issues = validateActivitySteps(config, new Map([["helmInstall", helmInstallContract]]));
|
|
123
|
+
expect(issues.some((i) => i.message.includes("Unrecognized key") && i.message.includes("nameSpace"))).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("flags a wrong/missing arg key — e.g. activity(\"lifecycleDiff\", { environment: \"prod\" })", () => {
|
|
127
|
+
const step = activity("lifecycleDiff", { environment: "prod" });
|
|
128
|
+
const config = opWith({ phases: [phase("Diff", [step])] });
|
|
129
|
+
const issues = validateActivitySteps(config, new Map([["lifecycleDiff", lifecycleDiffContract]]));
|
|
130
|
+
// `env` (required) is missing, and `environment` is an unrecognized key.
|
|
131
|
+
expect(issues.some((i) => i.message.includes("args.env"))).toBe(true);
|
|
132
|
+
expect(issues.some((i) => i.message.includes("Unrecognized key") && i.message.includes("environment"))).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("flags an outcomeAttribute.from path that doesn't exist on the declared return type", () => {
|
|
136
|
+
const step = activity("lifecycleDiff", { env: "prod" });
|
|
137
|
+
step.outcomeAttribute = { name: "Drift", from: "drifed" }; // typo: drifted
|
|
138
|
+
const config = opWith({ phases: [phase("Diff", [step])] });
|
|
139
|
+
const issues = validateActivitySteps(config, new Map([["lifecycleDiff", lifecycleDiffContract]]));
|
|
140
|
+
expect(issues.some((i) => i.message.includes('outcomeAttribute.from "drifed"'))).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("flags a wrong-typed arg value even when the key is right", () => {
|
|
144
|
+
const step = activity("lifecycleDiff", { env: 42 as unknown as string });
|
|
145
|
+
const config = opWith({ phases: [phase("Diff", [step])] });
|
|
146
|
+
const issues = validateActivitySteps(config, new Map([["lifecycleDiff", lifecycleDiffContract]]));
|
|
147
|
+
expect(issues.some((i) => i.message.includes("args.env"))).toBe(true);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe("validateActivitySteps() — structural coverage", () => {
|
|
152
|
+
it("walks onFailure phases too", () => {
|
|
153
|
+
const step = activity("lifecycleDiff", { environment: "prod" });
|
|
154
|
+
const config = opWith({ phases: [], onFailure: [phase("Rollback", [step])] });
|
|
155
|
+
const issues = validateActivitySteps(config, new Map([["lifecycleDiff", lifecycleDiffContract]]));
|
|
156
|
+
expect(issues.length).toBeGreaterThan(0);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("walks activity steps nested inside an effect step", () => {
|
|
160
|
+
const receipt = EffectReceipt("seeded", {
|
|
161
|
+
effect: "db-seed",
|
|
162
|
+
flavor: "hash",
|
|
163
|
+
inputs: { file: "seed.sql", version: 3 },
|
|
164
|
+
});
|
|
165
|
+
const badStep = activity("lifecycleDiff", { environment: "prod" });
|
|
166
|
+
const effectStep = effect(receipt, [badStep]);
|
|
167
|
+
const config = opWith({ phases: [phase("Seed", [effectStep])] });
|
|
168
|
+
const issues = validateActivitySteps(config, new Map([["lifecycleDiff", lifecycleDiffContract]]));
|
|
169
|
+
expect(issues.length).toBeGreaterThan(0);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("reports every issue across multiple steps, not just the first", () => {
|
|
173
|
+
const step1 = activity("lifecycleDiff", { environment: "prod" });
|
|
174
|
+
const step2 = activity("kubectlApply", {}, "longInfa" as never);
|
|
175
|
+
const config = opWith({ phases: [phase("P", [step1, step2])] });
|
|
176
|
+
const issues = validateActivitySteps(config, new Map([["lifecycleDiff", lifecycleDiffContract]]));
|
|
177
|
+
expect(issues.some((i) => i.fn === "lifecycleDiff")).toBe(true);
|
|
178
|
+
expect(issues.some((i) => i.fn === "kubectlApply")).toBe(true);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Activity contracts — the Stage 1 fix for chant #1288 ("Ops are the one
|
|
3
|
+
* place chant accepts untyped arguments and resolves names at runtime").
|
|
4
|
+
*
|
|
5
|
+
* Maintainer decision (#1288, 2026-08-25): a staged approach. Stage 1 (this
|
|
6
|
+
* module) is registered args/return schemas per activity, validated by
|
|
7
|
+
* `chant build` against every step — non-breaking, no change to
|
|
8
|
+
* {@link ActivityStep.args}'s `Record<string, unknown>` shape or the step
|
|
9
|
+
* builders. Stage 2 (a separate PR) regenerates the step builders as fully
|
|
10
|
+
* typed wrappers for editor completion and go-to-definition. #1289 (op.json
|
|
11
|
+
* IR) and #1290 (step-output references) build on these contracts — #1290
|
|
12
|
+
* specifically needs a declared `returns` schema to validate a later step's
|
|
13
|
+
* reference into an earlier one's output against, which is why `returns` is
|
|
14
|
+
* part of the shape here even though nothing in Stage 1 reads it for that.
|
|
15
|
+
*
|
|
16
|
+
* An activity declares a contract alongside its implementation — the same
|
|
17
|
+
* "registration surface" the issue asked for (`activity-registry.ts`'s
|
|
18
|
+
* `collectActivities` already does this for implementations; this is the
|
|
19
|
+
* schema-shaped sibling). `chant build` — via a lexicon's own post-synth
|
|
20
|
+
* check, e.g. the temporal lexicon's TMP012 — resolves each step's `fn`
|
|
21
|
+
* against a contract map built the same way and validates `args` and
|
|
22
|
+
* `outcomeAttribute.from`. A step whose `fn` has no registered contract is
|
|
23
|
+
* skipped (not an error): this is deliberately incremental — a lexicon opts
|
|
24
|
+
* an activity in by declaring a contract for it, and the k8s/aws/azure/gcp/
|
|
25
|
+
* fly activity sets are expected to pick this up lexicon by lexicon rather
|
|
26
|
+
* than all at once (see the issue's "worth checking this lands cleanly"
|
|
27
|
+
* note).
|
|
28
|
+
*
|
|
29
|
+
* Ownership is decentralized on purpose: each lexicon declares contracts for
|
|
30
|
+
* the activities it implements and validates them with its own post-synth
|
|
31
|
+
* check (the same `rulePrefix`-per-lexicon pattern every other check in
|
|
32
|
+
* chant already uses), rather than a shared cross-lexicon registry. A
|
|
33
|
+
* `Temporal::Op` step can call an activity contributed by any lexicon, and
|
|
34
|
+
* `PostSynthContext.entities` already carries the whole resolved graph to
|
|
35
|
+
* every lexicon's checks, so no new plumbing is needed for that to work.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import { z } from "zod";
|
|
39
|
+
import type { OpConfig, PhaseDefinition, ActivityStep, StepDefinition } from "./types";
|
|
40
|
+
|
|
41
|
+
/** Every literal value {@link ActivityStep.profile} may hold. Kept in sync with `types.ts`'s `ActivityStep["profile"]`. */
|
|
42
|
+
export const KNOWN_ACTIVITY_PROFILES = [
|
|
43
|
+
"fastIdempotent",
|
|
44
|
+
"longInfra",
|
|
45
|
+
"k8sWait",
|
|
46
|
+
"humanGate",
|
|
47
|
+
"argoSync",
|
|
48
|
+
"policyCheck",
|
|
49
|
+
] as const;
|
|
50
|
+
|
|
51
|
+
const CONTRACT_BRAND = Symbol.for("chant.op.activityContract");
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* A registered activity's args/return schemas — the declaration that lets
|
|
55
|
+
* `chant build` catch a typo'd or mistyped step before it ever reaches a
|
|
56
|
+
* cluster.
|
|
57
|
+
*
|
|
58
|
+
* Author `args` with `z.strictObject(...)` (or an equivalent that rejects
|
|
59
|
+
* unrecognized keys), not `z.object(...)`. Zod's default `.object()` silently
|
|
60
|
+
* drops a key it doesn't recognize instead of failing — exactly the
|
|
61
|
+
* `helmInstall("api", "./chart", { nameSpace: "prod" })` failure class the
|
|
62
|
+
* issue names, where the misspelled key vanishes instead of erroring. A
|
|
63
|
+
* strict schema turns that into a build error.
|
|
64
|
+
*/
|
|
65
|
+
export interface ActivityContract<Args = unknown, Return = unknown> {
|
|
66
|
+
readonly [CONTRACT_BRAND]: true;
|
|
67
|
+
/** The activity's registered name — must match a step's `fn`. */
|
|
68
|
+
name: string;
|
|
69
|
+
/** Schema every step's `args` (defaulted to `{}` when omitted) must satisfy. */
|
|
70
|
+
args: z.ZodType<Args>;
|
|
71
|
+
/**
|
|
72
|
+
* Schema the activity resolves to. Optional: an activity that returns
|
|
73
|
+
* nothing meaningful (or hasn't had its return type written down yet) can
|
|
74
|
+
* omit it. Needed to validate a step's `outcomeAttribute.from` path, and —
|
|
75
|
+
* per #1290 — a later step's reference into this one's output.
|
|
76
|
+
*/
|
|
77
|
+
returns?: z.ZodType<Return>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Declare an activity contract. */
|
|
81
|
+
export function activityContract<ArgsSchema extends z.ZodTypeAny, ReturnSchema extends z.ZodTypeAny = never>(
|
|
82
|
+
name: string,
|
|
83
|
+
args: ArgsSchema,
|
|
84
|
+
returns?: ReturnSchema,
|
|
85
|
+
): ActivityContract<z.infer<ArgsSchema>, ReturnSchema extends z.ZodTypeAny ? z.infer<ReturnSchema> : unknown> {
|
|
86
|
+
return {
|
|
87
|
+
[CONTRACT_BRAND]: true,
|
|
88
|
+
name,
|
|
89
|
+
args,
|
|
90
|
+
...(returns ? { returns } : {}),
|
|
91
|
+
} as ActivityContract<z.infer<ArgsSchema>, ReturnSchema extends z.ZodTypeAny ? z.infer<ReturnSchema> : unknown>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Structural guard for a value produced by {@link activityContract}. */
|
|
95
|
+
export function isActivityContract(value: unknown): value is ActivityContract {
|
|
96
|
+
return typeof value === "object" && value !== null && (value as Record<symbol, unknown>)[CONTRACT_BRAND] === true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Add every {@link ActivityContract} exported from an activity-contracts
|
|
101
|
+
* module to `into`, keyed by its declared `name` — the schema-shaped sibling
|
|
102
|
+
* of `activity-registry.ts`'s `collectActivities`.
|
|
103
|
+
*/
|
|
104
|
+
export function collectActivityContracts(mod: Record<string, unknown>, into: Map<string, ActivityContract>): void {
|
|
105
|
+
for (const value of Object.values(mod)) {
|
|
106
|
+
if (isActivityContract(value)) into.set(value.name, value);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── Validation ──────────────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
export interface ActivityContractIssue {
|
|
113
|
+
/** The Op the offending step belongs to. */
|
|
114
|
+
opName: string;
|
|
115
|
+
/** The phase the offending step belongs to. */
|
|
116
|
+
phase: string;
|
|
117
|
+
/** The activity name the offending step calls. */
|
|
118
|
+
fn: string;
|
|
119
|
+
/** Human-readable description of the mismatch. */
|
|
120
|
+
message: string;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Every `ActivityStep` in a phase, including ones nested inside an `EffectStep`. */
|
|
124
|
+
function activityStepsOf(steps: StepDefinition[]): ActivityStep[] {
|
|
125
|
+
return steps.flatMap((s) => (s.kind === "activity" ? [s] : s.kind === "effect" ? s.steps.filter((n) => n.kind === "activity") : []));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Same global symbol `step-output-ref.ts` brands a `StepOutputRef` with —
|
|
129
|
+
// `Symbol.for(...)` interns by string key, so this recognizes one without
|
|
130
|
+
// importing that module (which itself imports `pathExistsInSchema` below;
|
|
131
|
+
// importing the other way would make the two files a cycle).
|
|
132
|
+
const STEP_OUTPUT_REF_BRAND = Symbol.for("chant.op.stepOutputRef");
|
|
133
|
+
function isStepOutputRefValue(value: unknown): boolean {
|
|
134
|
+
return typeof value === "object" && value !== null && (value as Record<symbol, unknown>)[STEP_OUTPUT_REF_BRAND] === true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** The value at `path` (a zod issue's `.path`) inside `obj`, or `undefined` if any segment doesn't resolve. */
|
|
138
|
+
function valueAtPath(obj: unknown, path: ReadonlyArray<PropertyKey>): unknown {
|
|
139
|
+
let current = obj;
|
|
140
|
+
for (const segment of path) {
|
|
141
|
+
if (current === null || typeof current !== "object") return undefined;
|
|
142
|
+
current = (current as Record<PropertyKey, unknown>)[segment];
|
|
143
|
+
}
|
|
144
|
+
return current;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Unwrap `ZodOptional`/`ZodNullable`/`ZodDefault` (and similar) down to the schema they wrap. */
|
|
148
|
+
function unwrap(schema: z.ZodTypeAny): z.ZodTypeAny {
|
|
149
|
+
let current = schema;
|
|
150
|
+
while (typeof (current as unknown as { unwrap?: () => z.ZodTypeAny }).unwrap === "function") {
|
|
151
|
+
current = (current as unknown as { unwrap: () => z.ZodTypeAny }).unwrap();
|
|
152
|
+
}
|
|
153
|
+
return current;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Does dot-path `path` resolve to a field that exists on `schema`? Object
|
|
158
|
+
* shapes only — a return schema whose root (or an intermediate segment) is
|
|
159
|
+
* a `z.record(...)`/`z.array(...)` rather than a `z.object(...)` hard-errors
|
|
160
|
+
* (returns `false`) instead of skipping, deliberately (chant #1290 comment
|
|
161
|
+
* on #1288's pre-merge review): a record's keys are dynamic and an array's
|
|
162
|
+
* elements are index-addressed, neither of which a dot-path segment can
|
|
163
|
+
* check against in any way that's more meaningful than "the author probably
|
|
164
|
+
* meant something else." No declared `returns` schema needs this today, so
|
|
165
|
+
* there's no live case to design against yet. The escape hatch is an empty
|
|
166
|
+
* path — `outcomeAttribute.from` omitted, or a {@link StepOutputRef}'s
|
|
167
|
+
* `path` omitted — which references the whole return value and never calls
|
|
168
|
+
* this function; a record/array-returning activity's whole value is always
|
|
169
|
+
* a valid reference target.
|
|
170
|
+
*/
|
|
171
|
+
export function pathExistsInSchema(schema: z.ZodTypeAny, path: string): boolean {
|
|
172
|
+
return schemaAtPath(schema, path.split(".")) !== undefined;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The zod schema at property-key path `path` inside `schema`, walking
|
|
177
|
+
* through `z.ZodObject` shapes only (unwrapping optional/nullable/default at
|
|
178
|
+
* each level, same as {@link pathExistsInSchema}). `undefined` when a
|
|
179
|
+
* segment doesn't resolve, or an intermediate schema isn't a `z.ZodObject` —
|
|
180
|
+
* same record/array hard-stop {@link pathExistsInSchema} documents. An empty
|
|
181
|
+
* `path` returns `schema` itself (unwrapped).
|
|
182
|
+
*/
|
|
183
|
+
export function schemaAtPath(schema: z.ZodTypeAny, path: ReadonlyArray<string>): z.ZodTypeAny | undefined {
|
|
184
|
+
let current = unwrap(schema);
|
|
185
|
+
for (const segment of path) {
|
|
186
|
+
if (!(current instanceof z.ZodObject)) return undefined;
|
|
187
|
+
const shape = current.shape as Record<string, z.ZodTypeAny>;
|
|
188
|
+
if (!(segment in shape)) return undefined;
|
|
189
|
+
current = unwrap(shape[segment]);
|
|
190
|
+
}
|
|
191
|
+
return current;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* A primitive-shape classification of a zod schema — `string`/`number`/
|
|
196
|
+
* `boolean`/`object`/`array`, or `undefined` for anything else (a union,
|
|
197
|
+
* enum, literal, `z.any()`/`z.unknown()`, a transform, …). Used by the
|
|
198
|
+
* step-output-ref cross-contract type check (chant #1950-3) to compare a
|
|
199
|
+
* producer's declared return type against a consumer's declared arg type at
|
|
200
|
+
* the same structural position — deliberately shallow: it bails (returns
|
|
201
|
+
* `undefined`) on anything fancier than these five kinds rather than trying
|
|
202
|
+
* to reason about it, per that check's "bail out silently" design.
|
|
203
|
+
*/
|
|
204
|
+
export type PrimitiveSchemaKind = "string" | "number" | "boolean" | "object" | "array";
|
|
205
|
+
|
|
206
|
+
export function primitiveKindOf(schema: z.ZodTypeAny): PrimitiveSchemaKind | undefined {
|
|
207
|
+
const s = unwrap(schema);
|
|
208
|
+
if (s instanceof z.ZodString) return "string";
|
|
209
|
+
if (s instanceof z.ZodNumber) return "number";
|
|
210
|
+
if (s instanceof z.ZodBoolean) return "boolean";
|
|
211
|
+
if (s instanceof z.ZodObject) return "object";
|
|
212
|
+
if (s instanceof z.ZodArray) return "array";
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Validate every activity step in an Op's phases (main and `onFailure`)
|
|
218
|
+
* against a contract map. A step whose `fn` has no entry in `contracts` is
|
|
219
|
+
* skipped — Stage 1 is opt-in per activity, not a hard requirement that
|
|
220
|
+
* every activity have a declared contract.
|
|
221
|
+
*
|
|
222
|
+
* Catches the four failure classes chant #1288 names:
|
|
223
|
+
* - an unrecognized `profile` (checked against {@link KNOWN_ACTIVITY_PROFILES}, independent of whether `fn` has a contract),
|
|
224
|
+
* - an args key the declared schema doesn't recognize,
|
|
225
|
+
* - an args value of the wrong type (including a required key that's missing),
|
|
226
|
+
* - an `outcomeAttribute.from` path that can't exist on the declared return type.
|
|
227
|
+
*/
|
|
228
|
+
export function validateActivitySteps(
|
|
229
|
+
config: Pick<OpConfig, "name" | "phases" | "onFailure">,
|
|
230
|
+
contracts: ReadonlyMap<string, ActivityContract>,
|
|
231
|
+
): ActivityContractIssue[] {
|
|
232
|
+
const issues: ActivityContractIssue[] = [];
|
|
233
|
+
const phasesToWalk: PhaseDefinition[] = [...config.phases, ...(config.onFailure ?? [])];
|
|
234
|
+
|
|
235
|
+
for (const phase of phasesToWalk) {
|
|
236
|
+
for (const step of activityStepsOf(phase.steps)) {
|
|
237
|
+
if (step.profile && !(KNOWN_ACTIVITY_PROFILES as readonly string[]).includes(step.profile)) {
|
|
238
|
+
issues.push({
|
|
239
|
+
opName: config.name,
|
|
240
|
+
phase: phase.name,
|
|
241
|
+
fn: step.fn,
|
|
242
|
+
message: `unknown profile "${step.profile}" (known: ${KNOWN_ACTIVITY_PROFILES.join(", ")})`,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const contract = contracts.get(step.fn);
|
|
247
|
+
if (!contract) continue;
|
|
248
|
+
|
|
249
|
+
const parsed = contract.args.safeParse(step.args ?? {});
|
|
250
|
+
if (!parsed.success) {
|
|
251
|
+
for (const issue of parsed.error.issues) {
|
|
252
|
+
// A step-output reference (#1290) sitting at this path is a
|
|
253
|
+
// placeholder object at build time, not the value it will
|
|
254
|
+
// resolve to — so an args-schema type mismatch here is a false
|
|
255
|
+
// positive; TMP013 (`validateStepOutputRefs`) is what validates
|
|
256
|
+
// a reference, against the *producer's* declared return schema.
|
|
257
|
+
// An unrecognized-key issue's path is the parent object (`[]`
|
|
258
|
+
// for a top-level extra key), which is never itself a reference,
|
|
259
|
+
// so a genuinely misspelled key is still caught either way.
|
|
260
|
+
if (isStepOutputRefValue(valueAtPath(step.args, issue.path))) continue;
|
|
261
|
+
const path = issue.path.length > 0 ? issue.path.join(".") : "(args)";
|
|
262
|
+
issues.push({ opName: config.name, phase: phase.name, fn: step.fn, message: `args.${path}: ${issue.message}` });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (step.outcomeAttribute?.from && contract.returns && !pathExistsInSchema(contract.returns, step.outcomeAttribute.from)) {
|
|
267
|
+
issues.push({
|
|
268
|
+
opName: config.name,
|
|
269
|
+
phase: phase.name,
|
|
270
|
+
fn: step.fn,
|
|
271
|
+
message: `outcomeAttribute.from "${step.outcomeAttribute.from}" does not exist on "${step.fn}"'s declared return type`,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return issues;
|
|
278
|
+
}
|