@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.
Files changed (152) hide show
  1. package/dist/build-params.d.ts +108 -0
  2. package/dist/build-params.d.ts.map +1 -0
  3. package/dist/build.d.ts +79 -1
  4. package/dist/build.d.ts.map +1 -1
  5. package/dist/cli/commands/build.d.ts +31 -0
  6. package/dist/cli/commands/build.d.ts.map +1 -1
  7. package/dist/cli/commands/check-lexicon-examples.d.ts +41 -0
  8. package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -0
  9. package/dist/cli/commands/check-lexicon-intrinsics.d.ts +54 -0
  10. package/dist/cli/commands/check-lexicon-intrinsics.d.ts.map +1 -0
  11. package/dist/cli/commands/check-lexicon.d.ts +1 -1
  12. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  13. package/dist/cli/commands/lint.d.ts +27 -0
  14. package/dist/cli/commands/lint.d.ts.map +1 -1
  15. package/dist/cli/handlers/build.d.ts.map +1 -1
  16. package/dist/cli/handlers/components.d.ts.map +1 -1
  17. package/dist/cli/handlers/lint.d.ts.map +1 -1
  18. package/dist/cli/handlers/run-client.d.ts +1 -1
  19. package/dist/cli/handlers/run-client.d.ts.map +1 -1
  20. package/dist/cli/handlers/run.d.ts.map +1 -1
  21. package/dist/cli/main.d.ts.map +1 -1
  22. package/dist/cli/plugins.d.ts +16 -0
  23. package/dist/cli/plugins.d.ts.map +1 -1
  24. package/dist/cli/registry.d.ts +10 -0
  25. package/dist/cli/registry.d.ts.map +1 -1
  26. package/dist/codegen/docs-sections.d.ts.map +1 -1
  27. package/dist/components/cli-support.d.ts +12 -5
  28. package/dist/components/cli-support.d.ts.map +1 -1
  29. package/dist/components/discover.d.ts +62 -7
  30. package/dist/components/discover.d.ts.map +1 -1
  31. package/dist/components/sandbox/driver.d.ts +12 -0
  32. package/dist/components/sandbox/driver.d.ts.map +1 -0
  33. package/dist/components/sandbox/run.d.ts +42 -0
  34. package/dist/components/sandbox/run.d.ts.map +1 -0
  35. package/dist/composite.d.ts +5 -0
  36. package/dist/composite.d.ts.map +1 -1
  37. package/dist/config.d.ts +71 -0
  38. package/dist/config.d.ts.map +1 -1
  39. package/dist/discovery/collect.d.ts.map +1 -1
  40. package/dist/discovery/entity-wire-codec.d.ts +166 -0
  41. package/dist/discovery/entity-wire-codec.d.ts.map +1 -0
  42. package/dist/discovery/entity-wire.d.ts +50 -0
  43. package/dist/discovery/entity-wire.d.ts.map +1 -0
  44. package/dist/discovery/fold-import.d.ts +210 -0
  45. package/dist/discovery/fold-import.d.ts.map +1 -0
  46. package/dist/discovery/index.d.ts +74 -1
  47. package/dist/discovery/index.d.ts.map +1 -1
  48. package/dist/discovery/sandbox/bundle.d.ts +18 -0
  49. package/dist/discovery/sandbox/bundle.d.ts.map +1 -0
  50. package/dist/discovery/sandbox/child-errors.d.ts +15 -0
  51. package/dist/discovery/sandbox/child-errors.d.ts.map +1 -0
  52. package/dist/discovery/sandbox/driver.d.ts +13 -0
  53. package/dist/discovery/sandbox/driver.d.ts.map +1 -0
  54. package/dist/discovery/sandbox/run.d.ts +69 -0
  55. package/dist/discovery/sandbox/run.d.ts.map +1 -0
  56. package/dist/errors.d.ts +9 -1
  57. package/dist/errors.d.ts.map +1 -1
  58. package/dist/fold/fold.d.ts +226 -0
  59. package/dist/fold/fold.d.ts.map +1 -0
  60. package/dist/fold/subset.d.ts +102 -0
  61. package/dist/fold/subset.d.ts.map +1 -0
  62. package/dist/index.d.ts +2 -0
  63. package/dist/index.d.ts.map +1 -1
  64. package/dist/lexicon-output.d.ts +7 -2
  65. package/dist/lexicon-output.d.ts.map +1 -1
  66. package/dist/lexicon-schema.d.ts +2 -2
  67. package/dist/lexicon-schema.d.ts.map +1 -1
  68. package/dist/lexicon.d.ts +81 -1
  69. package/dist/lexicon.d.ts.map +1 -1
  70. package/dist/lifecycle/release-ledger.d.ts +11 -0
  71. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  72. package/dist/lint/component-checks.d.ts +7 -1
  73. package/dist/lint/component-checks.d.ts.map +1 -1
  74. package/dist/lint/rules/evl001-non-literal-expression.d.ts.map +1 -1
  75. package/dist/lint/rules/evl003-dynamic-property-access.d.ts.map +1 -1
  76. package/dist/params.d.ts +60 -0
  77. package/dist/params.d.ts.map +1 -0
  78. package/dist/provenance.d.ts +21 -0
  79. package/dist/provenance.d.ts.map +1 -1
  80. package/package.json +2 -1
  81. package/src/build-params.test.ts +144 -0
  82. package/src/build-params.ts +207 -0
  83. package/src/build.test.ts +38 -0
  84. package/src/build.ts +144 -7
  85. package/src/cli/commands/build.test.ts +220 -2
  86. package/src/cli/commands/build.ts +111 -3
  87. package/src/cli/commands/check-lexicon-examples.test.ts +92 -0
  88. package/src/cli/commands/check-lexicon-examples.ts +103 -0
  89. package/src/cli/commands/check-lexicon-intrinsics.test.ts +145 -0
  90. package/src/cli/commands/check-lexicon-intrinsics.ts +312 -0
  91. package/src/cli/commands/check-lexicon.test.ts +34 -0
  92. package/src/cli/commands/check-lexicon.ts +119 -1
  93. package/src/cli/commands/lint.ts +31 -3
  94. package/src/cli/commands/onboard.ts +1 -1
  95. package/src/cli/component-security-boundary.test.ts +170 -0
  96. package/src/cli/handlers/build.ts +24 -3
  97. package/src/cli/handlers/components.ts +9 -2
  98. package/src/cli/handlers/dev.ts +1 -1
  99. package/src/cli/handlers/graph.ts +7 -5
  100. package/src/cli/handlers/lifecycle.ts +2 -2
  101. package/src/cli/handlers/lint.ts +2 -0
  102. package/src/cli/handlers/misc.ts +2 -2
  103. package/src/cli/handlers/run-client.ts +1 -1
  104. package/src/cli/handlers/run.ts +20 -5
  105. package/src/cli/main.test.ts +22 -0
  106. package/src/cli/main.ts +39 -0
  107. package/src/cli/plugins.ts +20 -4
  108. package/src/cli/registry.ts +10 -0
  109. package/src/cli/security-boundary.test.ts +135 -0
  110. package/src/codegen/docs-sections.test.ts +61 -0
  111. package/src/codegen/docs-sections.ts +7 -3
  112. package/src/components/cli-support.ts +22 -10
  113. package/src/components/discover.ts +127 -25
  114. package/src/components/sandbox/driver.ts +114 -0
  115. package/src/components/sandbox/run.test.ts +185 -0
  116. package/src/components/sandbox/run.ts +177 -0
  117. package/src/composite.test.ts +21 -0
  118. package/src/composite.ts +20 -1
  119. package/src/config.ts +81 -0
  120. package/src/discovery/collect.ts +17 -3
  121. package/src/discovery/entity-wire-codec.ts +485 -0
  122. package/src/discovery/entity-wire.test.ts +240 -0
  123. package/src/discovery/entity-wire.ts +67 -0
  124. package/src/discovery/fold-import.test.ts +1026 -0
  125. package/src/discovery/fold-import.ts +1805 -0
  126. package/src/discovery/index.test.ts +191 -1
  127. package/src/discovery/index.ts +233 -1
  128. package/src/discovery/sandbox/bundle.ts +218 -0
  129. package/src/discovery/sandbox/child-errors.ts +65 -0
  130. package/src/discovery/sandbox/driver.ts +147 -0
  131. package/src/discovery/sandbox/run.test.ts +179 -0
  132. package/src/discovery/sandbox/run.ts +196 -0
  133. package/src/errors.ts +9 -1
  134. package/src/fold/fold.test.ts +535 -0
  135. package/src/fold/fold.ts +648 -0
  136. package/src/fold/subset-doc-parity.test.ts +183 -0
  137. package/src/fold/subset.test.ts +241 -0
  138. package/src/fold/subset.ts +302 -0
  139. package/src/index.ts +2 -0
  140. package/src/lexicon-output.ts +7 -2
  141. package/src/lexicon-schema.test.ts +14 -0
  142. package/src/lexicon-schema.ts +3 -1
  143. package/src/lexicon.ts +82 -1
  144. package/src/lifecycle/git.test.ts +10 -5
  145. package/src/lifecycle/release-ledger.test.ts +28 -0
  146. package/src/lifecycle/release-ledger.ts +11 -0
  147. package/src/lint/component-checks.ts +8 -1
  148. package/src/lint/rules/evl001-non-literal-expression.ts +26 -110
  149. package/src/lint/rules/evl003-dynamic-property-access.ts +11 -2
  150. package/src/params.test.ts +22 -0
  151. package/src/params.ts +66 -0
  152. 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";
@@ -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
- /** @internal Intrinsic value when constructed from an Intrinsic rather than AttrRef */
49
- private readonly _intrinsic: Intrinsic | null;
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
  // ---------------------------------------------------------------------------
@@ -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
- isTag: z.boolean().optional(),
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
- readonly isTag?: boolean;
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
- const remotePath = join(import.meta.dirname ?? "/tmp", `chant-state-remote-${Date.now()}-${Math.random()}`);
186
- const clonePath = join(import.meta.dirname ?? "/tmp", `chant-state-clone-${Date.now()}-${Math.random()}`);
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], import.meta.dirname ?? "/tmp");
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(import.meta.dirname ?? "/tmp", `chant-state-clone-b-${Date.now()}-${Math.random()}`);
241
+ const cloneB = join(tmpdir(), `chant-state-clone-b-${Date.now()}-${Math.random()}`);
237
242
  try {
238
- git(["clone", "-q", remotePath, cloneB], import.meta.dirname ?? "/tmp");
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) {