@intentius/chant 0.19.1 → 0.20.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/build-params.d.ts +108 -0
- package/dist/build-params.d.ts.map +1 -0
- package/dist/build.d.ts +79 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts +31 -0
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-examples.d.ts +41 -0
- package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -0
- package/dist/cli/commands/check-lexicon-intrinsics.d.ts +54 -0
- package/dist/cli/commands/check-lexicon-intrinsics.d.ts.map +1 -0
- package/dist/cli/commands/check-lexicon.d.ts +1 -1
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts +27 -0
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/components.d.ts.map +1 -1
- package/dist/cli/handlers/lint.d.ts.map +1 -1
- package/dist/cli/handlers/run-client.d.ts +1 -1
- package/dist/cli/handlers/run-client.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts +16 -0
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +10 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/codegen/docs-sections.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +12 -5
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/discover.d.ts +62 -7
- package/dist/components/discover.d.ts.map +1 -1
- package/dist/components/sandbox/driver.d.ts +12 -0
- package/dist/components/sandbox/driver.d.ts.map +1 -0
- package/dist/components/sandbox/run.d.ts +42 -0
- package/dist/components/sandbox/run.d.ts.map +1 -0
- package/dist/composite.d.ts +5 -0
- package/dist/composite.d.ts.map +1 -1
- package/dist/config.d.ts +71 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/discovery/collect.d.ts.map +1 -1
- package/dist/discovery/entity-wire-codec.d.ts +166 -0
- package/dist/discovery/entity-wire-codec.d.ts.map +1 -0
- package/dist/discovery/entity-wire.d.ts +50 -0
- package/dist/discovery/entity-wire.d.ts.map +1 -0
- package/dist/discovery/fold-import.d.ts +210 -0
- package/dist/discovery/fold-import.d.ts.map +1 -0
- package/dist/discovery/index.d.ts +74 -1
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/discovery/sandbox/bundle.d.ts +18 -0
- package/dist/discovery/sandbox/bundle.d.ts.map +1 -0
- package/dist/discovery/sandbox/child-errors.d.ts +15 -0
- package/dist/discovery/sandbox/child-errors.d.ts.map +1 -0
- package/dist/discovery/sandbox/driver.d.ts +13 -0
- package/dist/discovery/sandbox/driver.d.ts.map +1 -0
- package/dist/discovery/sandbox/run.d.ts +69 -0
- package/dist/discovery/sandbox/run.d.ts.map +1 -0
- package/dist/errors.d.ts +9 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/fold/fold.d.ts +226 -0
- package/dist/fold/fold.d.ts.map +1 -0
- package/dist/fold/subset.d.ts +102 -0
- package/dist/fold/subset.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/lexicon-output.d.ts +7 -2
- package/dist/lexicon-output.d.ts.map +1 -1
- package/dist/lexicon-schema.d.ts +2 -2
- package/dist/lexicon-schema.d.ts.map +1 -1
- package/dist/lexicon.d.ts +81 -1
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/release-ledger.d.ts +11 -0
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lint/component-checks.d.ts +7 -1
- package/dist/lint/component-checks.d.ts.map +1 -1
- package/dist/lint/rules/evl001-non-literal-expression.d.ts.map +1 -1
- package/dist/lint/rules/evl003-dynamic-property-access.d.ts.map +1 -1
- package/dist/params.d.ts +60 -0
- package/dist/params.d.ts.map +1 -0
- package/dist/provenance.d.ts +21 -0
- package/dist/provenance.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/build-params.test.ts +144 -0
- package/src/build-params.ts +207 -0
- package/src/build.test.ts +38 -0
- package/src/build.ts +144 -7
- package/src/cli/commands/build.test.ts +220 -2
- package/src/cli/commands/build.ts +111 -3
- package/src/cli/commands/check-lexicon-examples.test.ts +92 -0
- package/src/cli/commands/check-lexicon-examples.ts +103 -0
- package/src/cli/commands/check-lexicon-intrinsics.test.ts +145 -0
- package/src/cli/commands/check-lexicon-intrinsics.ts +312 -0
- package/src/cli/commands/check-lexicon.test.ts +34 -0
- package/src/cli/commands/check-lexicon.ts +119 -1
- package/src/cli/commands/lint.ts +31 -3
- package/src/cli/commands/onboard.ts +1 -1
- package/src/cli/component-security-boundary.test.ts +170 -0
- package/src/cli/handlers/build.ts +24 -3
- package/src/cli/handlers/components.ts +9 -2
- package/src/cli/handlers/dev.ts +1 -1
- package/src/cli/handlers/graph.ts +7 -5
- package/src/cli/handlers/lifecycle.ts +2 -2
- package/src/cli/handlers/lint.ts +2 -0
- package/src/cli/handlers/misc.ts +2 -2
- package/src/cli/handlers/run-client.ts +1 -1
- package/src/cli/handlers/run.ts +20 -5
- package/src/cli/main.test.ts +22 -0
- package/src/cli/main.ts +39 -0
- package/src/cli/plugins.ts +20 -4
- package/src/cli/registry.ts +10 -0
- package/src/cli/security-boundary.test.ts +135 -0
- package/src/codegen/docs-sections.test.ts +61 -0
- package/src/codegen/docs-sections.ts +7 -3
- package/src/components/cli-support.ts +22 -10
- package/src/components/discover.ts +127 -25
- package/src/components/sandbox/driver.ts +114 -0
- package/src/components/sandbox/run.test.ts +185 -0
- package/src/components/sandbox/run.ts +177 -0
- package/src/composite.test.ts +21 -0
- package/src/composite.ts +20 -1
- package/src/config.ts +81 -0
- package/src/discovery/collect.ts +17 -3
- package/src/discovery/entity-wire-codec.ts +485 -0
- package/src/discovery/entity-wire.test.ts +240 -0
- package/src/discovery/entity-wire.ts +67 -0
- package/src/discovery/fold-import.test.ts +1026 -0
- package/src/discovery/fold-import.ts +1805 -0
- package/src/discovery/index.test.ts +191 -1
- package/src/discovery/index.ts +233 -1
- package/src/discovery/sandbox/bundle.ts +218 -0
- package/src/discovery/sandbox/child-errors.ts +65 -0
- package/src/discovery/sandbox/driver.ts +147 -0
- package/src/discovery/sandbox/run.test.ts +179 -0
- package/src/discovery/sandbox/run.ts +196 -0
- package/src/errors.ts +9 -1
- package/src/fold/fold.test.ts +535 -0
- package/src/fold/fold.ts +648 -0
- package/src/fold/subset-doc-parity.test.ts +183 -0
- package/src/fold/subset.test.ts +241 -0
- package/src/fold/subset.ts +302 -0
- package/src/index.ts +2 -0
- package/src/lexicon-output.ts +7 -2
- package/src/lexicon-schema.test.ts +14 -0
- package/src/lexicon-schema.ts +3 -1
- package/src/lexicon.ts +82 -1
- package/src/lifecycle/git.test.ts +10 -5
- package/src/lifecycle/release-ledger.test.ts +28 -0
- package/src/lifecycle/release-ledger.ts +11 -0
- package/src/lint/component-checks.ts +8 -1
- package/src/lint/rules/evl001-non-literal-expression.ts +26 -110
- package/src/lint/rules/evl003-dynamic-property-access.ts +11 -2
- package/src/params.test.ts +22 -0
- package/src/params.ts +66 -0
- package/src/provenance.ts +22 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* subset — the single canonical definition of chant's statically-foldable
|
|
5
|
+
* expression subset (chant #1024, epic #1019).
|
|
6
|
+
*
|
|
7
|
+
* `fold()` ({@link "./fold"}, the enforcement layer — a construct outside
|
|
8
|
+
* this subset simply has no case there) and EVL001/EVL003
|
|
9
|
+
* ({@link "../lint/rules/evl001-non-literal-expression"},
|
|
10
|
+
* {@link "../lint/rules/evl003-dynamic-property-access"} — the pre-flight
|
|
11
|
+
* diagnostic layer) both import this module so the two can never drift
|
|
12
|
+
* apart on *which node kinds, operators, and key shapes* are foldable.
|
|
13
|
+
* Before this module existed, `fold()` and EVL001 each hand-rolled their
|
|
14
|
+
* own recursive classifier; they agreed almost everywhere but had several
|
|
15
|
+
* real, silent gaps (see git history of #1024 for the enumerated list —
|
|
16
|
+
* dynamic object-literal keys, template/tagged-template interiors, and
|
|
17
|
+
* unrestricted binary/unary operators were all accepted by EVL001 but
|
|
18
|
+
* rejected by `fold()`). This module is the fix: one classifier, two
|
|
19
|
+
* importers.
|
|
20
|
+
*
|
|
21
|
+
* Scope — this module classifies *shape* only: the syntactic kind of an
|
|
22
|
+
* expression, its operator, and (for keys) its literal-ness. It
|
|
23
|
+
* deliberately does NOT resolve bindings or perform any evaluation, because
|
|
24
|
+
* three things a full fold needs are inherently environment-dependent and
|
|
25
|
+
* cannot be recovered from shape alone:
|
|
26
|
+
*
|
|
27
|
+
* 1. Identifier *resolution* — is a bare name actually a local `const`,
|
|
28
|
+
* vs. an unbound name? That needs the file's `consts` map. Both
|
|
29
|
+
* `fold()` and this module treat a bare identifier as shape-valid;
|
|
30
|
+
* `fold()` alone resolves it (and rejects if unresolved) once it has
|
|
31
|
+
* that map. A lint rule has no equivalent binding-resolution pass
|
|
32
|
+
* today, so EVL (via this module) stays permissive here — a known,
|
|
33
|
+
* intentional asymmetry, not a bug: it can only ever be a *false
|
|
34
|
+
* negative* on EVL's part (EVL passes something `fold()` might later
|
|
35
|
+
* reject for being unresolved), never the reverse.
|
|
36
|
+
* 2. Tagged-template *tag registration* — needs a lexicon's intrinsics
|
|
37
|
+
* manifest, which isn't available to a syntax-only lint rule. `fold()`
|
|
38
|
+
* alone checks it; this module treats any tag name as shape-valid and
|
|
39
|
+
* only classifies the interpolated values.
|
|
40
|
+
* 3. Runtime *type* of a folded value — e.g. spreading `const n = 5`
|
|
41
|
+
* (`{...n}`) is shape-valid (`n` is a plain identifier) but `fold()`
|
|
42
|
+
* rejects it once it discovers `n` folds to a number, not an object.
|
|
43
|
+
* Only real evaluation catches this; EVL004 independently narrows
|
|
44
|
+
* spread sources to a stricter "traceable to a const" shape, which
|
|
45
|
+
* catches most real-world misuse without evaluating, but is not a
|
|
46
|
+
* full substitute.
|
|
47
|
+
*
|
|
48
|
+
* One more inherent gap, on the *value-flow* side rather than shape:
|
|
49
|
+
* `fold()` evaluates `&&`/`||`/`??` and `? :` lazily — it only folds the
|
|
50
|
+
* side/branch actually taken, exactly like the JS runtime — so an
|
|
51
|
+
* otherwise-unfoldable *untaken* branch does not reject
|
|
52
|
+
* (`false && sideEffect()` folds cleanly to `false`). This module (and so
|
|
53
|
+
* EVL) is flow-insensitive: it has no notion of "taken", so it requires
|
|
54
|
+
* every operand/branch to be shape-valid. This can only make EVL *stricter*
|
|
55
|
+
* than `fold()` (a false positive relative to fold, flagging code the
|
|
56
|
+
* folder would actually accept) — never the reverse. Making EVL
|
|
57
|
+
* flow-sensitive would mean re-implementing an evaluator inside a lint
|
|
58
|
+
* rule; out of scope here. See #1024.
|
|
59
|
+
*/
|
|
60
|
+
|
|
61
|
+
/** The two EVL rule ids a shape violation can be attributed to. */
|
|
62
|
+
export type SubsetRuleId = "EVL001" | "EVL003";
|
|
63
|
+
|
|
64
|
+
/** A located, shape-level rejection: the offending node, the EVL rule id it maps to, and a message. */
|
|
65
|
+
export interface SubsetViolation {
|
|
66
|
+
node: ts.Node;
|
|
67
|
+
ruleId: SubsetRuleId;
|
|
68
|
+
message: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function violation(node: ts.Node, message: string, ruleId: SubsetRuleId = "EVL001"): SubsetViolation {
|
|
72
|
+
return { node, ruleId, message };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Binary operators `fold()` implements — see `fold()`'s operator switch in ./fold.ts. */
|
|
76
|
+
export const SUPPORTED_BINARY_OPERATORS: ReadonlySet<ts.SyntaxKind> = new Set([
|
|
77
|
+
ts.SyntaxKind.AmpersandAmpersandToken,
|
|
78
|
+
ts.SyntaxKind.BarBarToken,
|
|
79
|
+
ts.SyntaxKind.QuestionQuestionToken,
|
|
80
|
+
ts.SyntaxKind.PlusToken,
|
|
81
|
+
ts.SyntaxKind.MinusToken,
|
|
82
|
+
ts.SyntaxKind.AsteriskToken,
|
|
83
|
+
ts.SyntaxKind.SlashToken,
|
|
84
|
+
ts.SyntaxKind.EqualsEqualsEqualsToken,
|
|
85
|
+
ts.SyntaxKind.ExclamationEqualsEqualsToken,
|
|
86
|
+
ts.SyntaxKind.GreaterThanToken,
|
|
87
|
+
ts.SyntaxKind.LessThanToken,
|
|
88
|
+
ts.SyntaxKind.GreaterThanEqualsToken,
|
|
89
|
+
ts.SyntaxKind.LessThanEqualsToken,
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
/** Prefix unary operators `fold()` implements: logical-not and numeric negation. */
|
|
93
|
+
export const SUPPORTED_UNARY_OPERATORS: ReadonlySet<ts.SyntaxKind> = new Set([
|
|
94
|
+
ts.SyntaxKind.ExclamationToken,
|
|
95
|
+
ts.SyntaxKind.MinusToken,
|
|
96
|
+
]);
|
|
97
|
+
|
|
98
|
+
/** A property name foldable without execution: identifier, string, or numeric literal (not a computed name). */
|
|
99
|
+
export function isLiteralPropertyName(
|
|
100
|
+
node: ts.PropertyName,
|
|
101
|
+
): node is ts.Identifier | ts.StringLiteral | ts.NumericLiteral {
|
|
102
|
+
return ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** An element-access key foldable without execution: a string or numeric LITERAL only (EVL003 semantics). */
|
|
106
|
+
export function isLiteralElementKey(node: ts.Expression): node is ts.StringLiteral | ts.NumericLiteral {
|
|
107
|
+
return ts.isStringLiteral(node) || ts.isNumericLiteral(node);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// Shared message builders — `fold()` and `findSubsetViolation` both call
|
|
112
|
+
// these so the diagnostic text for the same violation kind is the same
|
|
113
|
+
// string, not two hand-written copies that can drift.
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
export function computedPropertyNameMessage(node: ts.PropertyName): string {
|
|
117
|
+
return `computed/dynamic property name not foldable: ${node.getText()}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function dynamicElementAccessMessage(keyNode: ts.Expression): string {
|
|
121
|
+
return `dynamic property access — computed key must be a string or numeric literal: ${keyNode.getText()}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export const UNSUPPORTED_OBJECT_MEMBER_MESSAGE = "unsupported object member";
|
|
125
|
+
|
|
126
|
+
export const UNSUPPORTED_UNARY_MESSAGE = "unsupported unary";
|
|
127
|
+
|
|
128
|
+
export function unsupportedBinaryMessage(opKind: ts.SyntaxKind): string {
|
|
129
|
+
return `unsupported binary operator: ${ts.SyntaxKind[opKind]}`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function callExpressionMessage(node: ts.CallExpression): string {
|
|
133
|
+
return `function call as a value is not foldable: ${node.expression.getText()}(...)`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function unsupportedExpressionMessage(node: ts.Node): string {
|
|
137
|
+
return `unsupported expression: ${ts.SyntaxKind[node.kind]}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function resourceCtorArgMessage(typeName: string): string {
|
|
141
|
+
return `resource constructor argument must be an object literal: ${typeName}(...)`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Classify one object-literal member (`{ a: 1 }`'s `a: 1`, `{ ...x }`'s
|
|
146
|
+
* `...x`, or a shorthand `{ a }`). Checks the key's shape before the
|
|
147
|
+
* value's, mirroring `propName()` in fold.ts, which runs before folding
|
|
148
|
+
* the value. Returns the first violation within this member, or
|
|
149
|
+
* `undefined` when it's fully in the subset.
|
|
150
|
+
*/
|
|
151
|
+
export function checkObjectMember(prop: ts.ObjectLiteralElementLike): SubsetViolation | undefined {
|
|
152
|
+
if (ts.isPropertyAssignment(prop)) {
|
|
153
|
+
if (!isLiteralPropertyName(prop.name)) {
|
|
154
|
+
return violation(prop.name, computedPropertyNameMessage(prop.name));
|
|
155
|
+
}
|
|
156
|
+
return findSubsetViolation(prop.initializer);
|
|
157
|
+
}
|
|
158
|
+
if (ts.isShorthandPropertyAssignment(prop)) {
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
if (ts.isSpreadAssignment(prop)) {
|
|
162
|
+
return findSubsetViolation(prop.expression);
|
|
163
|
+
}
|
|
164
|
+
return violation(prop, UNSUPPORTED_OBJECT_MEMBER_MESSAGE);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Classify one array-literal element: a value, or a `...spread`. */
|
|
168
|
+
function checkArrayElement(el: ts.Expression): SubsetViolation | undefined {
|
|
169
|
+
if (ts.isSpreadElement(el)) return findSubsetViolation(el.expression);
|
|
170
|
+
return findSubsetViolation(el);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* The canonical recursive shape classifier: is `node`'s expression *shape*
|
|
175
|
+
* within the subset `fold()` can reduce? Mirrors `fold()`'s own dispatch
|
|
176
|
+
* node-kind for node-kind (see ./fold.ts) — every branch here has a
|
|
177
|
+
* matching branch there, and vice versa — but performs no
|
|
178
|
+
* resolution/evaluation (see the module doc comment for the three
|
|
179
|
+
* environment-dependent exceptions). Returns the first (deepest,
|
|
180
|
+
* `fold()`-evaluation-order) unsupported node, or `undefined` when `node`'s
|
|
181
|
+
* whole shape is foldable.
|
|
182
|
+
*/
|
|
183
|
+
export function findSubsetViolation(node: ts.Node): SubsetViolation | undefined {
|
|
184
|
+
if (
|
|
185
|
+
ts.isParenthesizedExpression(node) ||
|
|
186
|
+
ts.isAsExpression(node) ||
|
|
187
|
+
ts.isSatisfiesExpression(node) ||
|
|
188
|
+
ts.isNonNullExpression(node)
|
|
189
|
+
) {
|
|
190
|
+
return findSubsetViolation(node.expression);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (
|
|
194
|
+
ts.isStringLiteral(node) ||
|
|
195
|
+
ts.isNoSubstitutionTemplateLiteral(node) ||
|
|
196
|
+
ts.isNumericLiteral(node) ||
|
|
197
|
+
node.kind === ts.SyntaxKind.TrueKeyword ||
|
|
198
|
+
node.kind === ts.SyntaxKind.FalseKeyword ||
|
|
199
|
+
node.kind === ts.SyntaxKind.NullKeyword
|
|
200
|
+
) {
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Identifiers (including a bare `undefined` reference, which the parser
|
|
205
|
+
// represents as an Identifier, not a keyword token) — resolution is
|
|
206
|
+
// environment-dependent, see module doc.
|
|
207
|
+
if (ts.isIdentifier(node)) return undefined;
|
|
208
|
+
|
|
209
|
+
if (ts.isTaggedTemplateExpression(node)) {
|
|
210
|
+
// Tagged-template interiors are OPAQUE to the shape classifier. A tagged
|
|
211
|
+
// template may be a registered lexicon intrinsic (e.g. Sub`...`) whose
|
|
212
|
+
// interpolations legitimately contain deploy-time intrinsic references
|
|
213
|
+
// (Ref(env), AWS.StackName, ...) — valid, but not statically foldable. EVL
|
|
214
|
+
// has no intrinsic registry at lint time (see module doc), so it cannot tell
|
|
215
|
+
// an intrinsic call from a plain one; recursing here would false-flag Ref()
|
|
216
|
+
// inside Sub`...` and break every intrinsic-using example. fold() DOES have
|
|
217
|
+
// the registry: it recurses into a *registered* tag's interior itself
|
|
218
|
+
// (foldIntrinsicValue) and rejects an unfoldable one there. So an unfoldable
|
|
219
|
+
// tagged-template interior is a documented fold/EVL divergence, not a hole.
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (ts.isTemplateExpression(node)) {
|
|
224
|
+
for (const span of node.templateSpans) {
|
|
225
|
+
const v = findSubsetViolation(span.expression);
|
|
226
|
+
if (v) return v;
|
|
227
|
+
}
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
232
|
+
for (const prop of node.properties) {
|
|
233
|
+
const v = checkObjectMember(prop);
|
|
234
|
+
if (v) return v;
|
|
235
|
+
}
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (ts.isArrayLiteralExpression(node)) {
|
|
240
|
+
for (const el of node.elements) {
|
|
241
|
+
const v = checkArrayElement(el);
|
|
242
|
+
if (v) return v;
|
|
243
|
+
}
|
|
244
|
+
return undefined;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
248
|
+
return findSubsetViolation(node.expression);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (ts.isElementAccessExpression(node)) {
|
|
252
|
+
if (!isLiteralElementKey(node.argumentExpression)) {
|
|
253
|
+
return violation(node.argumentExpression, dynamicElementAccessMessage(node.argumentExpression), "EVL003");
|
|
254
|
+
}
|
|
255
|
+
return findSubsetViolation(node.expression);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (ts.isPrefixUnaryExpression(node)) {
|
|
259
|
+
if (!SUPPORTED_UNARY_OPERATORS.has(node.operator)) {
|
|
260
|
+
return violation(node, UNSUPPORTED_UNARY_MESSAGE);
|
|
261
|
+
}
|
|
262
|
+
return findSubsetViolation(node.operand);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (ts.isBinaryExpression(node)) {
|
|
266
|
+
const opKind = node.operatorToken.kind;
|
|
267
|
+
if (!SUPPORTED_BINARY_OPERATORS.has(opKind)) {
|
|
268
|
+
return violation(node, unsupportedBinaryMessage(opKind));
|
|
269
|
+
}
|
|
270
|
+
// Flow-insensitive — see module doc: fold() short-circuits &&/||/?? and
|
|
271
|
+
// only evaluates the taken side; EVL requires both sides shape-valid.
|
|
272
|
+
return findSubsetViolation(node.left) ?? findSubsetViolation(node.right);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (ts.isConditionalExpression(node)) {
|
|
276
|
+
// Flow-insensitive — see module doc: fold() only folds the taken branch.
|
|
277
|
+
return (
|
|
278
|
+
findSubsetViolation(node.condition) ??
|
|
279
|
+
findSubsetViolation(node.whenTrue) ??
|
|
280
|
+
findSubsetViolation(node.whenFalse)
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (ts.isNewExpression(node)) {
|
|
285
|
+
const [firstArg] = node.arguments ?? [];
|
|
286
|
+
if (!firstArg) return undefined;
|
|
287
|
+
if (!ts.isObjectLiteralExpression(firstArg)) {
|
|
288
|
+
return violation(firstArg, resourceCtorArgMessage(node.expression.getText()));
|
|
289
|
+
}
|
|
290
|
+
return findSubsetViolation(firstArg);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (ts.isSpreadElement(node)) {
|
|
294
|
+
return findSubsetViolation(node.expression);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (ts.isCallExpression(node)) {
|
|
298
|
+
return violation(node, callExpressionMessage(node));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return violation(node, unsupportedExpressionMessage(node));
|
|
302
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
export * from "./declarable";
|
|
8
8
|
export * from "./composite";
|
|
9
9
|
export * from "./provenance";
|
|
10
|
+
export * from "./build-params";
|
|
10
11
|
export * from "./intrinsic";
|
|
11
12
|
export * from "./types";
|
|
12
13
|
export * from "./errors";
|
|
@@ -33,6 +34,7 @@ export * from "./graph-dot";
|
|
|
33
34
|
export * from "./graph-layout";
|
|
34
35
|
export * from "./graph-lens";
|
|
35
36
|
export * from "./detectLexicon";
|
|
37
|
+
export * from "./fold/fold";
|
|
36
38
|
export * from "./lint/parser";
|
|
37
39
|
export * from "./lint/rule";
|
|
38
40
|
export * from "./lint/rules";
|
package/src/lexicon-output.ts
CHANGED
|
@@ -45,8 +45,13 @@ export class LexiconOutput implements Intrinsic {
|
|
|
45
45
|
readonly outputName: string;
|
|
46
46
|
/** @internal WeakRef to the source entity object for identity-based matching */
|
|
47
47
|
readonly _sourceParent: WeakRef<object> | null;
|
|
48
|
-
/**
|
|
49
|
-
|
|
48
|
+
/**
|
|
49
|
+
* @internal Intrinsic value when constructed from an Intrinsic rather than AttrRef.
|
|
50
|
+
* Readable outside the class (like `_sourceParent` above) so the entity-wire
|
|
51
|
+
* encoder can reach it without an `as unknown as` cast that would erase type
|
|
52
|
+
* checking on every field it reads (#1047).
|
|
53
|
+
*/
|
|
54
|
+
readonly _intrinsic: Intrinsic | null;
|
|
50
55
|
|
|
51
56
|
constructor(ref: AttrRef | Intrinsic | string, name: string) {
|
|
52
57
|
if (ref instanceof AttrRef) {
|
|
@@ -131,6 +131,20 @@ describe("IntrinsicDefSchema", () => {
|
|
|
131
131
|
const result = IntrinsicDefSchema.safeParse({ description: "no name" });
|
|
132
132
|
expect(result.success).toBe(false);
|
|
133
133
|
});
|
|
134
|
+
|
|
135
|
+
// chant #1067 — isTag went from optional to required: a registration
|
|
136
|
+
// silently defaulting to "not a tag" is exactly how #1039 shipped
|
|
137
|
+
// (aws's Sub had no isTag at all and never folded in production).
|
|
138
|
+
test("rejects a registration with isTag omitted", () => {
|
|
139
|
+
const result = IntrinsicDefSchema.safeParse({ name: "Sub", description: "no isTag" });
|
|
140
|
+
expect(result.success).toBe(false);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("accepts isTag: true and isTag: false, rejects non-boolean isTag", () => {
|
|
144
|
+
expect(IntrinsicDefSchema.safeParse({ name: "Sub", isTag: true }).success).toBe(true);
|
|
145
|
+
expect(IntrinsicDefSchema.safeParse({ name: "Ref", isTag: false }).success).toBe(true);
|
|
146
|
+
expect(IntrinsicDefSchema.safeParse({ name: "Ref", isTag: "false" }).success).toBe(false);
|
|
147
|
+
});
|
|
134
148
|
});
|
|
135
149
|
|
|
136
150
|
// ---------------------------------------------------------------------------
|
package/src/lexicon-schema.ts
CHANGED
|
@@ -13,7 +13,9 @@ export const IntrinsicDefSchema = z.object({
|
|
|
13
13
|
name: z.string().min(1, "intrinsic name must not be empty"),
|
|
14
14
|
description: z.string().optional(),
|
|
15
15
|
outputKey: z.string().optional(),
|
|
16
|
-
|
|
16
|
+
// Required (chant #1067) — no silent default for whether an intrinsic
|
|
17
|
+
// folds. See IntrinsicDef.isTag in ../lexicon.ts for the history.
|
|
18
|
+
isTag: z.boolean(),
|
|
17
19
|
});
|
|
18
20
|
|
|
19
21
|
// ---------------------------------------------------------------------------
|
package/src/lexicon.ts
CHANGED
|
@@ -18,6 +18,41 @@ export type { ReferenceCatalog, IdentityRule, RefRule } from "./graph-refs";
|
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Manifest for a packaged lexicon — metadata embedded in the tarball.
|
|
21
|
+
*
|
|
22
|
+
* chant #1067 decided each optional field below explicitly rather than
|
|
23
|
+
* leaving them all equally unchecked:
|
|
24
|
+
*
|
|
25
|
+
* - `chantVersion` — now validated for presence/shape by `chant dev
|
|
26
|
+
* check-lexicon` (a lexicon's `dist/manifest.json` must declare one).
|
|
27
|
+
* NOT validated for compatibility against the core version actually
|
|
28
|
+
* running — that needs a live check at plugin-load time (a different
|
|
29
|
+
* surface: `loadPlugin`/`loadPlugins` in `./cli/plugins.ts`), which stays
|
|
30
|
+
* a deliberate non-goal here.
|
|
31
|
+
* - `namespace` — deliberately NOT validated for cross-lexicon uniqueness in
|
|
32
|
+
* #1067. `chant dev check-lexicon <dir>` only ever inspects one lexicon at
|
|
33
|
+
* a time, so it structurally can't catch a collision between two
|
|
34
|
+
* *different* lexicons. The natural home is `checkConflicts` (./cli/
|
|
35
|
+
* conflict-check.ts), which already detects cross-lexicon rule-id/skill/
|
|
36
|
+
* MCP-tool/MCP-resource collisions when multiple plugins load together —
|
|
37
|
+
* but `namespace` isn't on the runtime `LexiconPlugin` surface at all
|
|
38
|
+
* (manifest-only, baked in at package time), so extending that check would
|
|
39
|
+
* mean adding a new field to `LexiconPlugin` itself. Deferred rather than
|
|
40
|
+
* done under time pressure alongside a concurrent, unrelated change to
|
|
41
|
+
* this same interface (chant #1064).
|
|
42
|
+
* - `pseudoParameters` — deliberately NOT validated against what the
|
|
43
|
+
* lexicon actually exports in #1067, unlike `intrinsics` (see
|
|
44
|
+
* `IntrinsicDef.isTag` below and `./cli/commands/check-lexicon-
|
|
45
|
+
* intrinsics.ts`). The same static-analysis approach would apply — check
|
|
46
|
+
* each declared pseudo-parameter's short name against the real exported
|
|
47
|
+
* namespace object's properties (`AWS.StackName`, `Azure.ResourceGroupName`,
|
|
48
|
+
* ...) — but unlike `intrinsics()`, `pseudoParameters(): string[]` is a
|
|
49
|
+
* flat list of dotted strings with no structural link back to a specific
|
|
50
|
+
* export/property declaration, making the same trick more involved to
|
|
51
|
+
* generalize correctly. Relevant to #1063 (folding cross-file references
|
|
52
|
+
* into lexicon pseudo-parameter namespaces): once that lands, a wrong or
|
|
53
|
+
* missing pseudo-parameter property would silently break folding exactly
|
|
54
|
+
* the way #1039 did for intrinsics, which would be the moment this
|
|
55
|
+
* deferral needs revisiting.
|
|
21
56
|
*/
|
|
22
57
|
export interface LexiconManifest {
|
|
23
58
|
name: string;
|
|
@@ -102,7 +137,53 @@ export interface IntrinsicDef {
|
|
|
102
137
|
readonly name: string;
|
|
103
138
|
readonly description?: string;
|
|
104
139
|
readonly outputKey?: string;
|
|
105
|
-
|
|
140
|
+
/**
|
|
141
|
+
* Whether this intrinsic is authored as a JS tagged template (`` Sub`...` ``)
|
|
142
|
+
* rather than a plain function call (`Ref(...)`). Required — chant #1067 —
|
|
143
|
+
* because an omitted value silently defaulted to "not a tag" with no
|
|
144
|
+
* signal that the registration had never been decided. That produced #1039
|
|
145
|
+
* in both directions at once: aws's `Sub` (a genuine tagged template, the
|
|
146
|
+
* most-used intrinsic in the ecosystem) shipped with no `isTag` at all and
|
|
147
|
+
* silently never folded, while gitlab's `reference()` (a plain call)
|
|
148
|
+
* shipped with `isTag: true`. Both were wrong, and both shipped, because
|
|
149
|
+
* nothing forced the declaration or checked it against how the intrinsic is
|
|
150
|
+
* actually authored. `chant dev check-lexicon` now validates every
|
|
151
|
+
* registration here against its real declaration (tagged-template
|
|
152
|
+
* signature vs plain call) and against the package's own exports — see
|
|
153
|
+
* `../cli/commands/check-lexicon-intrinsics.ts`.
|
|
154
|
+
*/
|
|
155
|
+
readonly isTag: boolean;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Whether `chant build --fold` can ever fold a call to this intrinsic
|
|
160
|
+
* (chant #1062, epic #1019). Today the answer is a direct function of
|
|
161
|
+
* `isTag`: `fold()` (../fold/fold.ts) has no `CallExpression` case at all —
|
|
162
|
+
* a plain-call intrinsic (`Ref(...)`, `Concat(...)`, `reference(...)`, …) is
|
|
163
|
+
* always a run-fallback, no matter what it's named or registered as — while
|
|
164
|
+
* a *registered* tagged-template intrinsic (`Sub\`...\``) folds because
|
|
165
|
+
* `foldTaggedTemplate` recognizes its tag and recurses into the interior.
|
|
166
|
+
*
|
|
167
|
+
* This function is the single predicate both `fold()` (deciding whether a
|
|
168
|
+
* tag is registered for real) and the generated per-lexicon intrinsics page
|
|
169
|
+
* (`../codegen/docs-sections.ts`'s "Folds?" column) call — never two copies
|
|
170
|
+
* of the same `isTag === true` check that could silently drift. #1044 (per-
|
|
171
|
+
* intrinsic, per-lexicon foldability) changes this function's body to
|
|
172
|
+
* consult something more than `isTag` for a plain call that becomes
|
|
173
|
+
* foldable; every caller keeps working unchanged, and the generated matrix
|
|
174
|
+
* updates the moment a lexicon's registration says a given intrinsic now
|
|
175
|
+
* folds — no doc rewrite, no second code path to remember.
|
|
176
|
+
*
|
|
177
|
+
* Takes `{ isTag?: boolean }` rather than `Pick<IntrinsicDef, "isTag">`
|
|
178
|
+
* deliberately: `IntrinsicDef.isTag` is required for new registrations
|
|
179
|
+
* (chant #1067), but this predicate also reads `isTag` off untrusted,
|
|
180
|
+
* possibly-older parsed JSON (`ManifestJSON` in `./codegen/docs-types.ts`,
|
|
181
|
+
* on disk as a published lexicon's `dist/manifest.json`) that may predate
|
|
182
|
+
* the required field. `undefined` there means the same thing it always
|
|
183
|
+
* did — not a tag — so the check below is intentionally unchanged.
|
|
184
|
+
*/
|
|
185
|
+
export function intrinsicFolds(def: { isTag?: boolean }): boolean {
|
|
186
|
+
return def.isTag === true;
|
|
106
187
|
}
|
|
107
188
|
|
|
108
189
|
/**
|
|
@@ -2,6 +2,7 @@ import { describe, test, expect } from "vitest";
|
|
|
2
2
|
import { withTestDir } from "@intentius/chant-test-utils";
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
4
|
import { writeFileSync } from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
5
6
|
import { join } from "node:path";
|
|
6
7
|
import {
|
|
7
8
|
writeSnapshot,
|
|
@@ -182,12 +183,16 @@ describe("lifecycle/git", () => {
|
|
|
182
183
|
* `origin`. Returns the clone path; the caller writes snapshots there.
|
|
183
184
|
*/
|
|
184
185
|
async function setupClonePair(): Promise<{ clonePath: string; remotePath: string; cleanup: () => Promise<void> }> {
|
|
185
|
-
|
|
186
|
-
|
|
186
|
+
// These land in the OS temp dir, never under `import.meta.dirname` — that
|
|
187
|
+
// would put live git repos inside `packages/core/src`, which other suites
|
|
188
|
+
// walk recursively while this one creates and deletes them (update.test.ts
|
|
189
|
+
// -> copyTypeFiles hit ENOENT mid-walk that way).
|
|
190
|
+
const remotePath = join(tmpdir(), `chant-state-remote-${Date.now()}-${Math.random()}`);
|
|
191
|
+
const clonePath = join(tmpdir(), `chant-state-clone-${Date.now()}-${Math.random()}`);
|
|
187
192
|
const { mkdir, rm } = await import("node:fs/promises");
|
|
188
193
|
await mkdir(remotePath, { recursive: true });
|
|
189
194
|
git(["init", "-q", "--bare", "-b", "main"], remotePath);
|
|
190
|
-
git(["clone", "-q", remotePath, clonePath],
|
|
195
|
+
git(["clone", "-q", remotePath, clonePath], tmpdir());
|
|
191
196
|
git(["config", "user.email", "test@chant.dev"], clonePath);
|
|
192
197
|
git(["config", "user.name", "Test"], clonePath);
|
|
193
198
|
writeFileSync(join(clonePath, "README.md"), "fixture\n");
|
|
@@ -233,9 +238,9 @@ describe("lifecycle/git", () => {
|
|
|
233
238
|
test("concurrent write rejected: second push throws StaleLifecycleBranchError", async () => {
|
|
234
239
|
// Simulate two concurrent operators by setting up two clones of the same remote.
|
|
235
240
|
const { clonePath: cloneA, remotePath, cleanup } = await setupClonePair();
|
|
236
|
-
const cloneB = join(
|
|
241
|
+
const cloneB = join(tmpdir(), `chant-state-clone-b-${Date.now()}-${Math.random()}`);
|
|
237
242
|
try {
|
|
238
|
-
git(["clone", "-q", remotePath, cloneB],
|
|
243
|
+
git(["clone", "-q", remotePath, cloneB], tmpdir());
|
|
239
244
|
git(["config", "user.email", "test@chant.dev"], cloneB);
|
|
240
245
|
git(["config", "user.name", "Test"], cloneB);
|
|
241
246
|
|
|
@@ -129,6 +129,34 @@ describe("release-ledger", () => {
|
|
|
129
129
|
});
|
|
130
130
|
});
|
|
131
131
|
|
|
132
|
+
test("records an approver, distinct from the actor, for a gated change (#1035)", async () => {
|
|
133
|
+
await withTestDir(async (dir) => {
|
|
134
|
+
await initRepo(dir);
|
|
135
|
+
await appendReleaseRecord(
|
|
136
|
+
makeInput({ actor: "ci-bot", approver: "alice@corp" }),
|
|
137
|
+
{ cwd: dir },
|
|
138
|
+
);
|
|
139
|
+
const { records } = await readReleaseLedger("prod", { cwd: dir });
|
|
140
|
+
expect(records).toHaveLength(1);
|
|
141
|
+
expect(records[0].actor).toBe("ci-bot");
|
|
142
|
+
expect(records[0].approver).toBe("alice@corp");
|
|
143
|
+
// Separation of duties: approver is not just an echo of actor.
|
|
144
|
+
expect(records[0].approver).not.toBe(records[0].actor);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("approver is absent for an ungated change (#1035)", async () => {
|
|
149
|
+
await withTestDir(async (dir) => {
|
|
150
|
+
await initRepo(dir);
|
|
151
|
+
await appendReleaseRecord(makeInput(), { cwd: dir });
|
|
152
|
+
const { records } = await readReleaseLedger("prod", { cwd: dir });
|
|
153
|
+
expect(records).toHaveLength(1);
|
|
154
|
+
expect(records[0].approver).toBeUndefined();
|
|
155
|
+
// The optional approver never blocks a valid ungated record.
|
|
156
|
+
expect(validateReleaseRecord({ version: 1, ...makeInput() })).toEqual([]);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
132
160
|
test("release ledger and snapshot coexist on the same orphan branch without clobbering each other", async () => {
|
|
133
161
|
const { writeSnapshot, readSnapshot } = await import("./git");
|
|
134
162
|
await withTestDir(async (dir) => {
|
|
@@ -65,6 +65,17 @@ export interface ReleaseRecord {
|
|
|
65
65
|
timestamp: string;
|
|
66
66
|
/** Who/what triggered the deploy (a username, a CI actor, a service account). */
|
|
67
67
|
actor: string;
|
|
68
|
+
/**
|
|
69
|
+
* Who approved the change at its durable approval gate (#1035). Populated for
|
|
70
|
+
* a gated change — the same identity the approver supplied when they cleared
|
|
71
|
+
* the Temporal gate (`chant run signal ... --approver`, carried in the gate
|
|
72
|
+
* signal payload and thus the workflow history). Optional and omitted for an
|
|
73
|
+
* ungated change (nothing to approve), so `actor` alone answers "who made
|
|
74
|
+
* it" while `approver` answers "who approved it" only where a gate exists.
|
|
75
|
+
* Distinct from `actor`: the approver is deliberately a different person than
|
|
76
|
+
* the one who triggered the deploy (separation of duties).
|
|
77
|
+
*/
|
|
78
|
+
approver?: string;
|
|
68
79
|
/** Optional: the archive's own manifest digest (../components/verbs/build-archive.ts's `manifestDigest`), when the caller has it — lets a reader recover full build contents/provenance, not just the promoted image digest. */
|
|
69
80
|
manifestDigest?: string;
|
|
70
81
|
}
|
|
@@ -93,15 +93,22 @@ export interface ComponentCheck {
|
|
|
93
93
|
* error under the pseudo-check id "COMP000" rather than silently dropped —
|
|
94
94
|
* matching how `discoverComponents`'s caller (`../components/cli-support.ts`)
|
|
95
95
|
* always surfaces `result.errors` rather than ignoring them.
|
|
96
|
+
*
|
|
97
|
+
* `sandbox` (chant #1051, `chant lint --sandbox`) is threaded straight
|
|
98
|
+
* through to `discoverComponents` — `chant lint`'s own AST-only rule engine
|
|
99
|
+
* never executes project source (see this module's own doc comment), but
|
|
100
|
+
* COMP* checks exist precisely because they need the discovered `Component`
|
|
101
|
+
* graph, which does mean importing `*.component.ts` files.
|
|
96
102
|
*/
|
|
97
103
|
export async function runComponentChecks(
|
|
98
104
|
path: string,
|
|
99
105
|
checks: ComponentCheck[],
|
|
100
106
|
registryContext?: Pick<ComponentCheckContext, "knownKinds" | "rollbackPolicies">,
|
|
107
|
+
sandbox?: boolean,
|
|
101
108
|
): Promise<ComponentCheckDiagnostic[]> {
|
|
102
109
|
if (checks.length === 0) return [];
|
|
103
110
|
|
|
104
|
-
const result = await discoverComponents(path);
|
|
111
|
+
const result = await discoverComponents(path, { sandbox });
|
|
105
112
|
const diagnostics: ComponentCheckDiagnostic[] = [];
|
|
106
113
|
|
|
107
114
|
for (const err of result.errors) {
|