@intentius/chant 0.19.1 → 0.21.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 (164) 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 +71 -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/codegen/docs-types.d.ts +2 -0
  28. package/dist/codegen/docs-types.d.ts.map +1 -1
  29. package/dist/components/cli-support.d.ts +12 -5
  30. package/dist/components/cli-support.d.ts.map +1 -1
  31. package/dist/components/discover.d.ts +62 -7
  32. package/dist/components/discover.d.ts.map +1 -1
  33. package/dist/components/sandbox/driver.d.ts +12 -0
  34. package/dist/components/sandbox/driver.d.ts.map +1 -0
  35. package/dist/components/sandbox/run.d.ts +42 -0
  36. package/dist/components/sandbox/run.d.ts.map +1 -0
  37. package/dist/composite.d.ts +5 -0
  38. package/dist/composite.d.ts.map +1 -1
  39. package/dist/config.d.ts +71 -0
  40. package/dist/config.d.ts.map +1 -1
  41. package/dist/declarable.d.ts +16 -0
  42. package/dist/declarable.d.ts.map +1 -1
  43. package/dist/discovery/collect.d.ts.map +1 -1
  44. package/dist/discovery/entity-wire-codec.d.ts +166 -0
  45. package/dist/discovery/entity-wire-codec.d.ts.map +1 -0
  46. package/dist/discovery/entity-wire.d.ts +50 -0
  47. package/dist/discovery/entity-wire.d.ts.map +1 -0
  48. package/dist/discovery/fold-import.d.ts +239 -0
  49. package/dist/discovery/fold-import.d.ts.map +1 -0
  50. package/dist/discovery/index.d.ts +74 -1
  51. package/dist/discovery/index.d.ts.map +1 -1
  52. package/dist/discovery/sandbox/bundle.d.ts +18 -0
  53. package/dist/discovery/sandbox/bundle.d.ts.map +1 -0
  54. package/dist/discovery/sandbox/child-errors.d.ts +15 -0
  55. package/dist/discovery/sandbox/child-errors.d.ts.map +1 -0
  56. package/dist/discovery/sandbox/driver.d.ts +13 -0
  57. package/dist/discovery/sandbox/driver.d.ts.map +1 -0
  58. package/dist/discovery/sandbox/run.d.ts +69 -0
  59. package/dist/discovery/sandbox/run.d.ts.map +1 -0
  60. package/dist/errors.d.ts +9 -1
  61. package/dist/errors.d.ts.map +1 -1
  62. package/dist/fold/fold.d.ts +299 -0
  63. package/dist/fold/fold.d.ts.map +1 -0
  64. package/dist/fold/foldable-helpers.d.ts +121 -0
  65. package/dist/fold/foldable-helpers.d.ts.map +1 -0
  66. package/dist/fold/subset.d.ts +134 -0
  67. package/dist/fold/subset.d.ts.map +1 -0
  68. package/dist/index.d.ts +2 -0
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/lexicon-output.d.ts +7 -2
  71. package/dist/lexicon-output.d.ts.map +1 -1
  72. package/dist/lexicon-schema.d.ts +4 -2
  73. package/dist/lexicon-schema.d.ts.map +1 -1
  74. package/dist/lexicon.d.ts +131 -1
  75. package/dist/lexicon.d.ts.map +1 -1
  76. package/dist/lifecycle/release-ledger.d.ts +11 -0
  77. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  78. package/dist/lint/component-checks.d.ts +7 -1
  79. package/dist/lint/component-checks.d.ts.map +1 -1
  80. package/dist/lint/rules/evl001-non-literal-expression.d.ts.map +1 -1
  81. package/dist/lint/rules/evl003-dynamic-property-access.d.ts.map +1 -1
  82. package/dist/params.d.ts +60 -0
  83. package/dist/params.d.ts.map +1 -0
  84. package/dist/provenance.d.ts +21 -0
  85. package/dist/provenance.d.ts.map +1 -1
  86. package/dist/runtime.d.ts +10 -1
  87. package/dist/runtime.d.ts.map +1 -1
  88. package/package.json +2 -1
  89. package/src/build-params.test.ts +144 -0
  90. package/src/build-params.ts +207 -0
  91. package/src/build.test.ts +38 -0
  92. package/src/build.ts +144 -7
  93. package/src/cli/commands/build.test.ts +220 -2
  94. package/src/cli/commands/build.ts +111 -3
  95. package/src/cli/commands/check-lexicon-examples.test.ts +92 -0
  96. package/src/cli/commands/check-lexicon-examples.ts +103 -0
  97. package/src/cli/commands/check-lexicon-intrinsics.test.ts +179 -0
  98. package/src/cli/commands/check-lexicon-intrinsics.ts +348 -0
  99. package/src/cli/commands/check-lexicon.test.ts +34 -0
  100. package/src/cli/commands/check-lexicon.ts +137 -1
  101. package/src/cli/commands/lint.ts +31 -3
  102. package/src/cli/commands/onboard.ts +1 -1
  103. package/src/cli/component-security-boundary.test.ts +170 -0
  104. package/src/cli/handlers/build.ts +24 -3
  105. package/src/cli/handlers/components.ts +9 -2
  106. package/src/cli/handlers/dev.ts +1 -1
  107. package/src/cli/handlers/graph.ts +7 -5
  108. package/src/cli/handlers/lifecycle.ts +2 -2
  109. package/src/cli/handlers/lint.ts +2 -0
  110. package/src/cli/handlers/misc.ts +2 -2
  111. package/src/cli/handlers/run-client.ts +1 -1
  112. package/src/cli/handlers/run.ts +20 -5
  113. package/src/cli/main.test.ts +22 -0
  114. package/src/cli/main.ts +39 -0
  115. package/src/cli/plugins.ts +20 -4
  116. package/src/cli/registry.ts +10 -0
  117. package/src/cli/security-boundary.test.ts +135 -0
  118. package/src/codegen/docs-sections.test.ts +67 -0
  119. package/src/codegen/docs-sections.ts +7 -3
  120. package/src/codegen/docs-types.ts +2 -0
  121. package/src/components/cli-support.ts +22 -10
  122. package/src/components/discover.ts +127 -25
  123. package/src/components/sandbox/driver.ts +114 -0
  124. package/src/components/sandbox/run.test.ts +185 -0
  125. package/src/components/sandbox/run.ts +177 -0
  126. package/src/composite.test.ts +21 -0
  127. package/src/composite.ts +20 -1
  128. package/src/config.ts +81 -0
  129. package/src/declarable.ts +20 -0
  130. package/src/discovery/collect.ts +17 -3
  131. package/src/discovery/entity-wire-codec.ts +487 -0
  132. package/src/discovery/entity-wire.test.ts +240 -0
  133. package/src/discovery/entity-wire.ts +67 -0
  134. package/src/discovery/fold-import.test.ts +1598 -0
  135. package/src/discovery/fold-import.ts +1998 -0
  136. package/src/discovery/index.test.ts +191 -1
  137. package/src/discovery/index.ts +242 -1
  138. package/src/discovery/sandbox/bundle.ts +218 -0
  139. package/src/discovery/sandbox/child-errors.ts +65 -0
  140. package/src/discovery/sandbox/driver.ts +147 -0
  141. package/src/discovery/sandbox/run.test.ts +179 -0
  142. package/src/discovery/sandbox/run.ts +196 -0
  143. package/src/errors.ts +9 -1
  144. package/src/fold/fold.test.ts +812 -0
  145. package/src/fold/fold.ts +805 -0
  146. package/src/fold/foldable-helpers.ts +171 -0
  147. package/src/fold/subset-doc-parity.test.ts +210 -0
  148. package/src/fold/subset.test.ts +352 -0
  149. package/src/fold/subset.ts +383 -0
  150. package/src/index.ts +2 -0
  151. package/src/lexicon-output.ts +7 -2
  152. package/src/lexicon-schema.test.ts +57 -0
  153. package/src/lexicon-schema.ts +8 -1
  154. package/src/lexicon.ts +132 -1
  155. package/src/lifecycle/git.test.ts +10 -5
  156. package/src/lifecycle/release-ledger.test.ts +28 -0
  157. package/src/lifecycle/release-ledger.ts +11 -0
  158. package/src/lint/component-checks.ts +8 -1
  159. package/src/lint/rules/evl001-non-literal-expression.ts +26 -110
  160. package/src/lint/rules/evl003-dynamic-property-access.ts +11 -2
  161. package/src/params.test.ts +22 -0
  162. package/src/params.ts +66 -0
  163. package/src/provenance.ts +22 -0
  164. package/src/runtime.ts +11 -2
