@intentius/chant 0.19.1 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-params.d.ts +108 -0
- package/dist/build-params.d.ts.map +1 -0
- package/dist/build.d.ts +79 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts +31 -0
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-examples.d.ts +41 -0
- package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -0
- package/dist/cli/commands/check-lexicon-intrinsics.d.ts +54 -0
- package/dist/cli/commands/check-lexicon-intrinsics.d.ts.map +1 -0
- package/dist/cli/commands/check-lexicon.d.ts +1 -1
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts +27 -0
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/components.d.ts.map +1 -1
- package/dist/cli/handlers/lint.d.ts.map +1 -1
- package/dist/cli/handlers/run-client.d.ts +1 -1
- package/dist/cli/handlers/run-client.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts +16 -0
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +10 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/codegen/docs-sections.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +12 -5
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/discover.d.ts +62 -7
- package/dist/components/discover.d.ts.map +1 -1
- package/dist/components/sandbox/driver.d.ts +12 -0
- package/dist/components/sandbox/driver.d.ts.map +1 -0
- package/dist/components/sandbox/run.d.ts +42 -0
- package/dist/components/sandbox/run.d.ts.map +1 -0
- package/dist/composite.d.ts +5 -0
- package/dist/composite.d.ts.map +1 -1
- package/dist/config.d.ts +71 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/discovery/collect.d.ts.map +1 -1
- package/dist/discovery/entity-wire-codec.d.ts +166 -0
- package/dist/discovery/entity-wire-codec.d.ts.map +1 -0
- package/dist/discovery/entity-wire.d.ts +50 -0
- package/dist/discovery/entity-wire.d.ts.map +1 -0
- package/dist/discovery/fold-import.d.ts +210 -0
- package/dist/discovery/fold-import.d.ts.map +1 -0
- package/dist/discovery/index.d.ts +74 -1
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/discovery/sandbox/bundle.d.ts +18 -0
- package/dist/discovery/sandbox/bundle.d.ts.map +1 -0
- package/dist/discovery/sandbox/child-errors.d.ts +15 -0
- package/dist/discovery/sandbox/child-errors.d.ts.map +1 -0
- package/dist/discovery/sandbox/driver.d.ts +13 -0
- package/dist/discovery/sandbox/driver.d.ts.map +1 -0
- package/dist/discovery/sandbox/run.d.ts +69 -0
- package/dist/discovery/sandbox/run.d.ts.map +1 -0
- package/dist/errors.d.ts +9 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/fold/fold.d.ts +226 -0
- package/dist/fold/fold.d.ts.map +1 -0
- package/dist/fold/subset.d.ts +102 -0
- package/dist/fold/subset.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/lexicon-output.d.ts +7 -2
- package/dist/lexicon-output.d.ts.map +1 -1
- package/dist/lexicon-schema.d.ts +2 -2
- package/dist/lexicon-schema.d.ts.map +1 -1
- package/dist/lexicon.d.ts +81 -1
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/release-ledger.d.ts +11 -0
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lint/component-checks.d.ts +7 -1
- package/dist/lint/component-checks.d.ts.map +1 -1
- package/dist/lint/rules/evl001-non-literal-expression.d.ts.map +1 -1
- package/dist/lint/rules/evl003-dynamic-property-access.d.ts.map +1 -1
- package/dist/params.d.ts +60 -0
- package/dist/params.d.ts.map +1 -0
- package/dist/provenance.d.ts +21 -0
- package/dist/provenance.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/build-params.test.ts +144 -0
- package/src/build-params.ts +207 -0
- package/src/build.test.ts +38 -0
- package/src/build.ts +144 -7
- package/src/cli/commands/build.test.ts +220 -2
- package/src/cli/commands/build.ts +111 -3
- package/src/cli/commands/check-lexicon-examples.test.ts +92 -0
- package/src/cli/commands/check-lexicon-examples.ts +103 -0
- package/src/cli/commands/check-lexicon-intrinsics.test.ts +145 -0
- package/src/cli/commands/check-lexicon-intrinsics.ts +312 -0
- package/src/cli/commands/check-lexicon.test.ts +34 -0
- package/src/cli/commands/check-lexicon.ts +119 -1
- package/src/cli/commands/lint.ts +31 -3
- package/src/cli/commands/onboard.ts +1 -1
- package/src/cli/component-security-boundary.test.ts +170 -0
- package/src/cli/handlers/build.ts +24 -3
- package/src/cli/handlers/components.ts +9 -2
- package/src/cli/handlers/dev.ts +1 -1
- package/src/cli/handlers/graph.ts +7 -5
- package/src/cli/handlers/lifecycle.ts +2 -2
- package/src/cli/handlers/lint.ts +2 -0
- package/src/cli/handlers/misc.ts +2 -2
- package/src/cli/handlers/run-client.ts +1 -1
- package/src/cli/handlers/run.ts +20 -5
- package/src/cli/main.test.ts +22 -0
- package/src/cli/main.ts +39 -0
- package/src/cli/plugins.ts +20 -4
- package/src/cli/registry.ts +10 -0
- package/src/cli/security-boundary.test.ts +135 -0
- package/src/codegen/docs-sections.test.ts +61 -0
- package/src/codegen/docs-sections.ts +7 -3
- package/src/components/cli-support.ts +22 -10
- package/src/components/discover.ts +127 -25
- package/src/components/sandbox/driver.ts +114 -0
- package/src/components/sandbox/run.test.ts +185 -0
- package/src/components/sandbox/run.ts +177 -0
- package/src/composite.test.ts +21 -0
- package/src/composite.ts +20 -1
- package/src/config.ts +81 -0
- package/src/discovery/collect.ts +17 -3
- package/src/discovery/entity-wire-codec.ts +485 -0
- package/src/discovery/entity-wire.test.ts +240 -0
- package/src/discovery/entity-wire.ts +67 -0
- package/src/discovery/fold-import.test.ts +1026 -0
- package/src/discovery/fold-import.ts +1805 -0
- package/src/discovery/index.test.ts +191 -1
- package/src/discovery/index.ts +233 -1
- package/src/discovery/sandbox/bundle.ts +218 -0
- package/src/discovery/sandbox/child-errors.ts +65 -0
- package/src/discovery/sandbox/driver.ts +147 -0
- package/src/discovery/sandbox/run.test.ts +179 -0
- package/src/discovery/sandbox/run.ts +196 -0
- package/src/errors.ts +9 -1
- package/src/fold/fold.test.ts +535 -0
- package/src/fold/fold.ts +648 -0
- package/src/fold/subset-doc-parity.test.ts +183 -0
- package/src/fold/subset.test.ts +241 -0
- package/src/fold/subset.ts +302 -0
- package/src/index.ts +2 -0
- package/src/lexicon-output.ts +7 -2
- package/src/lexicon-schema.test.ts +14 -0
- package/src/lexicon-schema.ts +3 -1
- package/src/lexicon.ts +82 -1
- package/src/lifecycle/git.test.ts +10 -5
- package/src/lifecycle/release-ledger.test.ts +28 -0
- package/src/lifecycle/release-ledger.ts +11 -0
- package/src/lint/component-checks.ts +8 -1
- package/src/lint/rules/evl001-non-literal-expression.ts +26 -110
- package/src/lint/rules/evl003-dynamic-property-access.ts +11 -2
- package/src/params.test.ts +22 -0
- package/src/params.ts +66 -0
- package/src/provenance.ts +22 -0
package/src/cli/handlers/run.ts
CHANGED
|
@@ -176,7 +176,7 @@ async function runComponentsList(ctx: CommandContext): Promise<number> {
|
|
|
176
176
|
if (!requireTemporalMode(ctx, "chant run list --components")) return 1;
|
|
177
177
|
|
|
178
178
|
const projectPath = resolve(".");
|
|
179
|
-
const result = await listComponents(projectPath);
|
|
179
|
+
const result = await listComponents(projectPath, ctx.args.sandbox);
|
|
180
180
|
|
|
181
181
|
if (!result.success) {
|
|
182
182
|
for (const err of result.errors) console.error(formatError({ message: err }));
|
|
@@ -442,17 +442,27 @@ export async function runOpSignal(ctx: CommandContext): Promise<number> {
|
|
|
442
442
|
|
|
443
443
|
const projectPath = resolve(".");
|
|
444
444
|
const workflowId = ctx.args.components ? componentWorkflowId(name) : resolveWorkflowId(name);
|
|
445
|
+
// Approver identity for an approval gate (#1035): supplied via --approver, or
|
|
446
|
+
// resolved from the CI/user environment the same way `chant components
|
|
447
|
+
// release` resolves --actor. Rides the gate signal payload so Temporal
|
|
448
|
+
// persists "who approved" in the workflow history. Optional — a signal that
|
|
449
|
+
// is not an approval gate (or an approver who declines to identify) sends no
|
|
450
|
+
// payload and the gate still clears.
|
|
451
|
+
const approver = ctx.args.approver ?? process.env.GITHUB_ACTOR ?? process.env.GITLAB_USER_LOGIN ?? process.env.USER;
|
|
445
452
|
let handle: WorkflowHandleRaw;
|
|
446
453
|
try {
|
|
447
454
|
const { client } = await makeTemporalClient(ctx.args.profile, projectPath);
|
|
448
455
|
handle = client.workflow.getHandle(workflowId);
|
|
449
|
-
await handle.signal(signalName);
|
|
456
|
+
await handle.signal(signalName, ...(approver ? [{ approver }] : []));
|
|
450
457
|
} catch (err) {
|
|
451
458
|
console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
|
|
452
459
|
return 1;
|
|
453
460
|
}
|
|
454
461
|
|
|
455
|
-
console.error(formatSuccess(
|
|
462
|
+
console.error(formatSuccess(
|
|
463
|
+
`Signal "${signalName}" sent to ${ctx.args.components ? "component" : "Op"} "${name}"` +
|
|
464
|
+
(approver ? ` (approver: ${approver})` : ""),
|
|
465
|
+
));
|
|
456
466
|
return 0;
|
|
457
467
|
}
|
|
458
468
|
|
|
@@ -694,7 +704,12 @@ export async function runOpComponents(ctx: CommandContext): Promise<number> {
|
|
|
694
704
|
// `undefined` and every `onProgress?.(...)` call in the driver is a no-op —
|
|
695
705
|
// behavior is byte-for-byte unchanged from before this flag existed.
|
|
696
706
|
const onProgress = ctx.args.progressJson ? ndjsonProgressSink() : undefined;
|
|
697
|
-
const result = await runComponents(resolve("."), selector, {
|
|
707
|
+
const result = await runComponents(resolve("."), selector, {
|
|
708
|
+
env: ctx.args.env,
|
|
709
|
+
componentOutputs: seededOutputs,
|
|
710
|
+
onProgress,
|
|
711
|
+
sandbox: ctx.args.sandbox,
|
|
712
|
+
});
|
|
698
713
|
|
|
699
714
|
// Dump the accumulated outputs for a downstream job to seed from. Written
|
|
700
715
|
// even on failure (partial outputs) so a resumed run still has what completed.
|
|
@@ -767,7 +782,7 @@ async function runComponentTemporal(ctx: CommandContext, selector: string): Prom
|
|
|
767
782
|
}
|
|
768
783
|
|
|
769
784
|
const projectPath = resolve(".");
|
|
770
|
-
const resolved = await resolveComponentTargets(projectPath, selector);
|
|
785
|
+
const resolved = await resolveComponentTargets(projectPath, selector, ctx.args.sandbox);
|
|
771
786
|
if (!resolved.success || resolved.targets.length === 0) {
|
|
772
787
|
console.error(formatError({ message: resolved.error ?? `Component "${selector}" not found` }));
|
|
773
788
|
return 1;
|
package/src/cli/main.test.ts
CHANGED
|
@@ -248,6 +248,28 @@ describe("parseArgs", () => {
|
|
|
248
248
|
const result = parseArgs(["run", "--components", "search-service"]);
|
|
249
249
|
expect(result.progressJson).toBeUndefined();
|
|
250
250
|
});
|
|
251
|
+
|
|
252
|
+
// ── --param / --params-file (chant #1064) ────────────────────────────────
|
|
253
|
+
|
|
254
|
+
test("parses a single --param as a one-element array", () => {
|
|
255
|
+
const result = parseArgs(["build", "src", "--param", "tier=production"]);
|
|
256
|
+
expect(result.param).toEqual(["tier=production"]);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("repeated --param accumulates in order", () => {
|
|
260
|
+
const result = parseArgs(["build", "src", "--param", "tier=production", "--param", "env=staging"]);
|
|
261
|
+
expect(result.param).toEqual(["tier=production", "env=staging"]);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("--param is undefined when omitted", () => {
|
|
265
|
+
const result = parseArgs(["build", "src"]);
|
|
266
|
+
expect(result.param).toBeUndefined();
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("parses --params-file with a path", () => {
|
|
270
|
+
const result = parseArgs(["build", "src", "--params-file", "./params.json"]);
|
|
271
|
+
expect(result.paramsFile).toBe("./params.json");
|
|
272
|
+
});
|
|
251
273
|
});
|
|
252
274
|
|
|
253
275
|
// ── resolveCommand tests ──────────────────────────────────────────
|
package/src/cli/main.ts
CHANGED
|
@@ -45,6 +45,8 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
45
45
|
verbose: false,
|
|
46
46
|
help: false,
|
|
47
47
|
profile: undefined,
|
|
48
|
+
param: undefined,
|
|
49
|
+
paramsFile: undefined,
|
|
48
50
|
report: undefined,
|
|
49
51
|
local: undefined,
|
|
50
52
|
temporal: undefined,
|
|
@@ -210,10 +212,20 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
210
212
|
result.runId = args[++i];
|
|
211
213
|
} else if (arg === "--actor") {
|
|
212
214
|
result.actor = args[++i];
|
|
215
|
+
} else if (arg === "--approver") {
|
|
216
|
+
result.approver = args[++i];
|
|
213
217
|
} else if (arg === "--compare-to") {
|
|
214
218
|
result.compareTo = args[++i];
|
|
215
219
|
} else if (arg === "--no-release-record") {
|
|
216
220
|
result.noReleaseRecord = true;
|
|
221
|
+
} else if (arg === "--fold") {
|
|
222
|
+
result.fold = true;
|
|
223
|
+
} else if (arg === "--sandbox") {
|
|
224
|
+
result.sandbox = true;
|
|
225
|
+
} else if (arg === "--param") {
|
|
226
|
+
(result.param ??= []).push(args[++i]);
|
|
227
|
+
} else if (arg === "--params-file") {
|
|
228
|
+
result.paramsFile = args[++i];
|
|
217
229
|
} else if (!arg.startsWith("-")) {
|
|
218
230
|
if (!result.command) {
|
|
219
231
|
result.command = arg;
|
|
@@ -403,12 +415,39 @@ Options:
|
|
|
403
415
|
successful \`run --components\` deploy (default: on;
|
|
404
416
|
also settable via chant.config.ts's
|
|
405
417
|
release.autoRecord: false; #597)
|
|
418
|
+
--fold (build) Fold source modules statically instead of
|
|
419
|
+
running them; folds resource constructors and
|
|
420
|
+
composite factory calls (#1022/#1023), falling back
|
|
421
|
+
to run per-file for anything else outside the fold
|
|
422
|
+
subset (a cross-file-only reference, a re-export,
|
|
423
|
+
\`export default\`, ...). Logs which path each file
|
|
424
|
+
took. Default: off (also settable via
|
|
425
|
+
chant.config.ts's build.fold: true; #1022)
|
|
426
|
+
--sandbox (build) Run run-fallback source files (or every
|
|
427
|
+
file, without --fold) together, isolated, in one
|
|
428
|
+
sandboxed child process instead of in-process
|
|
429
|
+
(#1045). No filesystem write, no child process, no
|
|
430
|
+
worker threads, no ambient environment visible to
|
|
431
|
+
project source; network egress is NOT blocked (see
|
|
432
|
+
docs). Default: off (also settable via
|
|
433
|
+
chant.config.ts's build.sandbox: true; #1045)
|
|
434
|
+
--param <name=value> (build) Bind a declared build-time parameter
|
|
435
|
+
(chant.config.ts's buildParams) to a value, for
|
|
436
|
+
source to read as params.<name> (#1064) instead of
|
|
437
|
+
process.env — repeatable. Distinct from the AWS
|
|
438
|
+
lexicon's deploy-time Parameter(): this resolves
|
|
439
|
+
before synthesis, so it can change which resources
|
|
440
|
+
are produced at all. Highest precedence.
|
|
441
|
+
--params-file <path> (build) JSON file of { "name": value } build-time
|
|
442
|
+
parameter values (#1064). Second precedence, after
|
|
443
|
+
--param.
|
|
406
444
|
|
|
407
445
|
Examples:
|
|
408
446
|
chant build ./infra/
|
|
409
447
|
chant build ./infra/ --output stack.json
|
|
410
448
|
chant build ./infra/ --format yaml
|
|
411
449
|
chant build ./infra/ --watch
|
|
450
|
+
chant build ./infra/ --fold
|
|
412
451
|
chant build ./infra/ --components --generate gitlab
|
|
413
452
|
chant build ./infra/ --components --generate gitlab --output .gitlab-ci.yml
|
|
414
453
|
chant run --components search-service --env staging
|
package/src/cli/plugins.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isLexiconPlugin, type LexiconPlugin } from "../lexicon";
|
|
2
2
|
import { loadChantConfig } from "../config";
|
|
3
|
-
import {
|
|
3
|
+
import { findInfraFiles, detectLexicons } from "../index";
|
|
4
4
|
import { checkConflicts } from "./conflict-check";
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -80,6 +80,22 @@ export async function loadPlugins(lexiconNames: string[]): Promise<LexiconPlugin
|
|
|
80
80
|
*
|
|
81
81
|
* Reads `lexicons` from `chant.config.ts` / `chant.config.json` if present.
|
|
82
82
|
* Falls back to source-file detection via `detectLexicons()`.
|
|
83
|
+
*
|
|
84
|
+
* chant #1045 Phase 2 — this used to fall back to a full `discover()` call
|
|
85
|
+
* just to read its `sourceFiles` list, which imports and RUNS every project
|
|
86
|
+
* source file to collect entities that are then discarded here. That ran
|
|
87
|
+
* unconditionally, on every command that reaches this function without an
|
|
88
|
+
* explicit `chant.config.ts` `lexicons` list (`build`, `lint`, `doctor`,
|
|
89
|
+
* `import`, `graph`, the MCP server — effectively the whole CLI), with no
|
|
90
|
+
* `--fold`/`--sandbox` involved at all: a project could omit `lexicons` and
|
|
91
|
+
* every file would execute here, in the CLI's own process, before the
|
|
92
|
+
* caller's OWN (possibly folded, possibly sandboxed) build ever ran. Fixed
|
|
93
|
+
* by using `findInfraFiles()` alone — the pure directory walk `discover()`
|
|
94
|
+
* itself starts from, no import, no execution — since `detectLexicons()` is
|
|
95
|
+
* already a plain text/regex scan over file contents (`../detectLexicon.ts`)
|
|
96
|
+
* that never needed live module exports to begin with. This is strictly
|
|
97
|
+
* better than routing the detection through the sandbox: it removes the
|
|
98
|
+
* execution entirely rather than containing it, at no bundling/spawn cost.
|
|
83
99
|
*/
|
|
84
100
|
export async function resolveProjectLexicons(projectPath: string): Promise<string[]> {
|
|
85
101
|
const { config } = await loadChantConfig(projectPath);
|
|
@@ -88,7 +104,7 @@ export async function resolveProjectLexicons(projectPath: string): Promise<strin
|
|
|
88
104
|
return config.lexicons;
|
|
89
105
|
}
|
|
90
106
|
|
|
91
|
-
// Fallback: detect from source imports
|
|
92
|
-
const
|
|
93
|
-
return detectLexicons(
|
|
107
|
+
// Fallback: detect from source imports — a pure text scan, no execution.
|
|
108
|
+
const files = await findInfraFiles(projectPath);
|
|
109
|
+
return detectLexicons(files);
|
|
94
110
|
}
|
package/src/cli/registry.ts
CHANGED
|
@@ -136,6 +136,8 @@ export interface ParsedArgs {
|
|
|
136
136
|
runId?: string;
|
|
137
137
|
/** `chant components release record --actor <name>` (#568) — who/what triggered the deploy. */
|
|
138
138
|
actor?: string;
|
|
139
|
+
/** `--approver <name>` (#1035) — who approved a gated change. Supplied to `chant run signal` (rides the gate signal payload into workflow history) and to `chant components release` (recorded on the release ledger). Optional; absent for ungated changes. */
|
|
140
|
+
approver?: string;
|
|
139
141
|
/** `chant components status <env> --compare-to <env>` (#568) — a second environment to cross-check the same component's recorded digest against. */
|
|
140
142
|
compareTo?: string;
|
|
141
143
|
/** `chant run --components <name> --env <env> --no-release-record` (#597) — opt out of auto-emitting a release-ledger record after a successful component deploy. Default (flag omitted): recording is ON. Also settable project-wide via `chant.config.ts`'s `release.autoRecord: false`. */
|
|
@@ -144,6 +146,14 @@ export interface ParsedArgs {
|
|
|
144
146
|
dumpOutputs?: string;
|
|
145
147
|
/** `chant run --components <name> --seed-outputs <file>` (repeatable) — before the run, load each JSON outputs file (as written by `--dump-outputs`) and seed cross-component/cross-stack resolution with it, so a `stackOutput()`/`@<dep>.publish.*` reference to a component that ran in an earlier job resolves. */
|
|
146
148
|
seedOutputs?: string[];
|
|
149
|
+
/** `chant build --fold` (#1022/#1023, epic #1019) — opt-in: fold source modules statically instead of importing/running them; folds resource constructors and composite factory calls, falling back to run per-file for anything the folder can't represent (a cross-file-only reference, a re-export, `export default`, …). Also settable project-wide via `chant.config.ts`'s `build.fold: true`; the flag always wins when set. Default (flag omitted): the existing run path, unchanged. */
|
|
150
|
+
fold?: boolean;
|
|
151
|
+
/** `chant build --sandbox` (#1045 Phase 2) — opt-in: run-fallback source files (or every file, without `--fold`) execute together, isolated, in one sandboxed child process instead of in-process. Also settable project-wide via `chant.config.ts`'s `build.sandbox: true`; the flag always wins when set. Default (flag omitted): in-process execution, unchanged. */
|
|
152
|
+
sandbox?: boolean;
|
|
153
|
+
/** `chant build --param name=value` (#1064) — repeatable. Bound to `params.<name>` (`@intentius/chant/params`) for source to reference, after validation against `chant.config.ts`'s declared `buildParams`. Highest precedence over `--params-file`/a declared `env` mapping/the declared `default`. */
|
|
154
|
+
param?: string[];
|
|
155
|
+
/** `chant build --params-file <path>` (#1064) — a JSON file of `{ "name": value }` build-time parameter values. Second precedence, after `--param`. */
|
|
156
|
+
paramsFile?: string;
|
|
147
157
|
}
|
|
148
158
|
|
|
149
159
|
/**
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { mkdir, writeFile, rm, symlink, realpath } from "node:fs/promises";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { join, dirname, resolve } from "node:path";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* chant #1045 Phase 2 (post-merge finding) — regression test for a real
|
|
11
|
+
* bypass: `resolveProjectLexicons` (`./plugins.ts`) used to fall back to a
|
|
12
|
+
* full, unsandboxed `discover()` call whenever a project's `chant.config.ts`
|
|
13
|
+
* didn't list `lexicons` explicitly — importing and RUNNING every source
|
|
14
|
+
* file in the CLI's own process, with no `--fold`/`--sandbox` involved at
|
|
15
|
+
* all, before the caller's own (possibly folded, possibly sandboxed)
|
|
16
|
+
* discovery ever ran. `resolveProjectLexicons` sits underneath nearly every
|
|
17
|
+
* CLI command (`build`, `lint`, `doctor`, `import`, `graph`, the MCP server),
|
|
18
|
+
* so this reached far past `chant build --fold`.
|
|
19
|
+
*
|
|
20
|
+
* This is deliberately an end-to-end test of the real CLI entry point
|
|
21
|
+
* (`main.ts`, spawned as a genuine child process — the same way `bin/chant`
|
|
22
|
+
* invokes it), NOT a call to `build()`/`buildCommand()` directly: calling
|
|
23
|
+
* either of those skips the exact code path the bug lived in
|
|
24
|
+
* (`main.ts`'s `loadPluginsOrExit` → `resolveProjectLexicons`), which is
|
|
25
|
+
* precisely how the corpus differential and Phase 2's other tests — all of
|
|
26
|
+
* which call `build()` — passed while this stayed open.
|
|
27
|
+
*
|
|
28
|
+
* Fixture lives in a fresh tmpdir per test, never in the source tree.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
32
|
+
const mainTsPath = resolve(thisDir, "main.ts");
|
|
33
|
+
const repoRoot = resolve(thisDir, "../../../..");
|
|
34
|
+
|
|
35
|
+
describe("CLI end-to-end — resolveProjectLexicons must not execute project source", () => {
|
|
36
|
+
let testDir: string;
|
|
37
|
+
let exfilMarkerPath: string;
|
|
38
|
+
|
|
39
|
+
beforeEach(async () => {
|
|
40
|
+
testDir = await realpath(
|
|
41
|
+
await (async () => {
|
|
42
|
+
const dir = join(tmpdir(), `chant-security-boundary-test-${Date.now()}-${Math.random()}`);
|
|
43
|
+
await mkdir(join(dir, "src"), { recursive: true });
|
|
44
|
+
return dir;
|
|
45
|
+
})(),
|
|
46
|
+
);
|
|
47
|
+
exfilMarkerPath = join(testDir, "exfil-happened.txt");
|
|
48
|
+
|
|
49
|
+
// A real `node_modules/@intentius/chant-lexicon-k8s` symlink — same shape
|
|
50
|
+
// this monorepo's own workspace linking produces — so the fixture's bare
|
|
51
|
+
// `from "@intentius/chant-lexicon-k8s"` import resolves exactly the way a
|
|
52
|
+
// real installed project's would, and `detectLexicons`'s text scan (which
|
|
53
|
+
// matches that literal specifier) has something genuine to find.
|
|
54
|
+
await mkdir(join(testDir, "node_modules", "@intentius"), { recursive: true });
|
|
55
|
+
await symlink(
|
|
56
|
+
join(repoRoot, "lexicons", "k8s"),
|
|
57
|
+
join(testDir, "node_modules", "@intentius", "chant-lexicon-k8s"),
|
|
58
|
+
"dir",
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// No `lexicons` field — the exact condition that sends
|
|
62
|
+
// `resolveProjectLexicons` down the detection fallback.
|
|
63
|
+
await writeFile(join(testDir, "chant.config.ts"), "export default {};\n");
|
|
64
|
+
|
|
65
|
+
// A hostile source file: reads a file outside the project and the
|
|
66
|
+
// ambient environment at module top level (before its one Declarable
|
|
67
|
+
// export), and — if it got to run — writes a marker file so the test can
|
|
68
|
+
// tell "executed" apart from "folded" without depending on stdout
|
|
69
|
+
// capture alone.
|
|
70
|
+
await writeFile(
|
|
71
|
+
join(testDir, "src", "infra.ts"),
|
|
72
|
+
[
|
|
73
|
+
'import { readFileSync, writeFileSync } from "node:fs";',
|
|
74
|
+
'import { Namespace } from "@intentius/chant-lexicon-k8s";',
|
|
75
|
+
"",
|
|
76
|
+
"const stolen = readFileSync(\"/etc/hosts\", \"utf-8\");",
|
|
77
|
+
`writeFileSync(${JSON.stringify(exfilMarkerPath)}, "exfil: " + stolen.slice(0, 20) + " env=" + Object.keys(process.env).length);`,
|
|
78
|
+
'console.error(">>> EXFIL " + stolen.slice(0, 20) + " env=" + Object.keys(process.env).length);',
|
|
79
|
+
"",
|
|
80
|
+
'export const ns = new Namespace({ metadata: { name: "x" } });',
|
|
81
|
+
"",
|
|
82
|
+
].join("\n"),
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
afterEach(async () => {
|
|
87
|
+
await rm(testDir, { recursive: true, force: true });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
/** Spawn the real CLI (`main.ts`, exactly as `bin/chant` invokes it via `npx tsx`) and capture its output. */
|
|
91
|
+
function runCli(args: string[]): { status: number | null; stdout: string; stderr: string } {
|
|
92
|
+
const result = spawnSync("npx", ["tsx", mainTsPath, ...args], {
|
|
93
|
+
cwd: repoRoot,
|
|
94
|
+
encoding: "utf-8",
|
|
95
|
+
timeout: 30_000,
|
|
96
|
+
env: { ...process.env, CHANT_SECRET_FOR_TEST: "should-never-leak" },
|
|
97
|
+
});
|
|
98
|
+
return { status: result.status, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
test("`chant build --fold` on a project with no `lexicons` config does not execute the hostile file", () => {
|
|
102
|
+
const { status, stdout, stderr } = runCli(["build", testDir, "--fold"]);
|
|
103
|
+
|
|
104
|
+
expect(stderr).not.toMatch(/EXFIL/);
|
|
105
|
+
expect(stdout).not.toMatch(/EXFIL/);
|
|
106
|
+
expect(existsSync(exfilMarkerPath), "hostile file wrote its marker — it executed").toBe(false);
|
|
107
|
+
|
|
108
|
+
// The build still has to actually work — this isn't proving safety by
|
|
109
|
+
// making the build fail outright.
|
|
110
|
+
expect(status).toBe(0);
|
|
111
|
+
expect(stdout).toContain("kind: Namespace");
|
|
112
|
+
|
|
113
|
+
// The fold decision for the hostile file should say what's now true:
|
|
114
|
+
// this file really did fold with zero module execution.
|
|
115
|
+
expect(stderr).toMatch(/\[fold:fold\] src\/infra\.ts/);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("`chant build --fold --sandbox` also does not execute the hostile file (both layers closed)", () => {
|
|
119
|
+
const { status, stdout, stderr } = runCli(["build", testDir, "--fold", "--sandbox"]);
|
|
120
|
+
|
|
121
|
+
expect(stderr).not.toMatch(/EXFIL/);
|
|
122
|
+
expect(stdout).not.toMatch(/EXFIL/);
|
|
123
|
+
expect(existsSync(exfilMarkerPath)).toBe(false);
|
|
124
|
+
expect(status).toBe(0);
|
|
125
|
+
expect(stdout).toContain("kind: Namespace");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("`chant lint` (which also resolves lexicons this way) does not execute the hostile file", () => {
|
|
129
|
+
const { stdout, stderr } = runCli(["lint", testDir]);
|
|
130
|
+
|
|
131
|
+
expect(stderr).not.toMatch(/EXFIL/);
|
|
132
|
+
expect(stdout).not.toMatch(/EXFIL/);
|
|
133
|
+
expect(existsSync(exfilMarkerPath), "hostile file wrote its marker — it executed").toBe(false);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { generateIntrinsics } from "./docs-sections";
|
|
3
|
+
import { intrinsicFolds } from "../lexicon";
|
|
4
|
+
import type { DocsConfig, ManifestJSON } from "./docs-types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* chant #1062 (epic #1019) — the per-lexicon intrinsic foldability matrix.
|
|
8
|
+
* `generateIntrinsics`'s "Folds?" column must come from {@link intrinsicFolds}
|
|
9
|
+
* (`../lexicon.ts`), the SAME predicate `fold()` uses to decide whether a
|
|
10
|
+
* registered tag actually folds — not a second `isTag`-shaped guess that
|
|
11
|
+
* could silently disagree with it. This test locks in that the column
|
|
12
|
+
* exists, has the right value per intrinsic, and tracks `intrinsicFolds`
|
|
13
|
+
* rather than restating its logic.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const config: DocsConfig = {
|
|
17
|
+
name: "test",
|
|
18
|
+
displayName: "Test",
|
|
19
|
+
description: "A test lexicon",
|
|
20
|
+
distDir: "/tmp/does-not-matter",
|
|
21
|
+
outDir: "/tmp/does-not-matter",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function manifestWith(intrinsics: ManifestJSON["intrinsics"]): ManifestJSON {
|
|
25
|
+
return { name: "test", version: "0.0.0", intrinsics };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("generateIntrinsics — Folds? column", () => {
|
|
29
|
+
test("a registered tagged-template intrinsic folds", () => {
|
|
30
|
+
const page = generateIntrinsics(config, manifestWith([{ name: "Sub", isTag: true }]));
|
|
31
|
+
expect(page).toContain("| Function | Description | Output Key | Tag? | Folds? |");
|
|
32
|
+
expect(page).toContain("| `Sub` | — | `Sub` | Yes | Yes |");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("a plain-call intrinsic does not fold today", () => {
|
|
36
|
+
const page = generateIntrinsics(config, manifestWith([{ name: "Ref", isTag: false }]));
|
|
37
|
+
expect(page).toContain("| `Ref` | — | `Ref` | No | No |");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("an intrinsic with isTag omitted does not fold (same as isTag: false)", () => {
|
|
41
|
+
const page = generateIntrinsics(config, manifestWith([{ name: "reference", outputKey: "!reference" }]));
|
|
42
|
+
expect(page).toContain("| `reference` | — | `!reference` | No | No |");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("Folds? always matches intrinsicFolds() for every row, never a restated copy", () => {
|
|
46
|
+
const intrinsics: NonNullable<ManifestJSON["intrinsics"]> = [
|
|
47
|
+
{ name: "Sub", isTag: true },
|
|
48
|
+
{ name: "Ref" },
|
|
49
|
+
{ name: "GetAtt", isTag: false },
|
|
50
|
+
];
|
|
51
|
+
const page = generateIntrinsics(config, manifestWith(intrinsics));
|
|
52
|
+
for (const fn of intrinsics) {
|
|
53
|
+
const expected = intrinsicFolds(fn) ? "Yes" : "No";
|
|
54
|
+
const row = page.split("\n").find((line) => line.startsWith(`| \`${fn.name}\` |`));
|
|
55
|
+
expect(row, `no row rendered for ${fn.name}`).toBeDefined();
|
|
56
|
+
const cells = (row as string).split("|").map((c) => c.trim());
|
|
57
|
+
// | `name` | description | outputKey | Tag? | Folds? | → ["", "`name`", desc, key, tag, folds, ""]
|
|
58
|
+
expect(cells[5]).toBe(expected);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { escapeMdx } from "./docs-file-markers";
|
|
8
|
+
import { intrinsicFolds } from "../lexicon";
|
|
8
9
|
import type { DocsConfig, ManifestJSON, MetaEntry, RuleMeta } from "./docs-types";
|
|
9
10
|
|
|
10
11
|
export function generateOverview(
|
|
@@ -94,14 +95,17 @@ export function generateIntrinsics(
|
|
|
94
95
|
"",
|
|
95
96
|
`The ${config.displayName} lexicon provides **${intrinsics.length}** intrinsic functions.`,
|
|
96
97
|
"",
|
|
97
|
-
|
|
98
|
-
"
|
|
98
|
+
`**Tag?** shows how an intrinsic is authored: a genuine tagged template (\`Sub\\\`...\\\`\`) or a plain function call (\`Ref(...)\`). **Folds?** shows whether [\`chant build --fold\`](/chant/concepts/typescript-as-data/#folded-vs-run) can reduce a call to this intrinsic today, without running the file — generated directly from this lexicon's registration, not restated by hand. Right now folding only reaches tagged templates, so most rows read \`No\`; that reflects the current state of the folder, not something specific to these intrinsics, and it changes as folding gains coverage.`,
|
|
99
|
+
"",
|
|
100
|
+
"| Function | Description | Output Key | Tag? | Folds? |",
|
|
101
|
+
"|----------|-------------|------------|------|--------|",
|
|
99
102
|
];
|
|
100
103
|
|
|
101
104
|
for (const fn of intrinsics) {
|
|
102
105
|
const tag = fn.isTag ? "Yes" : "No";
|
|
106
|
+
const folds = intrinsicFolds(fn) ? "Yes" : "No";
|
|
103
107
|
lines.push(
|
|
104
|
-
`| \`${fn.name}\` | ${escapeMdx(fn.description ?? "—")} | \`${fn.outputKey ?? fn.name}\` | ${tag} |`,
|
|
108
|
+
`| \`${fn.name}\` | ${escapeMdx(fn.description ?? "—")} | \`${fn.outputKey ?? fn.name}\` | ${tag} | ${folds} |`,
|
|
105
109
|
);
|
|
106
110
|
}
|
|
107
111
|
|
|
@@ -61,8 +61,8 @@ export interface ListComponentsResult {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
/** Discover components under `path` and shape them for `chant list --components`. */
|
|
64
|
-
export async function listComponents(path: string): Promise<ListComponentsResult> {
|
|
65
|
-
const result = await discoverComponents(path);
|
|
64
|
+
export async function listComponents(path: string, sandbox?: boolean): Promise<ListComponentsResult> {
|
|
65
|
+
const result = await discoverComponents(path, { sandbox });
|
|
66
66
|
if (result.errors.length > 0) {
|
|
67
67
|
return { success: false, components: [], errors: result.errors.map((e) => e.message) };
|
|
68
68
|
}
|
|
@@ -100,8 +100,8 @@ export interface DescribeComponentResult {
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
/** Find and project one named component, for `chant describe <name> --components`. */
|
|
103
|
-
export async function describeComponent(path: string, name: string): Promise<DescribeComponentResult> {
|
|
104
|
-
const result = await discoverComponents(path);
|
|
103
|
+
export async function describeComponent(path: string, name: string, sandbox?: boolean): Promise<DescribeComponentResult> {
|
|
104
|
+
const result = await discoverComponents(path, { sandbox });
|
|
105
105
|
if (result.errors.length > 0) {
|
|
106
106
|
return { success: false, component: name, output: result.errors.map((e) => e.message).join("\n") };
|
|
107
107
|
}
|
|
@@ -139,8 +139,8 @@ export interface ComponentGraphResult {
|
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
/** Compute the components' dependency graph under `path`, for `chant graph --components`. */
|
|
142
|
-
export async function computeComponentGraph(path: string): Promise<ComponentGraphResult> {
|
|
143
|
-
const result = await discoverComponents(path);
|
|
142
|
+
export async function computeComponentGraph(path: string, sandbox?: boolean): Promise<ComponentGraphResult> {
|
|
143
|
+
const result = await discoverComponents(path, { sandbox });
|
|
144
144
|
if (result.errors.length > 0) {
|
|
145
145
|
return { success: false, order: [], waves: [], edges: [], error: result.errors.map((e) => e.message).join("\n") };
|
|
146
146
|
}
|
|
@@ -220,6 +220,7 @@ export async function generateComponentsPipeline(
|
|
|
220
220
|
path: string,
|
|
221
221
|
lexicon: GenerateLexicon,
|
|
222
222
|
options?: ComponentPipelineOptions,
|
|
223
|
+
sandbox?: boolean,
|
|
223
224
|
): Promise<GenerateComponentsResult> {
|
|
224
225
|
const plugin = await loadLexiconPlugin(lexicon);
|
|
225
226
|
if (!plugin?.generateComponentPipeline) {
|
|
@@ -229,7 +230,7 @@ export async function generateComponentsPipeline(
|
|
|
229
230
|
};
|
|
230
231
|
}
|
|
231
232
|
|
|
232
|
-
const result = await discoverComponents(path);
|
|
233
|
+
const result = await discoverComponents(path, { sandbox });
|
|
233
234
|
if (result.errors.length > 0) {
|
|
234
235
|
return { success: false, error: result.errors.map((e) => e.message).join("\n") };
|
|
235
236
|
}
|
|
@@ -295,6 +296,13 @@ export function findComponentGate(component: DriverComponent): { signalName: str
|
|
|
295
296
|
export interface RunComponentsOptions {
|
|
296
297
|
/** Target environment name, threaded into every capability's `DeployContext.env` (default: "local"). */
|
|
297
298
|
env?: string;
|
|
299
|
+
/**
|
|
300
|
+
* chant #1051 — opt-in: discover `*.component.ts` files in a sandboxed
|
|
301
|
+
* child process (`chant run --components <name|all> --sandbox`) instead of
|
|
302
|
+
* in the CLI's own process. See `discoverComponents`'s `sandbox` option
|
|
303
|
+
* (../discover.ts).
|
|
304
|
+
*/
|
|
305
|
+
sandbox?: boolean;
|
|
298
306
|
/** Additional capability plugin package names to load on top of the built-in starter set (see `buildCapabilityRegistry`). */
|
|
299
307
|
capabilityPlugins?: string[];
|
|
300
308
|
/**
|
|
@@ -392,8 +400,12 @@ export interface ResolvedComponentTargets {
|
|
|
392
400
|
* without also inheriting the local executor's pre-flight gate rejection
|
|
393
401
|
* (gates are exactly what the durable path exists to support).
|
|
394
402
|
*/
|
|
395
|
-
export async function resolveComponentTargets(
|
|
396
|
-
|
|
403
|
+
export async function resolveComponentTargets(
|
|
404
|
+
path: string,
|
|
405
|
+
selector: string,
|
|
406
|
+
sandbox?: boolean,
|
|
407
|
+
): Promise<ResolvedComponentTargets> {
|
|
408
|
+
const result = await discoverComponents(path, { sandbox });
|
|
397
409
|
if (result.errors.length > 0) {
|
|
398
410
|
return { success: false, targets: [], error: result.errors.map((e) => e.message).join("\n") };
|
|
399
411
|
}
|
|
@@ -421,7 +433,7 @@ export async function runComponents(
|
|
|
421
433
|
selector: string,
|
|
422
434
|
options: RunComponentsOptions = {},
|
|
423
435
|
): Promise<RunComponentsResult> {
|
|
424
|
-
const resolved = await resolveComponentTargets(path, selector);
|
|
436
|
+
const resolved = await resolveComponentTargets(path, selector, options.sandbox);
|
|
425
437
|
if (!resolved.success) {
|
|
426
438
|
return { success: false, selected: [], error: resolved.error };
|
|
427
439
|
}
|