@intentius/chant 0.49.0 → 0.50.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/operator.d.ts +32 -0
- package/dist/cli/handlers/operator.d.ts.map +1 -0
- package/dist/cli/handlers/scenario.d.ts +39 -0
- package/dist/cli/handlers/scenario.d.ts.map +1 -0
- package/dist/cli/main.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 +14 -2
- 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/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 +93 -0
- package/dist/lexicon.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/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 +4 -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/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.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/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.ts +12 -4
- package/src/cli/handlers/operator.test.ts +255 -0
- package/src/cli/handlers/operator.ts +240 -0
- package/src/cli/handlers/scenario.test.ts +456 -0
- package/src/cli/handlers/scenario.ts +330 -0
- package/src/cli/main.test.ts +23 -0
- package/src/cli/main.ts +72 -1
- 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 +14 -2
- 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/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 +103 -2
- 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/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 +4 -0
- package/src/lifecycle/lease.test.ts +343 -0
- package/src/lifecycle/lease.ts +270 -0
- 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 +45 -9
- 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/toml.test.ts +157 -384
- package/src/toml.ts +371 -5
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Step-output references — chant #1290 ("A step cannot reference a prior
|
|
3
|
+
* step's output, so values leave an Op only as search attributes").
|
|
4
|
+
*
|
|
5
|
+
* An Op's steps run in a fixed sequence chant already knows: phases in
|
|
6
|
+
* order, and (within a non-parallel phase) steps in order. Today the only
|
|
7
|
+
* way a value escapes a step is `outcomeAttribute`, which stringifies it
|
|
8
|
+
* into a Temporal search attribute — a UI filter, not something a later
|
|
9
|
+
* step can consume. `StepOutputRef` is the mechanism that lets a later
|
|
10
|
+
* step's `args` hold a *reference* to an earlier step's declared return
|
|
11
|
+
* value instead.
|
|
12
|
+
*
|
|
13
|
+
* Deliberately a reference, not an expression: `diff.out.driftedStacks` is
|
|
14
|
+
* a value placeholder the build resolves and the serializer compiles into a
|
|
15
|
+
* real local variable in the generated workflow — never `diff.out.count >
|
|
16
|
+
* 0` or a `.map()` over a reference. That property (an Op is data you can
|
|
17
|
+
* read and know what it does, not a program) is exactly what makes it safe
|
|
18
|
+
* to add this without Ops becoming programs; see the issue's "line not to
|
|
19
|
+
* cross".
|
|
20
|
+
*
|
|
21
|
+
* Scope, kept deliberately tight (see the issue's "Open questions"):
|
|
22
|
+
* - same-Op only — no cross-Op plumbing (that needs a durable place to put
|
|
23
|
+
* the value, which #1290 explicitly defers).
|
|
24
|
+
* - main `phases` only — a step inside `onFailure` or nested inside an
|
|
25
|
+
* `EffectStep` can neither produce nor consume a step-output reference.
|
|
26
|
+
* `onFailure` compensation runs only when something upstream already
|
|
27
|
+
* failed, so a main-phase step's captured result isn't reliably
|
|
28
|
+
* available there; an effect step's nested steps run only when the
|
|
29
|
+
* receipt comparison mismatches, so a value it produces isn't reliably
|
|
30
|
+
* available to anything outside it either. Both are conditional
|
|
31
|
+
* execution paths, which is exactly the class of thing #1290 keeps out
|
|
32
|
+
* of an Op's inert-data model.
|
|
33
|
+
* - producer must run to completion before the consumer starts: an earlier
|
|
34
|
+
* phase, or an earlier step within the same non-parallel phase. Two
|
|
35
|
+
* steps in the same parallel phase run concurrently (`Promise.all`) with
|
|
36
|
+
* no ordering guarantee, so neither may reference the other.
|
|
37
|
+
* - the producer needs a registered `ActivityContract` with a `returns`
|
|
38
|
+
* schema — without one there is nothing to validate the reference
|
|
39
|
+
* against, and it degrades into the same stringly path `outcomeAttribute`
|
|
40
|
+
* already had (chant #1288 comment on this issue).
|
|
41
|
+
*
|
|
42
|
+
* Cross-contract type compatibility (chant #1950 pre-merge review, finding
|
|
43
|
+
* 3): `validateStepOutputRefs` checks the producer side (a registered
|
|
44
|
+
* contract with a `returns` schema, and — when `path` is set — that the path
|
|
45
|
+
* resolves on it) and, separately, `validateActivitySteps`/TMP012 checks a
|
|
46
|
+
* step's `args` against its own contract's `args` schema — but a
|
|
47
|
+
* {@link StepOutputRef} sitting in `args` is a placeholder object at build
|
|
48
|
+
* time, not the value it resolves to, so TMP012 skips it (see
|
|
49
|
+
* `activity-contract.ts`'s `isStepOutputRefValue` guard) rather than
|
|
50
|
+
* false-positive on it. Nothing before this compared the *consumer's*
|
|
51
|
+
* declared type at that position against the *producer's* declared type at
|
|
52
|
+
* `path` — a string-returning path could feed a number-typed arg and both
|
|
53
|
+
* checks would pass. `validateStepOutputRefs` now adds one more check for
|
|
54
|
+
* exactly this: when `path` is set, and both the producer's and the
|
|
55
|
+
* consumer's contracts are registered, it compares the zod schema at the
|
|
56
|
+
* producer's `path` against the zod schema at the consumer's arg position by
|
|
57
|
+
* primitive shape (`string`/`number`/`boolean`/`object`/`array` — see
|
|
58
|
+
* `activity-contract.ts`'s `primitiveKindOf`) and flags a mismatch. This is
|
|
59
|
+
* deliberately shallow, the same "hard stop rather than guess" posture
|
|
60
|
+
* `pathExistsInSchema`'s doc above takes for a record/array root: a union, an
|
|
61
|
+
* enum, a literal, `z.any()`/`z.unknown()`, a transform, or a ref sitting
|
|
62
|
+
* inside an array in `args` (no stable position to look up an arg schema
|
|
63
|
+
* against) all bail silently — no issue raised, not a false negative
|
|
64
|
+
* reported as clean, just genuinely out of this check's reach. A more
|
|
65
|
+
* complete version (real zod-schema subtyping/union-aware comparison) is
|
|
66
|
+
* exactly the kind of thing #1288 Stage 2 (typed step builders, deriving
|
|
67
|
+
* from contracts instead of restating them) would need anyway; deferred
|
|
68
|
+
* there rather than grown ad hoc here. A whole-value reference (no `path`)
|
|
69
|
+
* is skipped by this check for the same reason it's skipped by the
|
|
70
|
+
* path-existence check: there's no single field to compare.
|
|
71
|
+
*
|
|
72
|
+
* References only, no expressions: the issue asks for this to be "enforced
|
|
73
|
+
* by a lint rule rather than left as a convention." What's here instead is
|
|
74
|
+
* a runtime-on-load guard — `diff.out.count > 0` or a template literal over
|
|
75
|
+
* a reference coerces it to a primitive, and the reference throws when that
|
|
76
|
+
* happens, at the moment chant loads the `.op.ts` module to build the
|
|
77
|
+
* entity graph (i.e. still before anything runs). A real static lint rule
|
|
78
|
+
* would need to parse-inspect the author's TypeScript for expression syntax
|
|
79
|
+
* around a reference — a genuine ESLint-rule-shaped feature, and a bigger
|
|
80
|
+
* one than this issue's scope. The coercion guard catches the same misuse
|
|
81
|
+
* (arithmetic, comparisons, template literals, `String(...)`) without it;
|
|
82
|
+
* a `.map()`-style structural misuse already throws on its own (`StepOutputRef`
|
|
83
|
+
* has no array methods).
|
|
84
|
+
*/
|
|
85
|
+
|
|
86
|
+
import { z } from "zod";
|
|
87
|
+
import type { ActivityStep, OpConfig, PhaseDefinition } from "./types";
|
|
88
|
+
import { pathExistsInSchema, schemaAtPath, primitiveKindOf, type ActivityContract, type ActivityContractIssue } from "./activity-contract";
|
|
89
|
+
|
|
90
|
+
const STEP_OUTPUT_REF_BRAND = Symbol.for("chant.op.stepOutputRef");
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* A typed reference to a prior step's declared return value. Inert by
|
|
94
|
+
* construction — it carries a producer step id and an optional dot-path
|
|
95
|
+
* into that producer's return schema, resolved by `chant build`
|
|
96
|
+
* (`validateStepOutputRefs`) and compiled by the temporal serializer into a
|
|
97
|
+
* local variable holding the awaited activity result.
|
|
98
|
+
*/
|
|
99
|
+
export interface StepOutputRef {
|
|
100
|
+
readonly [STEP_OUTPUT_REF_BRAND]: true;
|
|
101
|
+
readonly kind: "step-output-ref";
|
|
102
|
+
/** `ActivityStep.id` of the producing step. */
|
|
103
|
+
readonly step: string;
|
|
104
|
+
/**
|
|
105
|
+
* Dot-path into the producer's declared return value (e.g.
|
|
106
|
+
* `"driftedStacks"`, `"result.healthy"`). Omitted references the whole
|
|
107
|
+
* return value.
|
|
108
|
+
*/
|
|
109
|
+
readonly path?: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* `T` with every own property additionally accepting a {@link StepOutputRef}
|
|
114
|
+
* in its place (chant #1288 Stage 2) — the authoring-time counterpart of
|
|
115
|
+
* `args` accepting a reference anywhere in the structure (#1290): a typed
|
|
116
|
+
* step-builder wrapper whose opts type is `WithStepRefs<SomeActivityArgs>`
|
|
117
|
+
* lets an author pass `diff.out.driftedStacks` for any field without an
|
|
118
|
+
* `as` cast, while runtime validation of the reference itself is still
|
|
119
|
+
* `validateStepOutputRefs`' job, not this type's.
|
|
120
|
+
*/
|
|
121
|
+
export type WithStepRefs<T> = { [K in keyof T]: T[K] | StepOutputRef };
|
|
122
|
+
|
|
123
|
+
/** Structural guard for a value produced by {@link stepOutput} (or `activity()`'s `.out`). */
|
|
124
|
+
export function isStepOutputRef(value: unknown): value is StepOutputRef {
|
|
125
|
+
return typeof value === "object" && value !== null && (value as Record<symbol, unknown>)[STEP_OUTPUT_REF_BRAND] === true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function makeStepOutputRef(step: string, path?: string): StepOutputRef {
|
|
129
|
+
const ref = { [STEP_OUTPUT_REF_BRAND]: true, kind: "step-output-ref", step, ...(path ? { path } : {}) } as StepOutputRef;
|
|
130
|
+
// References only, no expressions (see module doc): coercing a reference
|
|
131
|
+
// to a primitive — `diff.out.count > 0`, `` `${diff.out.name}` ``,
|
|
132
|
+
// `String(diff.out.x)` — is exactly the "Op becomes a program" failure
|
|
133
|
+
// mode the issue calls out, so it throws immediately on load instead of
|
|
134
|
+
// silently producing a wrong value (NaN comparisons, "[object Object]").
|
|
135
|
+
const rejectExpression = (): never => {
|
|
136
|
+
throw new Error(
|
|
137
|
+
`step-output reference (step "${step}"${path ? `, path "${path}"` : ""}) was coerced to a primitive — ` +
|
|
138
|
+
"references only, no expressions: use it as a plain arg value, never inside a comparison, arithmetic, or template literal.",
|
|
139
|
+
);
|
|
140
|
+
};
|
|
141
|
+
Object.defineProperty(ref, Symbol.toPrimitive, { value: rejectExpression, enumerable: false });
|
|
142
|
+
Object.defineProperty(ref, "toString", { value: rejectExpression, enumerable: false });
|
|
143
|
+
Object.defineProperty(ref, "valueOf", { value: rejectExpression, enumerable: false });
|
|
144
|
+
return ref;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Reference a named step's output from a later step's `args`. Works with
|
|
149
|
+
* any step that has an `id` — an `activity()` builder result (which also
|
|
150
|
+
* gets the `.out` proxy sugar below) or a plain `{ kind: "activity", ...,
|
|
151
|
+
* id: "..." }` object literal, the shape composites author directly.
|
|
152
|
+
*
|
|
153
|
+
* `stepOutput(diff, "driftedStacks")` and `diff.out.driftedStacks` produce
|
|
154
|
+
* an identical reference; `stepOutput` is the explicit form for authors who
|
|
155
|
+
* build steps as object literals, or whose return schema has a top-level
|
|
156
|
+
* field literally named `step`, `path`, or `kind` (the `.out` proxy
|
|
157
|
+
* reserves those three property names for its own introspection).
|
|
158
|
+
*/
|
|
159
|
+
export function stepOutput(step: string | Pick<ActivityStep, "id">, path?: string): StepOutputRef {
|
|
160
|
+
const id = typeof step === "string" ? step : step.id;
|
|
161
|
+
if (!id) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
"stepOutput(): the step has no `id` — pass one via activity(fn, args, { id: \"...\" }) or set `id` on the step object literal first.",
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
return makeStepOutputRef(id, path);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* `.out` proxy attached to `activity()`'s result: `diff.out.driftedStacks`
|
|
171
|
+
* builds a {@link StepOutputRef} without the explicit `stepOutput()` call.
|
|
172
|
+
*
|
|
173
|
+
* A single property-access level, matching `outcomeAttribute.from`'s
|
|
174
|
+
* existing convention — the property name IS the dot-path, so a nested
|
|
175
|
+
* field is `diff.out["result.healthy"]`, not `diff.out.result.healthy`.
|
|
176
|
+
* `kind`, `step`, and `path` are reserved (the ref's own introspection
|
|
177
|
+
* fields); a return schema with a top-level field by one of those names
|
|
178
|
+
* needs `stepOutput(diff, "step")` instead.
|
|
179
|
+
*/
|
|
180
|
+
export function makeOutProxy(stepId: string): Record<string, StepOutputRef> {
|
|
181
|
+
const whole = makeStepOutputRef(stepId);
|
|
182
|
+
return new Proxy(whole as unknown as Record<string, StepOutputRef>, {
|
|
183
|
+
get(target, prop, receiver) {
|
|
184
|
+
if (typeof prop === "symbol" || prop === "kind" || prop === "step" || prop === "path" || prop === "toString" || prop === "valueOf") {
|
|
185
|
+
return Reflect.get(target, prop, receiver);
|
|
186
|
+
}
|
|
187
|
+
return makeStepOutputRef(stepId, prop);
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Every {@link StepOutputRef} found anywhere inside `value` (recursing through plain objects and arrays). */
|
|
193
|
+
export function collectStepOutputRefs(value: unknown): StepOutputRef[] {
|
|
194
|
+
if (isStepOutputRef(value)) return [value];
|
|
195
|
+
if (Array.isArray(value)) return value.flatMap(collectStepOutputRefs);
|
|
196
|
+
if (value && typeof value === "object") return Object.values(value).flatMap(collectStepOutputRefs);
|
|
197
|
+
return [];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* The property-key path from `value`'s root down to `target` (found by
|
|
202
|
+
* reference identity), or `undefined` if it isn't found or sits inside an
|
|
203
|
+
* array — an array index isn't a stable position to look up a declared arg
|
|
204
|
+
* schema against (chant #1950-3's cross-contract type check skips these,
|
|
205
|
+
* same "bail on anything fancier" policy as the rest of that check).
|
|
206
|
+
*/
|
|
207
|
+
function locateStepOutputRefPath(value: unknown, target: StepOutputRef, path: string[] = []): string[] | undefined {
|
|
208
|
+
if (value === target) return path;
|
|
209
|
+
if (Array.isArray(value)) return undefined;
|
|
210
|
+
if (value && typeof value === "object" && !isStepOutputRef(value)) {
|
|
211
|
+
for (const [k, v] of Object.entries(value)) {
|
|
212
|
+
const found = locateStepOutputRefPath(v, target, [...path, k]);
|
|
213
|
+
if (found) return found;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ── Validation ──────────────────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
interface StepLocation {
|
|
222
|
+
phaseIndex: number;
|
|
223
|
+
stepIndex: number;
|
|
224
|
+
parallel: boolean;
|
|
225
|
+
step: ActivityStep;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Top-level `ActivityStep`s of `phases`, in authored (phase, then step) order — gates, effects, and steps nested inside an effect are not producers or consumers (see the module doc's scope note). */
|
|
229
|
+
function locateActivitySteps(phases: PhaseDefinition[]): StepLocation[] {
|
|
230
|
+
const locations: StepLocation[] = [];
|
|
231
|
+
phases.forEach((phase, phaseIndex) => {
|
|
232
|
+
let stepIndex = 0;
|
|
233
|
+
for (const step of phase.steps) {
|
|
234
|
+
if (step.kind !== "activity") continue;
|
|
235
|
+
locations.push({ phaseIndex, stepIndex, parallel: phase.parallel === true, step });
|
|
236
|
+
stepIndex++;
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
return locations;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** True when `producer` is guaranteed to have completed before `consumer` starts. */
|
|
243
|
+
function producerPrecedes(producer: StepLocation, consumer: StepLocation): boolean {
|
|
244
|
+
if (producer.phaseIndex !== consumer.phaseIndex) return producer.phaseIndex < consumer.phaseIndex;
|
|
245
|
+
if (producer.parallel) return false; // same parallel phase — Promise.all, no ordering guarantee
|
|
246
|
+
return producer.stepIndex < consumer.stepIndex;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function effectNestedActivitySteps(phase: PhaseDefinition): ActivityStep[] {
|
|
250
|
+
return phase.steps.flatMap((s) => (s.kind === "effect" ? s.steps.filter((n) => n.kind === "activity") : []));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** `byId`/`duplicateIds` over an Op's top-level main-phase activity steps — shared by {@link validateStepOutputRefScope} and {@link validateStepOutputRefs}. */
|
|
254
|
+
function indexById(locations: StepLocation[]): { byId: Map<string, StepLocation>; duplicateIds: Set<string> } {
|
|
255
|
+
const byId = new Map<string, StepLocation>();
|
|
256
|
+
const duplicateIds = new Set<string>();
|
|
257
|
+
for (const loc of locations) {
|
|
258
|
+
if (!loc.step.id) continue;
|
|
259
|
+
if (byId.has(loc.step.id)) duplicateIds.add(loc.step.id);
|
|
260
|
+
else byId.set(loc.step.id, loc);
|
|
261
|
+
}
|
|
262
|
+
return { byId, duplicateIds };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Validate every step-output reference in an Op against scope and ordering
|
|
267
|
+
* alone — no contract needed. This is the subset of {@link
|
|
268
|
+
* validateStepOutputRefs}'s checks that don't depend on a contract map, kept
|
|
269
|
+
* as its own export so a caller with no contracts on hand (or that wants to
|
|
270
|
+
* defend against exactly this class of bug regardless of what contracts are
|
|
271
|
+
* registered) can still refuse a scope-invalid reference. The temporal
|
|
272
|
+
* lexicon's serializer (`serializeOps`) is exactly this caller: TMP013 (which
|
|
273
|
+
* calls the fuller {@link validateStepOutputRefs}) protects `chant build`,
|
|
274
|
+
* but `serializeOps` is a public export a caller can invoke directly,
|
|
275
|
+
* bypassing that lint pass — this is its own defense-in-depth.
|
|
276
|
+
*
|
|
277
|
+
* Flags:
|
|
278
|
+
* - a reference authored inside `onFailure`, or inside an `EffectStep`'s
|
|
279
|
+
* nested steps — out of scope by design (see the module doc); flagged
|
|
280
|
+
* explicitly here rather than left to fall through as "unknown producer",
|
|
281
|
+
* - a reference to an unregistered step id (no step in scope declares it),
|
|
282
|
+
* - a duplicate step id (ambiguous producer),
|
|
283
|
+
* - a reference to a step that does not precede the referencing step
|
|
284
|
+
* (a later phase, a later step in the same phase, itself, or a step in
|
|
285
|
+
* the same parallel phase).
|
|
286
|
+
*/
|
|
287
|
+
export function validateStepOutputRefScope(
|
|
288
|
+
config: Pick<OpConfig, "name" | "phases" | "onFailure">,
|
|
289
|
+
): ActivityContractIssue[] {
|
|
290
|
+
const issues: ActivityContractIssue[] = [];
|
|
291
|
+
|
|
292
|
+
const flagOutOfScope = (steps: ActivityStep[], phaseName: string, reason: string) => {
|
|
293
|
+
for (const step of steps) {
|
|
294
|
+
for (const ref of collectStepOutputRefs(step.args)) {
|
|
295
|
+
issues.push({
|
|
296
|
+
opName: config.name,
|
|
297
|
+
phase: phaseName,
|
|
298
|
+
fn: step.fn,
|
|
299
|
+
message: `references step "${ref.step}"'s output, but ${reason}`,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
for (const phase of config.onFailure ?? []) {
|
|
305
|
+
const steps = phase.steps.filter((s): s is ActivityStep => s.kind === "activity").concat(effectNestedActivitySteps(phase));
|
|
306
|
+
flagOutOfScope(steps, phase.name, "step-output references are not supported in onFailure compensation phases");
|
|
307
|
+
}
|
|
308
|
+
for (const phase of config.phases) {
|
|
309
|
+
flagOutOfScope(effectNestedActivitySteps(phase), phase.name, "step-output references are not supported for a step nested inside an effect step");
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const locations = locateActivitySteps(config.phases);
|
|
313
|
+
const { byId, duplicateIds } = indexById(locations);
|
|
314
|
+
const phaseNameOf = (loc: StepLocation): string => config.phases[loc.phaseIndex]!.name;
|
|
315
|
+
|
|
316
|
+
for (const consumer of locations) {
|
|
317
|
+
const refs = collectStepOutputRefs(consumer.step.args);
|
|
318
|
+
for (const ref of refs) {
|
|
319
|
+
if (duplicateIds.has(ref.step)) {
|
|
320
|
+
issues.push({
|
|
321
|
+
opName: config.name,
|
|
322
|
+
phase: phaseNameOf(consumer),
|
|
323
|
+
fn: consumer.step.fn,
|
|
324
|
+
message: `references step id "${ref.step}", but that id is used by more than one step in this Op — ids must be unique`,
|
|
325
|
+
});
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const producer = byId.get(ref.step);
|
|
330
|
+
if (!producer) {
|
|
331
|
+
issues.push({
|
|
332
|
+
opName: config.name,
|
|
333
|
+
phase: phaseNameOf(consumer),
|
|
334
|
+
fn: consumer.step.fn,
|
|
335
|
+
message: `references unknown step id "${ref.step}" — no step in this Op's main phases declares that id`,
|
|
336
|
+
});
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (!producerPrecedes(producer, consumer)) {
|
|
341
|
+
const reason =
|
|
342
|
+
producer.phaseIndex > consumer.phaseIndex
|
|
343
|
+
? `step "${ref.step}" is in a later phase`
|
|
344
|
+
: producer.parallel
|
|
345
|
+
? `step "${ref.step}" is in the same parallel phase (steps there run concurrently, with no ordering guarantee)`
|
|
346
|
+
: `step "${ref.step}" does not run before this step`;
|
|
347
|
+
issues.push({
|
|
348
|
+
opName: config.name,
|
|
349
|
+
phase: phaseNameOf(consumer),
|
|
350
|
+
fn: consumer.step.fn,
|
|
351
|
+
message: `references ${reason} — a step can only reference an earlier step's output`,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return issues;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Validate every step-output reference in an Op's main phases against a
|
|
362
|
+
* contract map and the Op's own step ordering. Scope: `config.phases` only
|
|
363
|
+
* (never `onFailure`, never a step nested inside an `EffectStep` — see the
|
|
364
|
+
* module doc). Layers contract-based checks on top of {@link
|
|
365
|
+
* validateStepOutputRefScope}'s scope/ordering checks.
|
|
366
|
+
*
|
|
367
|
+
* Flags everything {@link validateStepOutputRefScope} does, plus:
|
|
368
|
+
* - a reference into a step whose `fn` has no registered
|
|
369
|
+
* {@link ActivityContract}, or whose contract declares no `returns`
|
|
370
|
+
* schema — nothing to validate the reference against,
|
|
371
|
+
* - a `path` that does not resolve on the producer's declared return
|
|
372
|
+
* schema (an empty `path` — the whole return value — is always valid).
|
|
373
|
+
*/
|
|
374
|
+
export function validateStepOutputRefs(
|
|
375
|
+
config: Pick<OpConfig, "name" | "phases" | "onFailure">,
|
|
376
|
+
contracts: ReadonlyMap<string, ActivityContract>,
|
|
377
|
+
): ActivityContractIssue[] {
|
|
378
|
+
const issues = validateStepOutputRefScope(config);
|
|
379
|
+
|
|
380
|
+
const locations = locateActivitySteps(config.phases);
|
|
381
|
+
const { byId, duplicateIds } = indexById(locations);
|
|
382
|
+
const phaseNameOf = (loc: StepLocation): string => config.phases[loc.phaseIndex]!.name;
|
|
383
|
+
|
|
384
|
+
for (const consumer of locations) {
|
|
385
|
+
const refs = collectStepOutputRefs(consumer.step.args);
|
|
386
|
+
for (const ref of refs) {
|
|
387
|
+
// Already flagged by validateStepOutputRefScope — nothing to validate
|
|
388
|
+
// a contract-based check against.
|
|
389
|
+
if (duplicateIds.has(ref.step)) continue;
|
|
390
|
+
const producer = byId.get(ref.step);
|
|
391
|
+
if (!producer || !producerPrecedes(producer, consumer)) continue;
|
|
392
|
+
|
|
393
|
+
const producerContract = contracts.get(producer.step.fn);
|
|
394
|
+
if (!producerContract) {
|
|
395
|
+
issues.push({
|
|
396
|
+
opName: config.name,
|
|
397
|
+
phase: phaseNameOf(consumer),
|
|
398
|
+
fn: consumer.step.fn,
|
|
399
|
+
message: `references step "${ref.step}"'s output, but "${producer.step.fn}" has no registered activity contract to validate the reference against`,
|
|
400
|
+
});
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
if (!producerContract.returns) {
|
|
404
|
+
issues.push({
|
|
405
|
+
opName: config.name,
|
|
406
|
+
phase: phaseNameOf(consumer),
|
|
407
|
+
fn: consumer.step.fn,
|
|
408
|
+
message: `references step "${ref.step}"'s output, but "${producer.step.fn}"'s contract declares no return schema`,
|
|
409
|
+
});
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
if (ref.path && !pathExistsInSchema(producerContract.returns as z.ZodTypeAny, ref.path)) {
|
|
413
|
+
issues.push({
|
|
414
|
+
opName: config.name,
|
|
415
|
+
phase: phaseNameOf(consumer),
|
|
416
|
+
fn: consumer.step.fn,
|
|
417
|
+
message: `references step "${ref.step}"'s output path "${ref.path}", which does not exist on "${producer.step.fn}"'s declared return type`,
|
|
418
|
+
});
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Finding #1950-3: cross-contract primitive-type compatibility. Only
|
|
423
|
+
// meaningful for a field reference (`ref.path` set) against a
|
|
424
|
+
// registered consumer contract's arg schema at the same position in
|
|
425
|
+
// `args` — a whole-value reference (no path) is intentionally left
|
|
426
|
+
// unchecked (see the "whole-value reference... passes even without
|
|
427
|
+
// checking the producer's return shape" test/doc above). Bails
|
|
428
|
+
// silently (no issue) on anything not a plain string/number/boolean/
|
|
429
|
+
// object/array — a union, enum, literal, transform, `z.any()`, etc. —
|
|
430
|
+
// there's no cheap structural comparison for those.
|
|
431
|
+
if (ref.path) {
|
|
432
|
+
const consumerContract = contracts.get(consumer.step.fn);
|
|
433
|
+
const argPath = consumerContract && locateStepOutputRefPath(consumer.step.args, ref);
|
|
434
|
+
if (consumerContract && argPath) {
|
|
435
|
+
const consumerFieldSchema = schemaAtPath(consumerContract.args as z.ZodTypeAny, argPath);
|
|
436
|
+
const producerFieldSchema = schemaAtPath(producerContract.returns as z.ZodTypeAny, ref.path.split("."));
|
|
437
|
+
const consumerKind = consumerFieldSchema && primitiveKindOf(consumerFieldSchema);
|
|
438
|
+
const producerKind = producerFieldSchema && primitiveKindOf(producerFieldSchema);
|
|
439
|
+
if (consumerKind && producerKind && consumerKind !== producerKind) {
|
|
440
|
+
issues.push({
|
|
441
|
+
opName: config.name,
|
|
442
|
+
phase: phaseNameOf(consumer),
|
|
443
|
+
fn: consumer.step.fn,
|
|
444
|
+
message: `references step "${ref.step}"'s output path "${ref.path}" (${producerKind}), but arg "${argPath.join(".")}" on "${consumer.step.fn}" declares ${consumerKind} — type mismatch`,
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return issues;
|
|
453
|
+
}
|
package/src/op/types.ts
CHANGED
|
@@ -39,9 +39,26 @@ export type StepDefinition = ActivityStep | GateStep | EffectStep;
|
|
|
39
39
|
|
|
40
40
|
export interface ActivityStep {
|
|
41
41
|
kind: "activity";
|
|
42
|
+
/**
|
|
43
|
+
* Identifies this step so a later step can reference its output (#1290)
|
|
44
|
+
* via `stepOutput(id, path?)` or, when built with `activity()`, the
|
|
45
|
+
* `.out` proxy sugar. Only steps in an Op's main `phases` — not
|
|
46
|
+
* `onFailure`, not one nested inside an `EffectStep` — can be referenced.
|
|
47
|
+
*/
|
|
48
|
+
id?: string;
|
|
42
49
|
/** Name of the exported activity function in the pre-built activity library. */
|
|
43
50
|
fn: string;
|
|
44
|
-
/**
|
|
51
|
+
/**
|
|
52
|
+
* Arguments passed to the activity function. A value may be a
|
|
53
|
+
* {@link StepOutputRef} (anywhere in the structure, including nested
|
|
54
|
+
* inside a plain object or array) — a reference to an earlier step's
|
|
55
|
+
* declared return value, resolved at build time and compiled by the
|
|
56
|
+
* serializer into a local variable holding that step's result. Never an
|
|
57
|
+
* expression over one: `diff.out.count > 0` or a template literal
|
|
58
|
+
* coerces the reference to a primitive, which throws (see
|
|
59
|
+
* `step-output-ref.ts`'s module doc for why this is a runtime-on-load
|
|
60
|
+
* guard, not a static lint rule).
|
|
61
|
+
*/
|
|
45
62
|
args?: Record<string, unknown>;
|
|
46
63
|
/**
|
|
47
64
|
* Key from TEMPORAL_ACTIVITY_PROFILES controlling timeout + retry.
|
package/src/provenance.test.ts
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { describe, expect, test } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
describePathOrigin,
|
|
4
|
+
getPathProvenance,
|
|
5
|
+
getProvenance,
|
|
6
|
+
originOfPath,
|
|
7
|
+
setPathProvenance,
|
|
8
|
+
setProvenance,
|
|
9
|
+
type PathOrigin,
|
|
10
|
+
} from "./provenance";
|
|
11
|
+
import { Composite, expandComposite, propagate } from "./composite";
|
|
4
12
|
import { collectEntities } from "./discovery/collect";
|
|
5
13
|
import { DECLARABLE_MARKER, type Declarable } from "./declarable";
|
|
6
14
|
|
|
7
|
-
const decl = (entityType: string): Declarable =>
|
|
8
|
-
({ [DECLARABLE_MARKER]: true, lexicon: "test", entityType, kind: "resource", props
|
|
15
|
+
const decl = (entityType: string, props: Record<string, unknown> = {}): Declarable =>
|
|
16
|
+
({ [DECLARABLE_MARKER]: true, lexicon: "test", entityType, kind: "resource", props }) as unknown as Declarable;
|
|
9
17
|
|
|
10
18
|
const Pair = Composite<{ n: string }>((props) => ({
|
|
11
19
|
first: decl(`Test::First:${props.n}`),
|
|
@@ -77,3 +85,142 @@ describe("collectEntities stamps the source file", () => {
|
|
|
77
85
|
expect(prov?.composite).toBe("Pair");
|
|
78
86
|
});
|
|
79
87
|
});
|
|
88
|
+
|
|
89
|
+
describe("path provenance storage (#1443)", () => {
|
|
90
|
+
test("first writer wins per path, independently of other paths", () => {
|
|
91
|
+
const e = decl("Test::Thing");
|
|
92
|
+
setPathProvenance(e, "spec.replicas", { kind: "build-param", params: ["tier"] });
|
|
93
|
+
setPathProvenance(e, "spec.replicas", { kind: "authored" });
|
|
94
|
+
setPathProvenance(e, "", { kind: "authored" });
|
|
95
|
+
expect(getPathProvenance(e)).toEqual({
|
|
96
|
+
"": { kind: "authored" },
|
|
97
|
+
"spec.replicas": { kind: "build-param", params: ["tier"] },
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("keys come back sorted, whatever order they were written in", () => {
|
|
102
|
+
const e = decl("Test::Thing");
|
|
103
|
+
setPathProvenance(e, "spec.z", { kind: "authored" });
|
|
104
|
+
setPathProvenance(e, "metadata.a", { kind: "authored" });
|
|
105
|
+
setPathProvenance(e, "", { kind: "authored" });
|
|
106
|
+
expect(Object.keys(getPathProvenance(e)!)).toEqual(["", "metadata.a", "spec.z"]);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("path origins stay off the serialized entity", () => {
|
|
110
|
+
const e = decl("Test::Thing");
|
|
111
|
+
setPathProvenance(e, "spec.replicas", { kind: "authored" });
|
|
112
|
+
expect(JSON.stringify(e)).not.toContain("replicas");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("nothing recorded reads as undefined, not an empty record", () => {
|
|
116
|
+
expect(getPathProvenance(decl("Test::Thing"))).toBeUndefined();
|
|
117
|
+
expect(originOfPath(undefined, "spec.replicas")).toBeUndefined();
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe("originOfPath resolution", () => {
|
|
122
|
+
const paths: Record<string, PathOrigin> = {
|
|
123
|
+
"": { kind: "authored" },
|
|
124
|
+
spec: { kind: "composite", composite: "WebService", instance: "web" },
|
|
125
|
+
"spec.template.spec.containers": { kind: "build-param", params: ["image", "tier"] },
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
test("the longest matching prefix wins", () => {
|
|
129
|
+
expect(originOfPath(paths, "spec.template.spec.containers")).toEqual(paths["spec.template.spec.containers"]);
|
|
130
|
+
expect(originOfPath(paths, "spec.replicas")).toEqual(paths.spec);
|
|
131
|
+
expect(originOfPath(paths, "metadata.name")).toEqual(paths[""]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("a keyed list element inherits the origin recorded for the list (#1441 grammar)", () => {
|
|
135
|
+
expect(originOfPath(paths, "spec.template.spec.containers[#app].image")).toEqual(
|
|
136
|
+
paths["spec.template.spec.containers"],
|
|
137
|
+
);
|
|
138
|
+
expect(originOfPath(paths, "spec.template.spec.containers[0].image")).toEqual(
|
|
139
|
+
paths["spec.template.spec.containers"],
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("prefixes only match on a segment boundary", () => {
|
|
144
|
+
// `spec` must not claim `specialCase`, and a deeper recorded key must not
|
|
145
|
+
// claim a shallower query.
|
|
146
|
+
expect(originOfPath(paths, "specialCase")).toEqual(paths[""]);
|
|
147
|
+
expect(originOfPath({ "spec.containers.image": { kind: "authored" } }, "spec.containers")).toBeUndefined();
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe("describePathOrigin", () => {
|
|
152
|
+
test("renders each kind", () => {
|
|
153
|
+
expect(describePathOrigin({ kind: "authored" })).toBe("authored");
|
|
154
|
+
expect(describePathOrigin({ kind: "composite", composite: "WebService", instance: "web" })).toBe(
|
|
155
|
+
"composite WebService (web)",
|
|
156
|
+
);
|
|
157
|
+
expect(describePathOrigin({ kind: "build-param", params: ["region", "tier"] })).toBe("param region, tier");
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe("composite expansion records path origins (#1443)", () => {
|
|
162
|
+
test("each member's whole-entity origin names the composite and the instance", () => {
|
|
163
|
+
const expanded = expandComposite("web", Pair({ n: "x" }));
|
|
164
|
+
for (const [, entity] of expanded) {
|
|
165
|
+
expect(originOfPath(getPathProvenance(entity), "anything.at.all")).toEqual({
|
|
166
|
+
kind: "composite",
|
|
167
|
+
composite: "Pair",
|
|
168
|
+
instance: "web",
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("a nested member keeps the innermost composite but the outer instance", () => {
|
|
174
|
+
const Wrapper = Composite<{ n: string }>((props) => ({
|
|
175
|
+
inner: Pair({ n: props.n }) as unknown as Declarable,
|
|
176
|
+
}), "Wrapper");
|
|
177
|
+
const expanded = expandComposite("stack", Wrapper({ n: "y" }));
|
|
178
|
+
for (const [, entity] of expanded) {
|
|
179
|
+
expect(getPathProvenance(entity)?.[""]).toEqual({
|
|
180
|
+
kind: "composite",
|
|
181
|
+
composite: "Pair",
|
|
182
|
+
instance: "stack",
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("a propagated key the member never set is attributed to the instance", () => {
|
|
188
|
+
const Tagged = Composite<{ n: string }>(() => ({
|
|
189
|
+
only: decl("Test::Only", { name: "fixed" }),
|
|
190
|
+
}), "Tagged");
|
|
191
|
+
const expanded = expandComposite("env", propagate(Tagged({ n: "x" }), { tags: [{ key: "env", value: "prod" }] }));
|
|
192
|
+
const entity = expanded.get("envOnly")!;
|
|
193
|
+
expect(getPathProvenance(entity)?.tags).toEqual({
|
|
194
|
+
kind: "composite",
|
|
195
|
+
composite: "Tagged",
|
|
196
|
+
instance: "env",
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("a key both the member and the propagation wrote is left to the whole-entity origin", () => {
|
|
201
|
+
const Tagged = Composite<{ n: string }>(() => ({
|
|
202
|
+
only: decl("Test::Only", { tags: [{ key: "own", value: "1" }] }),
|
|
203
|
+
}), "Tagged");
|
|
204
|
+
const expanded = expandComposite("env", propagate(Tagged({ n: "x" }), { tags: [{ key: "env", value: "prod" }] }));
|
|
205
|
+
const paths = getPathProvenance(expanded.get("envOnly")!);
|
|
206
|
+
expect(paths?.tags).toBeUndefined();
|
|
207
|
+
expect(paths?.[""]).toBeDefined();
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe("collectEntities records the authored origin", () => {
|
|
212
|
+
test("a directly exported declarable is authored at the root", () => {
|
|
213
|
+
const a = decl("Test::A");
|
|
214
|
+
const entities = collectEntities([{ file: "/proj/src/infra.ts", exports: { a } }]);
|
|
215
|
+
expect(getPathProvenance(entities.get("a")!)?.[""]).toEqual({ kind: "authored" });
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("a composite member is not relabelled authored", () => {
|
|
219
|
+
const entities = collectEntities([{ file: "/proj/src/pipe.ts", exports: { p: Pair({ n: "z" }) } }]);
|
|
220
|
+
expect(getPathProvenance(entities.get("pFirst")!)?.[""]).toEqual({
|
|
221
|
+
kind: "composite",
|
|
222
|
+
composite: "Pair",
|
|
223
|
+
instance: "p",
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
});
|