@intentius/chant 0.46.0 → 0.49.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/core.d.ts +21 -3
- package/dist/audit/core.d.ts.map +1 -1
- package/dist/audit/discover.d.ts +3 -2
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/audit/rules-doc.d.ts.map +1 -1
- package/dist/build.d.ts +3 -3
- package/dist/build.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts +1 -1
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/mcp/resource-handlers.d.ts.map +1 -1
- package/dist/cli/mcp/tools/explain.d.ts +6 -0
- package/dist/cli/mcp/tools/explain.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts +1 -1
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/reporters/stylish.d.ts +15 -1
- package/dist/cli/reporters/stylish.d.ts.map +1 -1
- package/dist/components/auto-release.d.ts +4 -0
- package/dist/components/auto-release.d.ts.map +1 -1
- package/dist/components/starter-plugin.d.ts +2 -0
- package/dist/components/starter-plugin.d.ts.map +1 -1
- package/dist/components/verbs/ensure-secret.d.ts +50 -0
- package/dist/components/verbs/ensure-secret.d.ts.map +1 -0
- package/dist/components/verbs/index.d.ts +8 -0
- package/dist/components/verbs/index.d.ts.map +1 -1
- package/dist/components/verbs/r2-sync.d.ts +76 -0
- package/dist/components/verbs/r2-sync.d.ts.map +1 -0
- package/dist/components/verbs/wrangler.d.ts +108 -0
- package/dist/components/verbs/wrangler.d.ts.map +1 -0
- package/dist/config.d.ts +26 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/deep-observation.d.ts +14 -0
- package/dist/deep-observation.d.ts.map +1 -1
- package/dist/effect-receipt.d.ts +177 -0
- package/dist/effect-receipt.d.ts.map +1 -0
- package/dist/fold/subset.d.ts +15 -2
- package/dist/fold/subset.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/lexicon.d.ts +44 -3
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/change-set.d.ts +33 -5
- package/dist/lifecycle/change-set.d.ts.map +1 -1
- package/dist/lifecycle/index.d.ts +2 -0
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/observation-baseline.d.ts +21 -3
- package/dist/lifecycle/observation-baseline.d.ts.map +1 -1
- package/dist/lifecycle/receipt-plan.d.ts +62 -0
- package/dist/lifecycle/receipt-plan.d.ts.map +1 -0
- package/dist/lifecycle/release-ledger.d.ts +20 -0
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lifecycle/teardown.d.ts +6 -4
- package/dist/lifecycle/teardown.d.ts.map +1 -1
- package/dist/lifecycle/unobserved-gate.d.ts +67 -0
- package/dist/lifecycle/unobserved-gate.d.ts.map +1 -0
- package/dist/lint/knowledge-checks.d.ts +48 -0
- package/dist/lint/knowledge-checks.d.ts.map +1 -0
- package/dist/lint/output-checks.d.ts +5 -0
- package/dist/lint/output-checks.d.ts.map +1 -0
- package/dist/lint/pipeline-change-gate.d.ts +101 -0
- package/dist/lint/pipeline-change-gate.d.ts.map +1 -0
- package/dist/lint/post-synth.d.ts +12 -0
- package/dist/lint/post-synth.d.ts.map +1 -1
- package/dist/lint/receipt-checks.d.ts +9 -0
- package/dist/lint/receipt-checks.d.ts.map +1 -0
- package/dist/lint/rules/cor022-receipt-leaf.d.ts +13 -0
- package/dist/lint/rules/cor022-receipt-leaf.d.ts.map +1 -0
- package/dist/lint/rules/cor024-receipt-secret-pointer.d.ts +3 -0
- package/dist/lint/rules/cor024-receipt-secret-pointer.d.ts.map +1 -0
- package/dist/lint/rules/evl001-non-literal-expression.d.ts.map +1 -1
- package/dist/lint/rules/index.d.ts +3 -1
- package/dist/lint/rules/index.d.ts.map +1 -1
- package/dist/okf-read.d.ts +78 -0
- package/dist/okf-read.d.ts.map +1 -0
- package/dist/op/builders.d.ts +98 -1
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/index.d.ts +4 -2
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/local-executor.d.ts +2 -1
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/receipt-store.d.ts +138 -0
- package/dist/op/receipt-store.d.ts.map +1 -0
- package/dist/op/types.d.ts +31 -1
- package/dist/op/types.d.ts.map +1 -1
- package/dist/secret-materialization.d.ts +138 -0
- package/dist/secret-materialization.d.ts.map +1 -0
- package/dist/secret-provenance.d.ts +218 -0
- package/dist/secret-provenance.d.ts.map +1 -0
- package/dist/serializer.d.ts +11 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/yaml.d.ts.map +1 -1
- package/package.json +4 -1
- package/src/audit/core.test.ts +57 -0
- package/src/audit/core.ts +0 -0
- package/src/audit/detect-bundle.test.ts +1 -1
- package/src/audit/discover.test.ts +24 -0
- package/src/audit/discover.ts +11 -2
- package/src/audit/rules-doc.ts +11 -1
- package/src/build.test.ts +41 -0
- package/src/build.ts +34 -6
- package/src/cli/commands/__fixtures__/audit-fountain/agents/fleet.yaml +27 -0
- package/src/cli/commands/__fixtures__/audit-fountain/k8s/deploy.yaml +16 -0
- package/src/cli/commands/__fixtures__/audit-fountain-clean/fleet.yaml +20 -0
- package/src/cli/commands/audit.test.ts +53 -0
- package/src/cli/commands/audit.ts +1 -1
- package/src/cli/commands/build.test.ts +80 -0
- package/src/cli/commands/build.ts +106 -8
- package/src/cli/commands/lint.ts +15 -3
- package/src/cli/handlers/explain.test.ts +70 -1
- package/src/cli/handlers/graph.ts +2 -2
- package/src/cli/handlers/lifecycle.test.ts +115 -1
- package/src/cli/handlers/lifecycle.ts +84 -11
- package/src/cli/mcp/resource-handlers.ts +38 -1
- package/src/cli/mcp/server.test.ts +58 -1
- package/src/cli/mcp/tools/explain.ts +51 -2
- package/src/cli/plugins.ts +4 -2
- package/src/cli/reporters/stylish.test.ts +154 -0
- package/src/cli/reporters/stylish.ts +154 -33
- package/src/components/auto-release.ts +6 -0
- package/src/components/registry.test.ts +7 -2
- package/src/components/starter-plugin.ts +17 -0
- package/src/components/verbs/ensure-secret.test.ts +130 -0
- package/src/components/verbs/ensure-secret.ts +79 -0
- package/src/components/verbs/index.ts +8 -0
- package/src/components/verbs/r2-sync.test.ts +107 -0
- package/src/components/verbs/r2-sync.ts +124 -0
- package/src/components/verbs/wrangler.test.ts +170 -0
- package/src/components/verbs/wrangler.ts +241 -0
- package/src/config.test.ts +15 -0
- package/src/config.ts +30 -0
- package/src/deep-observation.test.ts +19 -0
- package/src/deep-observation.ts +17 -0
- package/src/effect-receipt-exclusion.test.ts +190 -0
- package/src/effect-receipt.test.ts +419 -0
- package/src/effect-receipt.ts +412 -0
- package/src/fold/subset.test.ts +26 -0
- package/src/fold/subset.ts +45 -19
- package/src/index.ts +4 -0
- package/src/lexicon.ts +48 -3
- package/src/lifecycle/change-set.ts +46 -7
- package/src/lifecycle/index.ts +2 -0
- package/src/lifecycle/observation-baseline.test.ts +46 -0
- package/src/lifecycle/observation-baseline.ts +33 -1
- package/src/lifecycle/receipt-plan.test.ts +250 -0
- package/src/lifecycle/receipt-plan.ts +249 -0
- package/src/lifecycle/release-ledger.ts +20 -0
- package/src/lifecycle/teardown.test.ts +31 -0
- package/src/lifecycle/teardown.ts +6 -4
- package/src/lifecycle/unobserved-gate.test.ts +109 -0
- package/src/lifecycle/unobserved-gate.ts +102 -0
- package/src/lint/knowledge-checks.test.ts +80 -0
- package/src/lint/knowledge-checks.ts +74 -0
- package/src/lint/output-checks.test.ts +85 -0
- package/src/lint/output-checks.ts +99 -0
- package/src/lint/pipeline-change-gate.test.ts +144 -0
- package/src/lint/pipeline-change-gate.ts +153 -0
- package/src/lint/post-synth.ts +15 -0
- package/src/lint/receipt-checks.test.ts +101 -0
- package/src/lint/receipt-checks.ts +93 -0
- package/src/lint/rules/cor022-receipt-leaf.test.ts +116 -0
- package/src/lint/rules/cor022-receipt-leaf.ts +130 -0
- package/src/lint/rules/cor024-receipt-secret-pointer.test.ts +121 -0
- package/src/lint/rules/cor024-receipt-secret-pointer.ts +218 -0
- package/src/lint/rules/evl001-non-literal-expression.test.ts +27 -0
- package/src/lint/rules/evl001-non-literal-expression.ts +8 -1
- package/src/lint/rules/index.ts +7 -1
- package/src/okf-read.test.ts +149 -0
- package/src/okf-read.ts +197 -0
- package/src/op/builders.ts +139 -1
- package/src/op/effect-step.test.ts +311 -0
- package/src/op/index.ts +10 -3
- package/src/op/local-executor.ts +172 -25
- package/src/op/op.test.ts +25 -2
- package/src/op/receipt-store.ts +211 -0
- package/src/op/types.ts +33 -1
- package/src/secret-materialization.test.ts +199 -0
- package/src/secret-materialization.ts +235 -0
- package/src/secret-provenance.test.ts +388 -0
- package/src/secret-provenance.ts +475 -0
- package/src/serializer.ts +12 -0
- package/src/yaml.test.ts +88 -0
- package/src/yaml.ts +76 -6
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Effect receipts (chant #1831, epic #1703).
|
|
3
|
+
*
|
|
4
|
+
* A RECEIPT is the declared witness that an out-of-band effect (a migration,
|
|
5
|
+
* a seed job, a one-shot bootstrap) has run — declared, diffed, and observed
|
|
6
|
+
* like any resource, but observe-only to the generic apply path: the
|
|
7
|
+
* `effect()` step (#1834) is the sole writer, on success, last. Anything
|
|
8
|
+
* else silently converts at-least-once into never (epic decision log, item
|
|
9
|
+
* 3). That write-exclusion is #1832's enforcement; what THIS module provides
|
|
10
|
+
* is the recognition marker that makes it possible: core code (lint, plan,
|
|
11
|
+
* apply) identifies receipts via {@link EFFECT_RECEIPT_MARKER} without any
|
|
12
|
+
* lexicon knowledge, while per-lexicon rows (#1835's `AWS::SSM::Parameter`)
|
|
13
|
+
* materialize them as real resources. Unlike a secret provenance declaration
|
|
14
|
+
* (./secret-provenance.ts), a receipt DOES serialize — the marker identifies,
|
|
15
|
+
* it does not exclude.
|
|
16
|
+
*
|
|
17
|
+
* The resolution split (epic decision log, item 5):
|
|
18
|
+
*
|
|
19
|
+
* - STATIC inputs hash at synthesis, in the serializer — a fully static
|
|
20
|
+
* receipt's expectation is already known when the template is written
|
|
21
|
+
* ({@link receiptExpectation}).
|
|
22
|
+
* - REFERENCE inputs (attr-refs and other intrinsics — deploy-time values)
|
|
23
|
+
* are recorded in placeholder form at synthesis and resolve in the plan
|
|
24
|
+
* engine (#1832) and again in the effect step (#1834), via
|
|
25
|
+
* {@link resolveReceiptExpectation}. Synthesis resolves NOTHING:
|
|
26
|
+
* {@link receiptExpectation} refuses a hash-flavor receipt that still
|
|
27
|
+
* carries references rather than hashing a placeholder.
|
|
28
|
+
* - Build-time `params.*` are not references here: they fold to literals
|
|
29
|
+
* before discovery ever runs (../params.ts), so by the time this factory
|
|
30
|
+
* sees them they are static.
|
|
31
|
+
*
|
|
32
|
+
* Nothing in this module reads live state. Every function is pure over the
|
|
33
|
+
* declaration and, for resolution, the caller-supplied resolver.
|
|
34
|
+
*
|
|
35
|
+
* Hashing is JCS-style canonical JSON (RFC 8785 shape: sorted keys, standard
|
|
36
|
+
* ECMAScript number/string encoding — implemented minimally here, no
|
|
37
|
+
* dependency) digested with sha256 ({@link canonicalJson},
|
|
38
|
+
* `sha256:<hex>` like the build-ledger digests in ./lifecycle/build-ledger.ts).
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import { createHash } from "node:crypto";
|
|
42
|
+
import { DECLARABLE_MARKER, type Declarable } from "./declarable";
|
|
43
|
+
import { isIntrinsic, type Intrinsic } from "./intrinsic";
|
|
44
|
+
|
|
45
|
+
/** The closed union of receipt flavors. */
|
|
46
|
+
export type EffectReceiptFlavor = "existence" | "hash";
|
|
47
|
+
|
|
48
|
+
/** Every receipt flavor, for exhaustiveness checks. */
|
|
49
|
+
export const EFFECT_RECEIPT_FLAVORS: readonly EffectReceiptFlavor[] = ["existence", "hash"];
|
|
50
|
+
|
|
51
|
+
/** Marker symbol identifying an effect receipt. `Symbol.for` so it survives
|
|
52
|
+
* the entity-wire codec (./discovery/entity-wire-codec.ts) and so a lexicon
|
|
53
|
+
* row can stamp the SAME symbol on its materialized resource — core's lint
|
|
54
|
+
* (#1833), plan (#1832), and apply write-exclusion recognize receipts through
|
|
55
|
+
* this marker alone, lexicon-independently. */
|
|
56
|
+
export const EFFECT_RECEIPT_MARKER = Symbol.for("chant.effect-receipt");
|
|
57
|
+
|
|
58
|
+
/** The `entityType` the core factory stamps. Lexicon materialization rows
|
|
59
|
+
* (#1835) use their own entityType and carry the marker instead. */
|
|
60
|
+
export const EFFECT_RECEIPT_ENTITY_TYPE = "Chant::EffectReceipt";
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The expected stored value of an `existence`-flavor receipt: a fixed marker
|
|
64
|
+
* constant, the same for every existence receipt. Present-and-equal means the
|
|
65
|
+
* effect has run; anything else renders an effect-will-fire row (#1832).
|
|
66
|
+
*/
|
|
67
|
+
export const EXISTENCE_EXPECTATION = "chant.effect-receipt:exists";
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* An effect receipt declaration — a Declarable, so discovery collects it,
|
|
71
|
+
* `chant list` shows it, and (unlike a secret declaration) serialization
|
|
72
|
+
* keeps it: a lexicon row turns it into a real, observable resource.
|
|
73
|
+
*/
|
|
74
|
+
export interface EffectReceiptDeclaration extends Declarable {
|
|
75
|
+
readonly [EFFECT_RECEIPT_MARKER]: true;
|
|
76
|
+
/** `"chant"` for the plain factory; a lexicon row (#1835) declares its own. */
|
|
77
|
+
readonly lexicon: string;
|
|
78
|
+
/** The core factory stamps EFFECT_RECEIPT_ENTITY_TYPE; a lexicon row uses its materialized kind. */
|
|
79
|
+
readonly entityType: string;
|
|
80
|
+
/** The receipt's own name (the export-level identity of the witness). */
|
|
81
|
+
readonly name: string;
|
|
82
|
+
/** The effect this receipt witnesses — the identity the `effect()` step
|
|
83
|
+
* (#1834) and the receipt path (#1835) key on. */
|
|
84
|
+
readonly effect: string;
|
|
85
|
+
/** How the receipt is compared: mere presence, or a digest of the inputs. */
|
|
86
|
+
readonly flavor: EffectReceiptFlavor;
|
|
87
|
+
/**
|
|
88
|
+
* The effect's inputs as recorded at synthesis: static values verbatim,
|
|
89
|
+
* references (intrinsics) kept in placeholder form — never resolved here.
|
|
90
|
+
*/
|
|
91
|
+
readonly inputs: Readonly<Record<string, unknown>>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Factory options for {@link EffectReceipt}. */
|
|
95
|
+
export interface EffectReceiptOptions {
|
|
96
|
+
/** The effect this receipt witnesses. Non-empty. */
|
|
97
|
+
readonly effect: string;
|
|
98
|
+
/** `existence` — presence is the witness; `hash` — a digest of the inputs
|
|
99
|
+
* is, so changed inputs re-propose the fire. */
|
|
100
|
+
readonly flavor: EffectReceiptFlavor;
|
|
101
|
+
/** The effect's inputs. Static values hash at synthesis; intrinsic values
|
|
102
|
+
* (attr-refs, deploy-time references) resolve at plan and at run. */
|
|
103
|
+
readonly inputs?: Record<string, unknown>;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Declare an effect receipt. The returned object is a locked Declarable:
|
|
108
|
+
* declared fields are immutable, plain-data input structures are frozen
|
|
109
|
+
* (intrinsics are left live — discovery still stamps logical names onto
|
|
110
|
+
* attr-refs), and the top-level object stays extensible for discovery's own
|
|
111
|
+
* symbol-keyed metadata.
|
|
112
|
+
*/
|
|
113
|
+
export function EffectReceipt(name: string, options: EffectReceiptOptions): EffectReceiptDeclaration {
|
|
114
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
115
|
+
throw new Error("EffectReceipt: `name` must be a non-empty string");
|
|
116
|
+
}
|
|
117
|
+
if (typeof options?.effect !== "string" || options.effect.length === 0) {
|
|
118
|
+
throw new Error(`EffectReceipt("${name}"): \`effect\` must be a non-empty string`);
|
|
119
|
+
}
|
|
120
|
+
if (!EFFECT_RECEIPT_FLAVORS.includes(options.flavor)) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`EffectReceipt("${name}"): unknown flavor "${String(options.flavor)}" — ` +
|
|
123
|
+
`expected one of ${EFFECT_RECEIPT_FLAVORS.join(", ")}`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
if (options.inputs !== undefined && (typeof options.inputs !== "object" || options.inputs === null || Array.isArray(options.inputs))) {
|
|
127
|
+
throw new Error(`EffectReceipt("${name}"): \`inputs\` must be a plain object when present`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const inputs = deepFreezeStatic({ ...(options.inputs ?? {}) }) as Readonly<Record<string, unknown>>;
|
|
131
|
+
|
|
132
|
+
const decl: EffectReceiptDeclaration = {
|
|
133
|
+
[DECLARABLE_MARKER]: true,
|
|
134
|
+
[EFFECT_RECEIPT_MARKER]: true,
|
|
135
|
+
lexicon: "chant",
|
|
136
|
+
entityType: EFFECT_RECEIPT_ENTITY_TYPE,
|
|
137
|
+
name,
|
|
138
|
+
effect: options.effect,
|
|
139
|
+
flavor: options.flavor,
|
|
140
|
+
inputs,
|
|
141
|
+
};
|
|
142
|
+
// Declared fields immutable, object extensible — same shape secret
|
|
143
|
+
// declarations use (./secret-provenance.ts's lockDeclaredFields).
|
|
144
|
+
for (const key of Object.keys(decl)) {
|
|
145
|
+
Object.defineProperty(decl, key, { writable: false, configurable: false });
|
|
146
|
+
}
|
|
147
|
+
return decl;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Freeze plain objects and arrays in place, leaving intrinsics (and any
|
|
151
|
+
* other class instance) untouched — discovery mutates AttrRefs when it
|
|
152
|
+
* assigns logical names. */
|
|
153
|
+
function deepFreezeStatic(value: unknown, seen: Set<object> = new Set()): unknown {
|
|
154
|
+
if (typeof value !== "object" || value === null || isIntrinsic(value)) return value;
|
|
155
|
+
if (seen.has(value)) return value;
|
|
156
|
+
seen.add(value);
|
|
157
|
+
if (Array.isArray(value)) {
|
|
158
|
+
for (const el of value) deepFreezeStatic(el, seen);
|
|
159
|
+
return Object.freeze(value);
|
|
160
|
+
}
|
|
161
|
+
if (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null) {
|
|
162
|
+
for (const el of Object.values(value)) deepFreezeStatic(el, seen);
|
|
163
|
+
return Object.freeze(value);
|
|
164
|
+
}
|
|
165
|
+
return value;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Type guard for an effect receipt — the recognition read core's guards use.
|
|
169
|
+
* True for the core declaration AND for any lexicon-materialized resource
|
|
170
|
+
* that carries the marker. */
|
|
171
|
+
export function isEffectReceipt(value: unknown): value is EffectReceiptDeclaration {
|
|
172
|
+
return (
|
|
173
|
+
typeof value === "object" &&
|
|
174
|
+
value !== null &&
|
|
175
|
+
EFFECT_RECEIPT_MARKER in value &&
|
|
176
|
+
(value as Record<symbol, unknown>)[EFFECT_RECEIPT_MARKER] === true
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Extract the effect receipts from a discovered entity map — the read surface
|
|
182
|
+
* for lint (#1833), the plan engine (#1832), and the apply write-exclusion.
|
|
183
|
+
* Keyed by entity name (export name), the same key `DiscoveryResult.entities`
|
|
184
|
+
* uses.
|
|
185
|
+
*/
|
|
186
|
+
export function collectEffectReceipts(
|
|
187
|
+
entities: ReadonlyMap<string, Declarable>,
|
|
188
|
+
): Map<string, EffectReceiptDeclaration> {
|
|
189
|
+
const out = new Map<string, EffectReceiptDeclaration>();
|
|
190
|
+
for (const [name, entity] of entities) {
|
|
191
|
+
if (isEffectReceipt(entity)) out.set(name, entity);
|
|
192
|
+
}
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Split an entity map into the apply-bound set and the receipts (#1832).
|
|
198
|
+
*
|
|
199
|
+
* The write-exclusion seam: receipts are observe-only to the generic apply
|
|
200
|
+
* path — the `effect()` step is the sole writer (epic #1703, decision 3), and
|
|
201
|
+
* a receipt the generic apply stamped would silently convert at-least-once
|
|
202
|
+
* into never. The build calls this at serializer-input assembly, the one core
|
|
203
|
+
* choke point every applier's input flows through (appliers consume serialized
|
|
204
|
+
* build outputs), so no lexicon's serialized apply document ever contains a
|
|
205
|
+
* receipt and no applier's desired or prune set can. Receipts still reach the
|
|
206
|
+
* serializer — for visibility rendering outside the apply-bound document
|
|
207
|
+
* (#1835) — via `SerializeContext.receipts`, never in the entity map.
|
|
208
|
+
*/
|
|
209
|
+
export function splitReceiptEntities(entities: ReadonlyMap<string, Declarable>): {
|
|
210
|
+
applyBound: Map<string, Declarable>;
|
|
211
|
+
receipts: Map<string, EffectReceiptDeclaration>;
|
|
212
|
+
} {
|
|
213
|
+
const applyBound = new Map<string, Declarable>();
|
|
214
|
+
const receipts = new Map<string, EffectReceiptDeclaration>();
|
|
215
|
+
for (const [name, entity] of entities) {
|
|
216
|
+
if (isEffectReceipt(entity)) receipts.set(name, entity);
|
|
217
|
+
else applyBound.set(name, entity);
|
|
218
|
+
}
|
|
219
|
+
return { applyBound, receipts };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
223
|
+
// Canonical hashing — JCS-style canonical JSON + sha256.
|
|
224
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* JCS-style canonical JSON (the RFC 8785 shape, implemented minimally):
|
|
228
|
+
* object keys sorted by UTF-16 code units, numbers and strings in standard
|
|
229
|
+
* ECMAScript `JSON.stringify` encoding, no insignificant whitespace.
|
|
230
|
+
* `toJSON()` is honored the way `JSON.stringify` honors it — which is what
|
|
231
|
+
* puts an intrinsic's PLACEHOLDER envelope (e.g. an attr-ref's
|
|
232
|
+
* `{"__attrRef":{...}}`) into the canonical form rather than a resolved
|
|
233
|
+
* value. Non-representable values (undefined outside an object property,
|
|
234
|
+
* functions, symbols, bigints, non-finite numbers, cycles) throw — a hash
|
|
235
|
+
* input silently coerced is a wrong expectation.
|
|
236
|
+
*/
|
|
237
|
+
export function canonicalJson(value: unknown): string {
|
|
238
|
+
const out = encodeCanonical(value, "$", new Set());
|
|
239
|
+
if (out === undefined) {
|
|
240
|
+
throw new Error(`canonicalJson: value at $ is not representable in JSON`);
|
|
241
|
+
}
|
|
242
|
+
return out;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Returns undefined only for values JSON.stringify would drop as an object
|
|
246
|
+
* property (undefined); throws for everything else non-representable. */
|
|
247
|
+
function encodeCanonical(value: unknown, path: string, seen: Set<object>): string | undefined {
|
|
248
|
+
// toJSON first, like JSON.stringify — once per node.
|
|
249
|
+
if (value !== null && (typeof value === "object" || typeof value === "function")) {
|
|
250
|
+
const toJSON = (value as { toJSON?: unknown }).toJSON;
|
|
251
|
+
if (typeof toJSON === "function") {
|
|
252
|
+
value = (toJSON as () => unknown).call(value);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (value === undefined) return undefined;
|
|
256
|
+
if (value === null) return "null";
|
|
257
|
+
switch (typeof value) {
|
|
258
|
+
case "boolean":
|
|
259
|
+
return value ? "true" : "false";
|
|
260
|
+
case "number":
|
|
261
|
+
if (!Number.isFinite(value)) {
|
|
262
|
+
throw new Error(`canonicalJson: non-finite number at ${path}`);
|
|
263
|
+
}
|
|
264
|
+
return String(value); // ECMAScript ToString — what JCS specifies.
|
|
265
|
+
case "string":
|
|
266
|
+
return JSON.stringify(value);
|
|
267
|
+
case "bigint":
|
|
268
|
+
throw new Error(`canonicalJson: bigint at ${path} is not representable in JSON`);
|
|
269
|
+
case "function":
|
|
270
|
+
case "symbol":
|
|
271
|
+
throw new Error(`canonicalJson: ${typeof value} at ${path} is not representable in JSON`);
|
|
272
|
+
}
|
|
273
|
+
const obj = value as object;
|
|
274
|
+
if (seen.has(obj)) {
|
|
275
|
+
throw new Error(`canonicalJson: circular structure at ${path}`);
|
|
276
|
+
}
|
|
277
|
+
seen.add(obj);
|
|
278
|
+
try {
|
|
279
|
+
if (Array.isArray(obj)) {
|
|
280
|
+
const parts = obj.map((el, i) => encodeCanonical(el, `${path}[${i}]`, seen) ?? "null");
|
|
281
|
+
return `[${parts.join(",")}]`;
|
|
282
|
+
}
|
|
283
|
+
const keys = Object.keys(obj).sort();
|
|
284
|
+
const parts: string[] = [];
|
|
285
|
+
for (const key of keys) {
|
|
286
|
+
const encoded = encodeCanonical((obj as Record<string, unknown>)[key], `${path}.${key}`, seen);
|
|
287
|
+
if (encoded !== undefined) parts.push(`${JSON.stringify(key)}:${encoded}`);
|
|
288
|
+
}
|
|
289
|
+
return `{${parts.join(",")}}`;
|
|
290
|
+
} finally {
|
|
291
|
+
seen.delete(obj);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function sha256Digest(canonical: string): string {
|
|
296
|
+
return `sha256:${createHash("sha256").update(canonical, "utf8").digest("hex")}`;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
300
|
+
// The resolution split.
|
|
301
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
302
|
+
|
|
303
|
+
/** The paths (dot/bracket, rooted at `inputs`) of every reference (intrinsic)
|
|
304
|
+
* input still unresolved on the receipt. Empty means the receipt is fully
|
|
305
|
+
* static and {@link receiptExpectation} can stamp its digest at synthesis. */
|
|
306
|
+
export function referenceInputPaths(receipt: EffectReceiptDeclaration): string[] {
|
|
307
|
+
const paths: string[] = [];
|
|
308
|
+
collectReferencePaths(receipt.inputs, "inputs", paths, new Set());
|
|
309
|
+
return paths;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function collectReferencePaths(value: unknown, path: string, out: string[], seen: Set<object>): void {
|
|
313
|
+
if (typeof value !== "object" || value === null) return;
|
|
314
|
+
if (isIntrinsic(value)) {
|
|
315
|
+
out.push(path);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
if (seen.has(value)) return;
|
|
319
|
+
seen.add(value);
|
|
320
|
+
if (Array.isArray(value)) {
|
|
321
|
+
value.forEach((el, i) => collectReferencePaths(el, `${path}[${i}]`, out, seen));
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
for (const [key, el] of Object.entries(value)) {
|
|
325
|
+
collectReferencePaths(el, `${path}.${key}`, out, seen);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* The receipt's expected stored value, computable at synthesis:
|
|
331
|
+
*
|
|
332
|
+
* - `existence` → {@link EXISTENCE_EXPECTATION}, always.
|
|
333
|
+
* - `hash` → `sha256:<hex>` over the canonical JSON of
|
|
334
|
+
* `{ effect, inputs }` — the effect name is bound into the digest so the
|
|
335
|
+
* same inputs under a different effect never collide.
|
|
336
|
+
*
|
|
337
|
+
* A hash-flavor receipt that still carries reference inputs THROWS instead
|
|
338
|
+
* of hashing placeholders: synthesis resolves nothing (epic decision log,
|
|
339
|
+
* item 5). The plan engine and the effect step get the digest through
|
|
340
|
+
* {@link resolveReceiptExpectation}.
|
|
341
|
+
*/
|
|
342
|
+
export function receiptExpectation(receipt: EffectReceiptDeclaration): string {
|
|
343
|
+
if (receipt.flavor === "existence") return EXISTENCE_EXPECTATION;
|
|
344
|
+
const refs = referenceInputPaths(receipt);
|
|
345
|
+
if (refs.length > 0) {
|
|
346
|
+
throw new Error(
|
|
347
|
+
`receiptExpectation("${receipt.name}"): unresolved reference inputs at ${refs.join(", ")} — ` +
|
|
348
|
+
`references resolve at plan and at run, never at synthesis; ` +
|
|
349
|
+
`use resolveReceiptExpectation with a resolver`,
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
return sha256Digest(canonicalJson({ effect: receipt.effect, inputs: receipt.inputs }));
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Resolves one reference input to its live value. `path` is the reference's
|
|
357
|
+
* location (as {@link referenceInputPaths} renders it). Must return a
|
|
358
|
+
* JSON-representable value — returning `undefined` or another intrinsic is
|
|
359
|
+
* an error, reported with the path.
|
|
360
|
+
*/
|
|
361
|
+
export type ReceiptInputResolver = (ref: Intrinsic, path: string) => unknown;
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* The receipt's expected stored value with references resolved — the form
|
|
365
|
+
* the plan engine (#1832) compares against the live receipt and the effect
|
|
366
|
+
* step (#1834) writes on success. Deterministic over the receipt and the
|
|
367
|
+
* resolver's answers; resolves nothing itself and reads no live state (the
|
|
368
|
+
* resolver is the caller's seam to deploy-time values).
|
|
369
|
+
*/
|
|
370
|
+
export function resolveReceiptExpectation(
|
|
371
|
+
receipt: EffectReceiptDeclaration,
|
|
372
|
+
resolver: ReceiptInputResolver,
|
|
373
|
+
): string {
|
|
374
|
+
if (receipt.flavor === "existence") return EXISTENCE_EXPECTATION;
|
|
375
|
+
const resolved = resolveValue(receipt.inputs, "inputs", resolver, new Set());
|
|
376
|
+
return sha256Digest(canonicalJson({ effect: receipt.effect, inputs: resolved }));
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function resolveValue(
|
|
380
|
+
value: unknown,
|
|
381
|
+
path: string,
|
|
382
|
+
resolver: ReceiptInputResolver,
|
|
383
|
+
seen: Set<object>,
|
|
384
|
+
): unknown {
|
|
385
|
+
if (typeof value !== "object" || value === null) return value;
|
|
386
|
+
if (isIntrinsic(value)) {
|
|
387
|
+
const resolved = resolver(value, path);
|
|
388
|
+
if (resolved === undefined) {
|
|
389
|
+
throw new Error(`resolveReceiptExpectation: resolver returned undefined for reference at ${path}`);
|
|
390
|
+
}
|
|
391
|
+
if (isIntrinsic(resolved)) {
|
|
392
|
+
throw new Error(`resolveReceiptExpectation: resolver returned another reference for ${path}`);
|
|
393
|
+
}
|
|
394
|
+
return resolved;
|
|
395
|
+
}
|
|
396
|
+
if (seen.has(value)) {
|
|
397
|
+
throw new Error(`resolveReceiptExpectation: circular structure at ${path}`);
|
|
398
|
+
}
|
|
399
|
+
seen.add(value);
|
|
400
|
+
try {
|
|
401
|
+
if (Array.isArray(value)) {
|
|
402
|
+
return value.map((el, i) => resolveValue(el, `${path}[${i}]`, resolver, seen));
|
|
403
|
+
}
|
|
404
|
+
const out: Record<string, unknown> = {};
|
|
405
|
+
for (const [key, el] of Object.entries(value)) {
|
|
406
|
+
out[key] = resolveValue(el, `${path}.${key}`, resolver, seen);
|
|
407
|
+
}
|
|
408
|
+
return out;
|
|
409
|
+
} finally {
|
|
410
|
+
seen.delete(value);
|
|
411
|
+
}
|
|
412
|
+
}
|
package/src/fold/subset.test.ts
CHANGED
|
@@ -258,6 +258,32 @@ describe("documented divergences — NOT unified by design (see subset.ts module
|
|
|
258
258
|
expect(evl001NonLiteralExpressionRule.check(context)).toHaveLength(0);
|
|
259
259
|
});
|
|
260
260
|
|
|
261
|
+
test("composite step access: fold still rejects a call as a value; EVL001 accepts .step access (chant #1544, opt-in only)", () => {
|
|
262
|
+
// `Checkout({...}).step` — the single-action Composite() wrapper idiom
|
|
263
|
+
// every lexicon's own docs/examples embed inline inside a Job's
|
|
264
|
+
// `steps:` array. fold() has no way to invoke an arbitrary composite
|
|
265
|
+
// factory, so it still rejects this shape and falls the file back to
|
|
266
|
+
// the run path (documented, correct behavior — never an error). EVL001
|
|
267
|
+
// now opts INTO treating it as shape-valid via `allowCompositeStepAccess`
|
|
268
|
+
// — a caller of `findSubsetViolation` that does not pass that flag
|
|
269
|
+
// (fold(), or this very call below) is completely unaffected.
|
|
270
|
+
const source = `const bad = new Thing({ x: Checkout({}).step });`;
|
|
271
|
+
const sourceFile = ts.createSourceFile("t.ts", source, ts.ScriptTarget.Latest, true);
|
|
272
|
+
const consts = collectConsts(sourceFile);
|
|
273
|
+
const badInit = consts.get("bad") as ts.NewExpression;
|
|
274
|
+
|
|
275
|
+
expect(() => foldResource(badInit, consts, [])).toThrow(FoldError);
|
|
276
|
+
|
|
277
|
+
// findSubsetViolation with no third argument (fold()'s own answer, and
|
|
278
|
+
// every pre-#1544 caller) is unchanged — still a violation.
|
|
279
|
+
const bareCallExpr = (badInit.arguments![0] as ts.ObjectLiteralExpression).properties[0];
|
|
280
|
+
expect(findSubsetViolation(bareCallExpr)).toBeDefined();
|
|
281
|
+
|
|
282
|
+
// EVL001 itself opts in and stops flagging it.
|
|
283
|
+
const context: LintContext = { sourceFile, entities: [], filePath: "t.ts", lexicon: undefined };
|
|
284
|
+
expect(evl001NonLiteralExpressionRule.check(context)).toHaveLength(0);
|
|
285
|
+
});
|
|
286
|
+
|
|
261
287
|
test("nested resource construction: NO LONGER a divergence (chant #1169) — both fold and EVL001 accept it", () => {
|
|
262
288
|
// This was the largest divergence in the table until #1169: a nested
|
|
263
289
|
// `new Type()` as a property value could only fold to a {__resource, props}
|
package/src/fold/subset.ts
CHANGED
|
@@ -231,18 +231,19 @@ export function unsupportedExpressionMessage(node: ts.Node): string {
|
|
|
231
231
|
export function checkObjectMember(
|
|
232
232
|
prop: ts.ObjectLiteralElementLike,
|
|
233
233
|
intrinsics?: readonly IntrinsicDef[],
|
|
234
|
+
allowCompositeStepAccess?: boolean,
|
|
234
235
|
): SubsetViolation | undefined {
|
|
235
236
|
if (ts.isPropertyAssignment(prop)) {
|
|
236
237
|
if (!isLiteralPropertyName(prop.name)) {
|
|
237
238
|
return violation(prop.name, computedPropertyNameMessage(prop.name));
|
|
238
239
|
}
|
|
239
|
-
return findSubsetViolation(prop.initializer, intrinsics);
|
|
240
|
+
return findSubsetViolation(prop.initializer, intrinsics, allowCompositeStepAccess);
|
|
240
241
|
}
|
|
241
242
|
if (ts.isShorthandPropertyAssignment(prop)) {
|
|
242
243
|
return undefined;
|
|
243
244
|
}
|
|
244
245
|
if (ts.isSpreadAssignment(prop)) {
|
|
245
|
-
return findSubsetViolation(prop.expression, intrinsics);
|
|
246
|
+
return findSubsetViolation(prop.expression, intrinsics, allowCompositeStepAccess);
|
|
246
247
|
}
|
|
247
248
|
return violation(prop, UNSUPPORTED_OBJECT_MEMBER_MESSAGE);
|
|
248
249
|
}
|
|
@@ -251,9 +252,10 @@ export function checkObjectMember(
|
|
|
251
252
|
function checkArrayElement(
|
|
252
253
|
el: ts.Expression,
|
|
253
254
|
intrinsics?: readonly IntrinsicDef[],
|
|
255
|
+
allowCompositeStepAccess?: boolean,
|
|
254
256
|
): SubsetViolation | undefined {
|
|
255
|
-
if (ts.isSpreadElement(el)) return findSubsetViolation(el.expression, intrinsics);
|
|
256
|
-
return findSubsetViolation(el, intrinsics);
|
|
257
|
+
if (ts.isSpreadElement(el)) return findSubsetViolation(el.expression, intrinsics, allowCompositeStepAccess);
|
|
258
|
+
return findSubsetViolation(el, intrinsics, allowCompositeStepAccess);
|
|
257
259
|
}
|
|
258
260
|
|
|
259
261
|
/**
|
|
@@ -265,10 +267,24 @@ function checkArrayElement(
|
|
|
265
267
|
* environment-dependent exceptions). Returns the first (deepest,
|
|
266
268
|
* `fold()`-evaluation-order) unsupported node, or `undefined` when `node`'s
|
|
267
269
|
* whole shape is foldable.
|
|
270
|
+
*
|
|
271
|
+
* `allowCompositeStepAccess` (chant #1544) is an EVL-only, MORE-permissive
|
|
272
|
+
* divergence in the same direction as points 1/2/2b/2c in the module doc
|
|
273
|
+
* above: a call immediately narrowed to a single member access — the
|
|
274
|
+
* `Checkout({...}).step` shape every lexicon's single-action `Composite()`
|
|
275
|
+
* wrappers document as their embedded-inline idiom (see composites.mdx —
|
|
276
|
+
* "normally embedded inline as `checkout.step` inside a `Job`'s `steps`
|
|
277
|
+
* array") — is treated as shape-valid. `fold()` itself never passes this
|
|
278
|
+
* (it has no such parameter to pass; only EVL001 opts in), so `fold()`'s own
|
|
279
|
+
* behavior is unchanged: it still falls the file back to the run path for
|
|
280
|
+
* this shape, exactly as composites.mdx describes as the expected, correct
|
|
281
|
+
* outcome — not an error. What changes is that EVL001 stops treating that
|
|
282
|
+
* ordinary, documented fallback as a lint error.
|
|
268
283
|
*/
|
|
269
284
|
export function findSubsetViolation(
|
|
270
285
|
node: ts.Node,
|
|
271
286
|
intrinsics?: readonly IntrinsicDef[],
|
|
287
|
+
allowCompositeStepAccess?: boolean,
|
|
272
288
|
): SubsetViolation | undefined {
|
|
273
289
|
if (
|
|
274
290
|
ts.isParenthesizedExpression(node) ||
|
|
@@ -276,7 +292,7 @@ export function findSubsetViolation(
|
|
|
276
292
|
ts.isSatisfiesExpression(node) ||
|
|
277
293
|
ts.isNonNullExpression(node)
|
|
278
294
|
) {
|
|
279
|
-
return findSubsetViolation(node.expression, intrinsics);
|
|
295
|
+
return findSubsetViolation(node.expression, intrinsics, allowCompositeStepAccess);
|
|
280
296
|
}
|
|
281
297
|
|
|
282
298
|
if (
|
|
@@ -311,7 +327,7 @@ export function findSubsetViolation(
|
|
|
311
327
|
|
|
312
328
|
if (ts.isTemplateExpression(node)) {
|
|
313
329
|
for (const span of node.templateSpans) {
|
|
314
|
-
const v = findSubsetViolation(span.expression, intrinsics);
|
|
330
|
+
const v = findSubsetViolation(span.expression, intrinsics, allowCompositeStepAccess);
|
|
315
331
|
if (v) return v;
|
|
316
332
|
}
|
|
317
333
|
return undefined;
|
|
@@ -319,7 +335,7 @@ export function findSubsetViolation(
|
|
|
319
335
|
|
|
320
336
|
if (ts.isObjectLiteralExpression(node)) {
|
|
321
337
|
for (const prop of node.properties) {
|
|
322
|
-
const v = checkObjectMember(prop, intrinsics);
|
|
338
|
+
const v = checkObjectMember(prop, intrinsics, allowCompositeStepAccess);
|
|
323
339
|
if (v) return v;
|
|
324
340
|
}
|
|
325
341
|
return undefined;
|
|
@@ -327,28 +343,35 @@ export function findSubsetViolation(
|
|
|
327
343
|
|
|
328
344
|
if (ts.isArrayLiteralExpression(node)) {
|
|
329
345
|
for (const el of node.elements) {
|
|
330
|
-
const v = checkArrayElement(el, intrinsics);
|
|
346
|
+
const v = checkArrayElement(el, intrinsics, allowCompositeStepAccess);
|
|
331
347
|
if (v) return v;
|
|
332
348
|
}
|
|
333
349
|
return undefined;
|
|
334
350
|
}
|
|
335
351
|
|
|
336
352
|
if (ts.isPropertyAccessExpression(node)) {
|
|
337
|
-
|
|
353
|
+
// chant #1544 — see this function's doc comment. `<call>(...).step`
|
|
354
|
+
// (any callee, any argument shape) is the composite-consumer idiom, EVL
|
|
355
|
+
// opt-in only: `fold()` never sets `allowCompositeStepAccess`, so it is
|
|
356
|
+
// unaffected and still falls back to the run path for this shape.
|
|
357
|
+
if (allowCompositeStepAccess && node.name.text === "step" && ts.isCallExpression(node.expression)) {
|
|
358
|
+
return undefined;
|
|
359
|
+
}
|
|
360
|
+
return findSubsetViolation(node.expression, intrinsics, allowCompositeStepAccess);
|
|
338
361
|
}
|
|
339
362
|
|
|
340
363
|
if (ts.isElementAccessExpression(node)) {
|
|
341
364
|
if (!isLiteralElementKey(node.argumentExpression)) {
|
|
342
365
|
return violation(node.argumentExpression, dynamicElementAccessMessage(node.argumentExpression), "EVL003");
|
|
343
366
|
}
|
|
344
|
-
return findSubsetViolation(node.expression, intrinsics);
|
|
367
|
+
return findSubsetViolation(node.expression, intrinsics, allowCompositeStepAccess);
|
|
345
368
|
}
|
|
346
369
|
|
|
347
370
|
if (ts.isPrefixUnaryExpression(node)) {
|
|
348
371
|
if (!SUPPORTED_UNARY_OPERATORS.has(node.operator)) {
|
|
349
372
|
return violation(node, UNSUPPORTED_UNARY_MESSAGE);
|
|
350
373
|
}
|
|
351
|
-
return findSubsetViolation(node.operand, intrinsics);
|
|
374
|
+
return findSubsetViolation(node.operand, intrinsics, allowCompositeStepAccess);
|
|
352
375
|
}
|
|
353
376
|
|
|
354
377
|
if (ts.isBinaryExpression(node)) {
|
|
@@ -358,15 +381,18 @@ export function findSubsetViolation(
|
|
|
358
381
|
}
|
|
359
382
|
// Flow-insensitive — see module doc: fold() short-circuits &&/||/?? and
|
|
360
383
|
// only evaluates the taken side; EVL requires both sides shape-valid.
|
|
361
|
-
return
|
|
384
|
+
return (
|
|
385
|
+
findSubsetViolation(node.left, intrinsics, allowCompositeStepAccess) ??
|
|
386
|
+
findSubsetViolation(node.right, intrinsics, allowCompositeStepAccess)
|
|
387
|
+
);
|
|
362
388
|
}
|
|
363
389
|
|
|
364
390
|
if (ts.isConditionalExpression(node)) {
|
|
365
391
|
// Flow-insensitive — see module doc: fold() only folds the taken branch.
|
|
366
392
|
return (
|
|
367
|
-
findSubsetViolation(node.condition, intrinsics) ??
|
|
368
|
-
findSubsetViolation(node.whenTrue, intrinsics) ??
|
|
369
|
-
findSubsetViolation(node.whenFalse, intrinsics)
|
|
393
|
+
findSubsetViolation(node.condition, intrinsics, allowCompositeStepAccess) ??
|
|
394
|
+
findSubsetViolation(node.whenTrue, intrinsics, allowCompositeStepAccess) ??
|
|
395
|
+
findSubsetViolation(node.whenFalse, intrinsics, allowCompositeStepAccess)
|
|
370
396
|
);
|
|
371
397
|
}
|
|
372
398
|
|
|
@@ -377,14 +403,14 @@ export function findSubsetViolation(
|
|
|
377
403
|
// argument is classified on its own terms and nothing is rejected merely
|
|
378
404
|
// for being in the "wrong" position.
|
|
379
405
|
for (const arg of node.arguments ?? []) {
|
|
380
|
-
const v = findSubsetViolation(arg, intrinsics);
|
|
406
|
+
const v = findSubsetViolation(arg, intrinsics, allowCompositeStepAccess);
|
|
381
407
|
if (v) return v;
|
|
382
408
|
}
|
|
383
409
|
return undefined;
|
|
384
410
|
}
|
|
385
411
|
|
|
386
412
|
if (ts.isSpreadElement(node)) {
|
|
387
|
-
return findSubsetViolation(node.expression, intrinsics);
|
|
413
|
+
return findSubsetViolation(node.expression, intrinsics, allowCompositeStepAccess);
|
|
388
414
|
}
|
|
389
415
|
|
|
390
416
|
if (ts.isCallExpression(node)) {
|
|
@@ -400,7 +426,7 @@ export function findSubsetViolation(
|
|
|
400
426
|
// ever be MORE permissive than `fold()`, never stricter.
|
|
401
427
|
if (ts.isIdentifier(node.expression) && isFoldableHelperName(node.expression.text)) {
|
|
402
428
|
for (const arg of node.arguments) {
|
|
403
|
-
const v = findSubsetViolation(arg, intrinsics);
|
|
429
|
+
const v = findSubsetViolation(arg, intrinsics, allowCompositeStepAccess);
|
|
404
430
|
if (v) return v;
|
|
405
431
|
}
|
|
406
432
|
return undefined;
|
|
@@ -420,7 +446,7 @@ export function findSubsetViolation(
|
|
|
420
446
|
intrinsics.some((i) => i.name === (node.expression as ts.Identifier).text && intrinsicCallFolds(i))
|
|
421
447
|
) {
|
|
422
448
|
for (const arg of node.arguments) {
|
|
423
|
-
const v = findSubsetViolation(arg, intrinsics);
|
|
449
|
+
const v = findSubsetViolation(arg, intrinsics, allowCompositeStepAccess);
|
|
424
450
|
if (v) return v;
|
|
425
451
|
}
|
|
426
452
|
return undefined;
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
export * from "./declarable";
|
|
8
8
|
export * from "./composite";
|
|
9
9
|
export * from "./provenance";
|
|
10
|
+
export * from "./secret-provenance";
|
|
11
|
+
export * from "./secret-materialization";
|
|
12
|
+
export * from "./effect-receipt";
|
|
10
13
|
export * from "./build-params";
|
|
11
14
|
export * from "./intrinsic";
|
|
12
15
|
export * from "./types";
|
|
@@ -43,6 +46,7 @@ export * from "./lint/selectors";
|
|
|
43
46
|
export * from "./lint/named-checks";
|
|
44
47
|
export * from "./lint/post-synth";
|
|
45
48
|
export * from "./lint/policy";
|
|
49
|
+
export * from "./lint/pipeline-change-gate";
|
|
46
50
|
export * from "./lint/rule-loader";
|
|
47
51
|
export * from "./lint/discover";
|
|
48
52
|
export * from "./import/parser";
|