@@ -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) {
@@ -1,6 +1,7 @@
1
1
  import * as ts from "typescript";
2
2
  import type { LintRule, LintContext, LintDiagnostic } from "../rule";
3
3
  import { isInsideCompositeFactory } from "./composite-scope";
4
+ import { checkObjectMember } from "../../fold/subset";
4
5
 
5
6
  /**
6
7
  * EVL001: Non-Literal Expression in Resource Constructor
@@ -9,104 +10,20 @@ import { isInsideCompositeFactory } from "./composite-scope";
9
10
  * Allowed: literals, identifiers, property access, object/array literals,
10
11
  * template expressions, binary/unary/conditional, as/satisfies casts.
11
12
  * Blocked: function calls, method calls, and other dynamic expressions.
13
+ *
14
+ * This is a *diagnostic* (chant #1024, epic #1019) — it exists to give a
15
+ * friendly, early, precisely-located error before synthesis, over exactly
16
+ * the same subset definition `fold()` (the actual enforcement layer,
17
+ * {@link "../../fold/fold"}) uses to decide what it can reduce. Both import
18
+ * the single classifier in {@link "../../fold/subset"}
19
+ * ({@link checkObjectMember}) so a construct flagged here is exactly a
20
+ * construct `fold()` rejects, and vice versa — see that module's doc
21
+ * comment for the few environment-dependent exceptions (identifier
22
+ * resolution, intrinsic tag registration, spread-source runtime type,
23
+ * `&&`/`||`/`??`/`? :` short-circuit laziness) that can't be unified
24
+ * through shape alone.
12
25
  */
13
26
 
14
- function isStaticallyEvaluable(node: ts.Node): boolean {
15
- // Literals
16
- if (
17
- ts.isStringLiteral(node) ||
18
- ts.isNumericLiteral(node) ||
19
- ts.isNoSubstitutionTemplateLiteral(node) ||
20
- node.kind === ts.SyntaxKind.TrueKeyword ||
21
- node.kind === ts.SyntaxKind.FalseKeyword ||
22
- node.kind === ts.SyntaxKind.NullKeyword ||
23
- node.kind === ts.SyntaxKind.UndefinedKeyword
24
- ) {
25
- return true;
26
- }
27
-
28
- // Identifiers (variable references)
29
- if (ts.isIdentifier(node)) return true;
30
-
31
- // Property access: obj.prop
32
- if (ts.isPropertyAccessExpression(node)) {
33
- return isStaticallyEvaluable(node.expression);
34
- }
35
-
36
- // Element access with static key: obj["key"] or obj[0]
37
- if (ts.isElementAccessExpression(node)) {
38
- return (
39
- isStaticallyEvaluable(node.expression) &&
40
- (ts.isStringLiteral(node.argumentExpression) ||
41
- ts.isNumericLiteral(node.argumentExpression))
42
- );
43
- }
44
-
45
- // Object literals — check all property values
46
- if (ts.isObjectLiteralExpression(node)) {
47
- return node.properties.every((prop) => {
48
- if (ts.isPropertyAssignment(prop)) return isStaticallyEvaluable(prop.initializer);
49
- if (ts.isShorthandPropertyAssignment(prop)) return true;
50
- if (ts.isSpreadAssignment(prop)) return isStaticallyEvaluable(prop.expression);
51
- return false;
52
- });
53
- }
54
-
55
- // Array literals — check all elements
56
- if (ts.isArrayLiteralExpression(node)) {
57
- return node.elements.every((el) => isStaticallyEvaluable(el));
58
- }
59
-
60
- // Template expressions (tagged or untagged)
61
- if (ts.isTemplateExpression(node)) return true;
62
- if (ts.isTaggedTemplateExpression(node)) return true;
63
-
64
- // Binary expressions: a + b, a ?? b, etc.
65
- if (ts.isBinaryExpression(node)) {
66
- return isStaticallyEvaluable(node.left) && isStaticallyEvaluable(node.right);
67
- }
68
-
69
- // Prefix unary: !x, -x
70
- if (ts.isPrefixUnaryExpression(node)) {
71
- return isStaticallyEvaluable(node.operand);
72
- }
73
-
74
- // Conditional: a ? b : c
75
- if (ts.isConditionalExpression(node)) {
76
- return (
77
- isStaticallyEvaluable(node.condition) &&
78
- isStaticallyEvaluable(node.whenTrue) &&
79
- isStaticallyEvaluable(node.whenFalse)
80
- );
81
- }
82
-
83
- // Type assertions: x as T, x satisfies T
84
- if (ts.isAsExpression(node) || ts.isSatisfiesExpression(node)) {
85
- return isStaticallyEvaluable(node.expression);
86
- }
87
-
88
- // Parenthesized expression
89
- if (ts.isParenthesizedExpression(node)) {
90
- return isStaticallyEvaluable(node.expression);
91
- }
92
-
93
- // Non-null assertion: x!
94
- if (ts.isNonNullExpression(node)) {
95
- return isStaticallyEvaluable(node.expression);
96
- }
97
-
98
- // Spread element in arrays
99
- if (ts.isSpreadElement(node)) {
100
- return isStaticallyEvaluable(node.expression);
101
- }
102
-
103
- // new Expression (resource constructors) — allowed as property values
104
- if (ts.isNewExpression(node)) return true;
105
-
106
- // Everything else (call expressions, etc.) is not statically evaluable
107
- return false;
108
- }
109
-
110
27
  function checkNode(node: ts.Node, context: LintContext, diagnostics: LintDiagnostic[]): void {
111
28
  // Skip resource constructors inside Composite() factory callbacks
112
29
  if (ts.isNewExpression(node) && !isInsideCompositeFactory(node)) {
@@ -114,20 +31,19 @@ function checkNode(node: ts.Node, context: LintContext, diagnostics: LintDiagnos
114
31
  const firstArg = node.arguments[0];
115
32
  if (ts.isObjectLiteralExpression(firstArg)) {
116
33
  for (const prop of firstArg.properties) {
117
- if (ts.isPropertyAssignment(prop)) {
118
- if (!isStaticallyEvaluable(prop.initializer)) {
119
- const { line, character } = context.sourceFile.getLineAndCharacterOfPosition(
120
- prop.initializer.getStart(context.sourceFile),
121
- );
122
- diagnostics.push({
123
- file: context.filePath,
124
- line: line + 1,
125
- column: character + 1,
126
- ruleId: "EVL001",
127
- severity: "error",
128
- message: `Non-literal expression in resource constructor property — value must be statically evaluable`,
129
- });
130
- }
34
+ const violation = checkObjectMember(prop);
35
+ if (violation) {
36
+ const { line, character } = context.sourceFile.getLineAndCharacterOfPosition(
37
+ violation.node.getStart(context.sourceFile),
38
+ );
39
+ diagnostics.push({
40
+ file: context.filePath,
41
+ line: line + 1,
42
+ column: character + 1,
43
+ ruleId: "EVL001",
44
+ severity: "error",
45
+ message: `Non-literal expression in resource constructor property — value must be statically evaluable`,
46
+ });
131
47
  }
132
48
  }
133
49
  }
@@ -1,19 +1,28 @@
1
1
  import * as ts from "typescript";
2
2
  import type { LintRule, LintContext, LintDiagnostic } from "../rule";
3
+ import { isLiteralElementKey } from "../../fold/subset";
3
4
 
4
5
  /**
5
6
  * EVL003: Dynamic Property Access
6
7
  *
7
8
  * Computed property access (obj[key]) must use a string or numeric literal key.
8
9
  * Dynamic keys (variables, expressions) are not statically evaluable.
10
+ *
11
+ * Shares its key-shape predicate with `fold()`'s `elementKey()`
12
+ * ({@link "../../fold/fold"}) via {@link "../../fold/subset"} (#1024) — a
13
+ * dynamic element-access key is exactly the construct `fold()` rejects with
14
+ * `FoldError.ruleId === "EVL003"`, and the diagnostic is located at the key
15
+ * itself (not the whole `obj[key]` access), matching where `fold()`'s
16
+ * `elementKey()` throws — so a rejection and its EVL003 diagnostic cite the
17
+ * same position, not just the same rule id.
9
18
  */
10
19
 
11
20
  function checkNode(node: ts.Node, context: LintContext, diagnostics: LintDiagnostic[]): void {
12
21
  if (ts.isElementAccessExpression(node)) {
13
22
  const arg = node.argumentExpression;
14
- if (!ts.isStringLiteral(arg) && !ts.isNumericLiteral(arg)) {
23
+ if (!isLiteralElementKey(arg)) {
15
24
  const { line, character } = context.sourceFile.getLineAndCharacterOfPosition(
16
- node.getStart(context.sourceFile),
25
+ arg.getStart(context.sourceFile),
17
26
  );
18
27
  diagnostics.push({
19
28
  file: context.filePath,
@@ -0,0 +1,22 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { params, setBuildParams } from "./params";
3
+
4
+ describe("params — the shared build-time-parameters object", () => {
5
+ test("starts empty", () => {
6
+ setBuildParams({});
7
+ expect(params).toEqual({});
8
+ });
9
+
10
+ test("setBuildParams mutates the SAME object in place, never rebinds it", () => {
11
+ const before = params;
12
+ setBuildParams({ tier: "production", replicas: 3, enabled: true });
13
+ expect(params).toBe(before);
14
+ expect(params).toEqual({ tier: "production", replicas: 3, enabled: true });
15
+ });
16
+
17
+ test("a second call fully replaces the previous values (no stale leftover keys)", () => {
18
+ setBuildParams({ a: "1", b: "2" });
19
+ setBuildParams({ c: "3" });
20
+ expect(params).toEqual({ c: "3" });
21
+ });
22
+ });
package/src/params.ts ADDED
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Build-time parameters — the runtime binding source references (chant #1064,
3
+ * follow-up to epic #1019's fold work).
4
+ *
5
+ * This is deliberately NOT `Parameter` (`lexicons/aws/src/parameter.ts`), which
6
+ * is a deploy-time CloudFormation parameter: it emits a `Parameters:` block and
7
+ * resolves when the STACK deploys. A build-time parameter resolves before the
8
+ * template is even synthesized — its value can change WHICH resources are
9
+ * produced at all (loomster's `LOOM_TIER` selecting `light` vs `production` vs
10
+ * `production-ha`), which a deploy-time `Parameter` structurally cannot do.
11
+ *
12
+ * Project source never reads `process.env` directly to vary a build — that
13
+ * reads ambient state at module-evaluation time, which `fold()` correctly
14
+ * cannot reduce to a value (see ../fold/fold.ts). Instead it imports this
15
+ * module's `params` object:
16
+ *
17
+ * ```ts
18
+ * import { params } from "@intentius/chant/params";
19
+ * export const tier = params.tier as Tier;
20
+ * ```
21
+ *
22
+ * `params` is declared, validated (type + optional `enum`), and resolved by
23
+ * the CLI BEFORE discovery ever touches a project file (../build-params.ts's
24
+ * `resolveBuildParams`, driven by `chant build --param`/`--params-file`/a
25
+ * declared `env` mapping/`chant.config.ts`'s `buildParams` defaults). Two
26
+ * consumers read the resolved values, and both see the identical object:
27
+ *
28
+ * - The FOLD path (../discovery/fold-import.ts's `buildExternals`) recognizes
29
+ * a named `params` import resolving to *this* module and substitutes the
30
+ * already-resolved values directly, with zero import performed — so
31
+ * `params.tier` folds to a LITERAL, not a symbolic node. This is the entire
32
+ * point: the value is known at build invocation, so there is nothing left
33
+ * to defer.
34
+ * - The RUN path (a run-fallback file that imports this module for real)
35
+ * gets the exact same values, because `setBuildParams` below is called
36
+ * once, in-process, before ANY project file is imported or folded —
37
+ * mutating this shared object in place rather than replacing the binding,
38
+ * so a live-imported reference always observes the current build's values.
39
+ */
40
+
41
+ import type { BuildParamValue } from "./build-params";
42
+
43
+ export type { BuildParamValue };
44
+
45
+ /**
46
+ * The current build's resolved parameter values, keyed by declared name.
47
+ * Empty until a build populates it via {@link setBuildParams}. Mutated IN
48
+ * PLACE (never reassigned) so a real `import { params }` in a run-fallback
49
+ * file — which captures this exact object reference at import time — always
50
+ * observes whatever the current build resolved, even though the import
51
+ * happens after this module was first loaded.
52
+ */
53
+ export const params: Record<string, BuildParamValue> = {};
54
+
55
+ /**
56
+ * Replace the shared {@link params} object's contents in place. Called once
57
+ * per build (../discovery/index.ts's `discover()`), before any project file
58
+ * is imported or folded, with the CLI/config-resolved values
59
+ * (../build-params.ts's `resolveBuildParams`). Never called from project
60
+ * source — this is chant's own build-invocation plumbing, not a public
61
+ * authoring API.
62
+ */
63
+ export function setBuildParams(values: Readonly<Record<string, BuildParamValue>>): void {
64
+ for (const key of Object.keys(params)) delete params[key];
65
+ Object.assign(params, values);
66
+ }
package/src/provenance.ts CHANGED
@@ -8,6 +8,12 @@
8
8
  * This is entity-level provenance (which file declared it, and which composite
9
9
  * expanded it), not a YAML-line source map. It answers "where did this resource
10
10
  * come from?", which is the question an agent asks before changing it.
11
+ *
12
+ * {@link BuildParamProvenance} (chant #1064) is the other half: not "where did
13
+ * this ENTITY come from" but "what INPUTS was this whole BUILD invoked with" —
14
+ * the question ambient `process.env` reads used to answer invisibly. See
15
+ * ../build-params.ts for declaration/resolution and ../build.ts's
16
+ * `BuildResult.buildParams` for where a build surfaces it.
11
17
  */
12
18
 
13
19
  const PROVENANCE = Symbol.for("chant.provenance");
@@ -52,3 +58,19 @@ export function setProvenance(entity: object, prov: EntityProvenance): void {
52
58
  export function getProvenance(entity: object): EntityProvenance | undefined {
53
59
  return (entity as Record<symbol, unknown>)[PROVENANCE] as EntityProvenance | undefined;
54
60
  }
61
+
62
+ /**
63
+ * One resolved build-time parameter (chant #1064, see ../build-params.ts): its
64
+ * final value and which source won it, so a build's parameter inputs are
65
+ * auditable rather than inferred. `source` records precedence, most to least
66
+ * specific: an explicit `--param`/`--params-file` value beats a declared `env`
67
+ * mapping, which beats `chant.config.ts`'s `default`.
68
+ */
69
+ export interface BuildParamProvenance {
70
+ /** The declared parameter name (a key of `chant.config.ts`'s `buildParams`). */
71
+ name: string;
72
+ /** The resolved, type-coerced value actually bound to `params.<name>` for this build. */
73
+ value: string | number | boolean;
74
+ /** Which input supplied the value. */
75
+ source: "cli" | "params-file" | "env" | "default";
76
+ }
package/src/runtime.ts CHANGED
@@ -54,18 +54,27 @@ export function createResource(
54
54
 
55
55
  /**
56
56
  * Create a property-kind class for a given property type.
57
+ *
58
+ * Instances carry DECLARABLE_MARKER/lexicon/entityType/kind at runtime (set
59
+ * via defineProperty below), so they already satisfy `Declarable` — the
60
+ * return type just needs to say so. Before this, the signature was
61
+ * `Record<string, unknown>` with no `Declarable`, so any composite that
62
+ * returned a property-kind instance as a top-level member only type-checked
63
+ * by accident: either the caller went through an untyped `require()` (losing
64
+ * the type entirely) or never assigned the instance where its `Declarable`-ness
65
+ * was checked statically. See chant #1068.
57
66
  */
58
67
  export function createProperty(
59
68
  type: string,
60
69
  lexicon: string,
61
- ): new (props: Record<string, unknown>) => Record<string, unknown> {
70
+ ): new (props: Record<string, unknown>) => Declarable & Record<string, unknown> {
62
71
  const PropertyClass = function (this: Record<string, unknown>, props: Record<string, unknown>) {
63
72
  Object.defineProperty(this, DECLARABLE_MARKER, { value: true, enumerable: false });
64
73
  Object.defineProperty(this, "lexicon", { value: lexicon, enumerable: false });
65
74
  Object.defineProperty(this, "entityType", { value: type, enumerable: false });
66
75
  Object.defineProperty(this, "kind", { value: "property", enumerable: false });
67
76
  Object.defineProperty(this, "props", { value: props ?? {}, enumerable: false, configurable: true });
68
- } as unknown as new (props: Record<string, unknown>) => Record<string, unknown>;
77
+ } as unknown as new (props: Record<string, unknown>) => Declarable & Record<string, unknown>;
69
78
 
70
79
  Object.defineProperty(PropertyClass, "name", { value: type.split(".").pop() ?? type });
71
80