@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.
- 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 +71 -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/codegen/docs-types.d.ts +2 -0
- package/dist/codegen/docs-types.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/declarable.d.ts +16 -0
- package/dist/declarable.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 +239 -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 +299 -0
- package/dist/fold/fold.d.ts.map +1 -0
- package/dist/fold/foldable-helpers.d.ts +121 -0
- package/dist/fold/foldable-helpers.d.ts.map +1 -0
- package/dist/fold/subset.d.ts +134 -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 +4 -2
- package/dist/lexicon-schema.d.ts.map +1 -1
- package/dist/lexicon.d.ts +131 -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/dist/runtime.d.ts +10 -1
- package/dist/runtime.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 +179 -0
- package/src/cli/commands/check-lexicon-intrinsics.ts +348 -0
- package/src/cli/commands/check-lexicon.test.ts +34 -0
- package/src/cli/commands/check-lexicon.ts +137 -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 +67 -0
- package/src/codegen/docs-sections.ts +7 -3
- package/src/codegen/docs-types.ts +2 -0
- 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/declarable.ts +20 -0
- package/src/discovery/collect.ts +17 -3
- package/src/discovery/entity-wire-codec.ts +487 -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 +1598 -0
- package/src/discovery/fold-import.ts +1998 -0
- package/src/discovery/index.test.ts +191 -1
- package/src/discovery/index.ts +242 -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 +812 -0
- package/src/fold/fold.ts +805 -0
- package/src/fold/foldable-helpers.ts +171 -0
- package/src/fold/subset-doc-parity.test.ts +210 -0
- package/src/fold/subset.test.ts +352 -0
- package/src/fold/subset.ts +383 -0
- package/src/index.ts +2 -0
- package/src/lexicon-output.ts +7 -2
- package/src/lexicon-schema.test.ts +57 -0
- package/src/lexicon-schema.ts +8 -1
- package/src/lexicon.ts +132 -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
- 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
|
-
|
|
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) {
|
|
@@ -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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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 (!
|
|
23
|
+
if (!isLiteralElementKey(arg)) {
|
|
15
24
|
const { line, character } = context.sourceFile.getLineAndCharacterOfPosition(
|
|
16
|
-
|
|
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
|
|