@intentius/chant 0.26.0 → 0.28.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/cli/commands/build.d.ts +6 -5
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/config.d.ts +13 -10
- package/dist/config.d.ts.map +1 -1
- package/dist/discovery/sandbox/config-run.d.ts.map +1 -1
- package/dist/discovery/sandbox/fork.d.ts +25 -0
- package/dist/discovery/sandbox/fork.d.ts.map +1 -1
- package/dist/discovery/sandbox/policy-run.d.ts.map +1 -1
- package/dist/discovery/sandbox/run.d.ts.map +1 -1
- package/dist/kubectl-context.d.ts +73 -0
- package/dist/kubectl-context.d.ts.map +1 -0
- package/dist/lint/config.d.ts +80 -0
- package/dist/lint/config.d.ts.map +1 -1
- package/dist/lint/policy.d.ts +8 -2
- package/dist/lint/policy.d.ts.map +1 -1
- package/dist/lint/post-synth.d.ts +18 -1
- package/dist/lint/post-synth.d.ts.map +1 -1
- package/dist/stack-output.d.ts +9 -4
- package/dist/stack-output.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/build.test.ts +206 -2
- package/src/cli/commands/build.ts +43 -10
- package/src/cli/commands/lint.ts +17 -25
- package/src/cli/main.test.ts +6 -0
- package/src/cli/main.ts +10 -2
- package/src/config.test.ts +28 -1
- package/src/config.ts +16 -12
- package/src/discovery/sandbox/config-boundary.test.ts +55 -1
- package/src/discovery/sandbox/config-run.ts +3 -0
- package/src/discovery/sandbox/fork.ts +75 -1
- package/src/discovery/sandbox/policy-boundary.test.ts +56 -1
- package/src/discovery/sandbox/policy-run.ts +15 -1
- package/src/discovery/sandbox/run.test.ts +85 -1
- package/src/discovery/sandbox/run.ts +3 -0
- package/src/kubectl-context.test.ts +94 -0
- package/src/kubectl-context.ts +126 -0
- package/src/lint/config.test.ts +93 -1
- package/src/lint/config.ts +108 -0
- package/src/lint/policy.test.ts +90 -0
- package/src/lint/policy.ts +17 -5
- package/src/lint/post-synth.test.ts +4 -0
- package/src/lint/post-synth.ts +30 -1
- package/src/stack-output.test.ts +21 -3
- package/src/stack-output.ts +21 -9
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { describe, test, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
2
|
import { buildCommand, resolveBuildFormat, type BuildOptions } from "./build";
|
|
3
3
|
import type { Serializer } from "../../serializer";
|
|
4
|
+
import type { LexiconPlugin } from "../../lexicon";
|
|
4
5
|
import { parseYAML } from "../../yaml";
|
|
5
6
|
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
6
7
|
import { existsSync, readFileSync } from "node:fs";
|
|
7
8
|
import { join, dirname, resolve as resolvePath } from "node:path";
|
|
8
9
|
import { tmpdir } from "node:os";
|
|
9
10
|
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { resetSandboxPolicyExecutionForTests } from "../../lint/policy-sandbox";
|
|
10
12
|
|
|
11
13
|
describe("buildCommand", () => {
|
|
12
14
|
let testDir: string;
|
|
@@ -322,7 +324,7 @@ export const testEntity = {
|
|
|
322
324
|
expect(byName.get("env")).toEqual({ name: "env", value: "from-file", source: "params-file" });
|
|
323
325
|
});
|
|
324
326
|
|
|
325
|
-
test("
|
|
327
|
+
test("fold is the default (#1134): omitting the flag folds; --no-fold restores the run path", async () => {
|
|
326
328
|
await writeFile(
|
|
327
329
|
join(testDir, "test.infra.ts"),
|
|
328
330
|
`
|
|
@@ -345,7 +347,21 @@ export const testEntity = {
|
|
|
345
347
|
expect(result.success).toBe(true);
|
|
346
348
|
expect(result.resourceCount).toBe(1);
|
|
347
349
|
const anyFoldLine = errorSpy.mock.calls.map((call) => String(call[0])).some((line) => line.includes("[fold:"));
|
|
348
|
-
expect(anyFoldLine).toBe(
|
|
350
|
+
expect(anyFoldLine).toBe(true);
|
|
351
|
+
|
|
352
|
+
errorSpy.mockClear();
|
|
353
|
+
const runResult = await buildCommand({
|
|
354
|
+
path: testDir,
|
|
355
|
+
format: "json",
|
|
356
|
+
serializers: [mockSerializer],
|
|
357
|
+
fold: false,
|
|
358
|
+
});
|
|
359
|
+
expect(runResult.success).toBe(true);
|
|
360
|
+
expect(runResult.resourceCount).toBe(1);
|
|
361
|
+
const anyFoldLineOff = errorSpy.mock.calls
|
|
362
|
+
.map((call) => String(call[0]))
|
|
363
|
+
.some((line) => line.includes("[fold:"));
|
|
364
|
+
expect(anyFoldLineOff).toBe(false);
|
|
349
365
|
} finally {
|
|
350
366
|
errorSpy.mockRestore();
|
|
351
367
|
}
|
|
@@ -500,6 +516,194 @@ export const testEntity = {
|
|
|
500
516
|
{ Key: "env", Value: "prod" },
|
|
501
517
|
]);
|
|
502
518
|
});
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* chant #1138 — post-synth findings (a lexicon-shipped check's, and a
|
|
522
|
+
* project's `lint.policies`') honor `lint.rules` severity overrides the
|
|
523
|
+
* same way a pre-synth COR/EVL/COMP diagnostic does. `runComponentChecks`
|
|
524
|
+
* proved the shape (`component-lint.test.ts`); this proves the post-synth
|
|
525
|
+
* side of the same fix.
|
|
526
|
+
*/
|
|
527
|
+
describe("post-synth findings honor lint.rules (chant #1138)", () => {
|
|
528
|
+
// `armSandboxPolicyExecution` is a one-way, process-global latch — once a
|
|
529
|
+
// `--sandbox` build in this file arms it, EVERY later `buildCommand` call
|
|
530
|
+
// in this worker (sandboxed or not) would otherwise see it armed and
|
|
531
|
+
// refuse to load policies in-process. Reset it around each test here so
|
|
532
|
+
// the plain/sandboxed pairs below are actually independent.
|
|
533
|
+
beforeEach(() => resetSandboxPolicyExecutionForTests());
|
|
534
|
+
afterEach(() => resetSandboxPolicyExecutionForTests());
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* One trivial declarable using the `mockSerializer`'s "test" lexicon, so
|
|
538
|
+
* `result.outputs` is non-empty and the build's own "discovered source
|
|
539
|
+
* but produced no output" guard (`../commands/build.ts`) doesn't fire —
|
|
540
|
+
* these tests are about post-synth suppression, not that guard.
|
|
541
|
+
*/
|
|
542
|
+
async function writeTrivialEntity(): Promise<void> {
|
|
543
|
+
await writeFile(
|
|
544
|
+
join(testDir, "main.ts"),
|
|
545
|
+
`export const e = { lexicon: "test", entityType: "TestEntity", [Symbol.for("chant.declarable")]: true };\n`,
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/** A minimal `LexiconPlugin` whose one post-synth check always emits one fixed diagnostic — the fixed-input half of a severity-override test. */
|
|
550
|
+
function fakePostSynthPlugin(checkId: string, severity: "error" | "warning" | "info"): LexiconPlugin {
|
|
551
|
+
return {
|
|
552
|
+
name: "fake",
|
|
553
|
+
serializer: { name: "fake", rulePrefix: "FAKE", serialize: () => "{}" },
|
|
554
|
+
generate: async () => {},
|
|
555
|
+
validate: async () => {},
|
|
556
|
+
coverage: async () => {},
|
|
557
|
+
package: async () => {},
|
|
558
|
+
postSynthChecks: () => [
|
|
559
|
+
{
|
|
560
|
+
id: checkId,
|
|
561
|
+
description: "test check",
|
|
562
|
+
check: () => [{ checkId, severity, message: `${checkId} triggered` }],
|
|
563
|
+
},
|
|
564
|
+
],
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
test("lint.rules off suppresses a lexicon-shipped post-synth finding and reports a suppressed count", async () => {
|
|
569
|
+
await writeTrivialEntity();
|
|
570
|
+
await writeFile(
|
|
571
|
+
join(testDir, "chant.config.ts"),
|
|
572
|
+
`export default { lint: { rules: { "FAKE001": "off" } } };\n`,
|
|
573
|
+
);
|
|
574
|
+
|
|
575
|
+
const result = await buildCommand({
|
|
576
|
+
path: testDir,
|
|
577
|
+
format: "json",
|
|
578
|
+
serializers: [mockSerializer],
|
|
579
|
+
plugins: [fakePostSynthPlugin("FAKE001", "error")],
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
expect(result.success).toBe(true);
|
|
583
|
+
expect(result.errors).toEqual([]);
|
|
584
|
+
expect(result.errors.join("\n")).not.toContain("FAKE001 triggered");
|
|
585
|
+
expect(result.warnings.some((w) => w.includes("1 post-synth finding(s) suppressed"))).toBe(true);
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
test("lint.rules downgrades an error-severity post-synth check to warning, so it no longer fails the build", async () => {
|
|
589
|
+
await writeTrivialEntity();
|
|
590
|
+
await writeFile(
|
|
591
|
+
join(testDir, "chant.config.ts"),
|
|
592
|
+
`export default { lint: { rules: { "FAKE002": "warning" } } };\n`,
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
const result = await buildCommand({
|
|
596
|
+
path: testDir,
|
|
597
|
+
format: "json",
|
|
598
|
+
serializers: [mockSerializer],
|
|
599
|
+
plugins: [fakePostSynthPlugin("FAKE002", "error")],
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
expect(result.success).toBe(true);
|
|
603
|
+
expect(result.errors).toEqual([]);
|
|
604
|
+
expect(result.warnings.some((w) => w.includes("FAKE002 triggered"))).toBe(true);
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
test("lint.rules upgrades a warning-severity post-synth check to error, so it now fails the build", async () => {
|
|
608
|
+
await writeTrivialEntity();
|
|
609
|
+
await writeFile(
|
|
610
|
+
join(testDir, "chant.config.ts"),
|
|
611
|
+
`export default { lint: { rules: { "FAKE003": "error" } } };\n`,
|
|
612
|
+
);
|
|
613
|
+
|
|
614
|
+
const result = await buildCommand({
|
|
615
|
+
path: testDir,
|
|
616
|
+
format: "json",
|
|
617
|
+
serializers: [mockSerializer],
|
|
618
|
+
plugins: [fakePostSynthPlugin("FAKE003", "warning")],
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
expect(result.success).toBe(false);
|
|
622
|
+
expect(result.errors.some((e) => e.includes("FAKE003 triggered"))).toBe(true);
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
test("an unconfigured post-synth check id is unaffected — no drift from this fix for the common case", async () => {
|
|
626
|
+
await writeTrivialEntity();
|
|
627
|
+
await writeFile(join(testDir, "chant.config.ts"), `export default {};\n`);
|
|
628
|
+
|
|
629
|
+
const result = await buildCommand({
|
|
630
|
+
path: testDir,
|
|
631
|
+
format: "json",
|
|
632
|
+
serializers: [mockSerializer],
|
|
633
|
+
plugins: [fakePostSynthPlugin("FAKE004", "error")],
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
expect(result.success).toBe(false);
|
|
637
|
+
expect(result.errors.some((e) => e.includes("FAKE004 triggered"))).toBe(true);
|
|
638
|
+
expect(result.warnings.some((w) => w.includes("suppressed"))).toBe(false);
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
/** Write a `lint.policies` project: one policy file whose check always emits one fixed diagnostic. */
|
|
642
|
+
async function writePolicyProject(checkId: string, severity: "error" | "warning", rules: Record<string, string>): Promise<void> {
|
|
643
|
+
await mkdir(join(testDir, "policies"), { recursive: true });
|
|
644
|
+
await writeTrivialEntity();
|
|
645
|
+
await writeFile(
|
|
646
|
+
join(testDir, "policies", "org.ts"),
|
|
647
|
+
`export const check = {\n` +
|
|
648
|
+
` id: ${JSON.stringify(checkId)},\n` +
|
|
649
|
+
` description: "test policy",\n` +
|
|
650
|
+
` check: () => [{ checkId: ${JSON.stringify(checkId)}, severity: ${JSON.stringify(severity)}, message: ${JSON.stringify(`${checkId} triggered`)} }],\n` +
|
|
651
|
+
`};\n`,
|
|
652
|
+
);
|
|
653
|
+
await writeFile(
|
|
654
|
+
join(testDir, "chant.config.ts"),
|
|
655
|
+
`export default { lint: { policies: ["policies/org.ts"], rules: ${JSON.stringify(rules)} } };\n`,
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
test(
|
|
660
|
+
"lint.rules off suppresses a lint.policies finding identically under plain and --sandbox builds",
|
|
661
|
+
async () => {
|
|
662
|
+
await writePolicyProject("ORG-OFF", "error", { "ORG-OFF": "off" });
|
|
663
|
+
|
|
664
|
+
const plain = await buildCommand({ path: testDir, format: "json", serializers: [mockSerializer], plugins: [] });
|
|
665
|
+
const sandboxed = await buildCommand({
|
|
666
|
+
path: testDir,
|
|
667
|
+
format: "json",
|
|
668
|
+
serializers: [mockSerializer],
|
|
669
|
+
plugins: [],
|
|
670
|
+
fold: true,
|
|
671
|
+
sandbox: true,
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
for (const result of [plain, sandboxed]) {
|
|
675
|
+
expect(result.success).toBe(true);
|
|
676
|
+
expect(result.errors).toEqual([]);
|
|
677
|
+
expect(result.warnings.some((w) => w.includes("1 post-synth finding(s) suppressed"))).toBe(true);
|
|
678
|
+
}
|
|
679
|
+
},
|
|
680
|
+
30_000,
|
|
681
|
+
);
|
|
682
|
+
|
|
683
|
+
test(
|
|
684
|
+
"lint.rules upgrades a lint.policies warning to error identically under plain and --sandbox builds",
|
|
685
|
+
async () => {
|
|
686
|
+
await writePolicyProject("ORG-UP", "warning", { "ORG-UP": "error" });
|
|
687
|
+
|
|
688
|
+
const plain = await buildCommand({ path: testDir, format: "json", serializers: [mockSerializer], plugins: [] });
|
|
689
|
+
const sandboxed = await buildCommand({
|
|
690
|
+
path: testDir,
|
|
691
|
+
format: "json",
|
|
692
|
+
serializers: [mockSerializer],
|
|
693
|
+
plugins: [],
|
|
694
|
+
fold: true,
|
|
695
|
+
sandbox: true,
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
for (const result of [plain, sandboxed]) {
|
|
699
|
+
expect(result.success).toBe(false);
|
|
700
|
+
expect(result.errors.some((e) => e.includes("[policy:ORG-UP]") && e.includes("ORG-UP triggered"))).toBe(true);
|
|
701
|
+
expect(result.warnings.some((w) => w.includes("suppressed"))).toBe(false);
|
|
702
|
+
}
|
|
703
|
+
},
|
|
704
|
+
30_000,
|
|
705
|
+
);
|
|
706
|
+
});
|
|
503
707
|
});
|
|
504
708
|
|
|
505
709
|
// ── #284 bug 1: -o extension drives format when --format is absent ────────
|
|
@@ -4,6 +4,7 @@ import { resolveCliBuildParams } from "../build-params-cli";
|
|
|
4
4
|
import type { Serializer, SerializerResult } from "../../serializer";
|
|
5
5
|
import type { LexiconPlugin } from "../../lexicon";
|
|
6
6
|
import { runPostSynthChecks } from "../../lint/post-synth";
|
|
7
|
+
import { applyConfiguredSeverity } from "../../lint/config";
|
|
7
8
|
import { loadPolicyChecks } from "../../lint/policy";
|
|
8
9
|
import { armSandboxPolicyExecution, runProjectPolicies } from "../../lint/policy-sandbox";
|
|
9
10
|
import { sortedJsonReplacer } from "../../utils";
|
|
@@ -35,11 +36,12 @@ export interface BuildOptions {
|
|
|
35
36
|
*/
|
|
36
37
|
env?: string;
|
|
37
38
|
/**
|
|
38
|
-
* chant #1022 (epic #1019) —
|
|
39
|
-
*
|
|
40
|
-
* run per-file for anything the folder can't represent.
|
|
41
|
-
*
|
|
42
|
-
*
|
|
39
|
+
* chant #1022/#1134 (epic #1019) — fold source modules statically instead
|
|
40
|
+
* of importing/running them; the DEFAULT build path since #1134. Falls
|
|
41
|
+
* back to run per-file for anything the folder can't represent. Tri-state:
|
|
42
|
+
* `--fold` → true, `--no-fold` → false, unset → the project config /
|
|
43
|
+
* default via {@link resolveFoldEnabled}. An explicit flag always wins for
|
|
44
|
+
* the invocation, in either direction.
|
|
43
45
|
*/
|
|
44
46
|
fold?: boolean;
|
|
45
47
|
|
|
@@ -147,9 +149,9 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
|
|
|
147
149
|
const configDir = loaded.configPath ? dirname(loaded.configPath) : infraPath;
|
|
148
150
|
const policies = config.lint?.policies ?? [];
|
|
149
151
|
|
|
150
|
-
// #1022 —
|
|
151
|
-
//
|
|
152
|
-
//
|
|
152
|
+
// #1022/#1134 — fold is the default build path: an explicit CLI flag
|
|
153
|
+
// (--fold/--no-fold) wins over `chant.config.ts`'s `build.fold`, which
|
|
154
|
+
// wins over the default of `true`.
|
|
153
155
|
const fold = resolveFoldEnabled(config, options.fold);
|
|
154
156
|
|
|
155
157
|
// #1045 Phase 2 — opt-in sandboxed execution of run-fallback files (or,
|
|
@@ -281,6 +283,16 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
|
|
|
281
283
|
}
|
|
282
284
|
|
|
283
285
|
// Run post-synth checks from plugins — each plugin only sees its own lexicon's output
|
|
286
|
+
//
|
|
287
|
+
// chant #1138 — every diagnostic collected below (a lexicon-shipped check's
|
|
288
|
+
// AND a project's `lint.policies`') is resolved against `lint.rules` before
|
|
289
|
+
// it becomes an error/warning line, through the exact same
|
|
290
|
+
// `applyConfiguredSeverity` (../../lint/post-synth.ts) that keys off
|
|
291
|
+
// `diag.checkId` the way `lintCommand` keys an AST/COMP* diagnostic off its
|
|
292
|
+
// rule id — so `lint.rules: { WAW019: "off" }` suppresses a post-synth
|
|
293
|
+
// finding just as it suppresses a pre-synth one. A finding it suppresses is
|
|
294
|
+
// counted, not dropped silently — see `suppressedPostSynthCount` below.
|
|
295
|
+
let suppressedPostSynthCount = 0;
|
|
284
296
|
if (result.errors.length === 0 && options.plugins) {
|
|
285
297
|
for (const plugin of options.plugins) {
|
|
286
298
|
if (!plugin.postSynthChecks) continue;
|
|
@@ -300,7 +312,9 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
|
|
|
300
312
|
|
|
301
313
|
const scopedResult = { ...result, outputs: scopedOutputs };
|
|
302
314
|
const postDiags = runPostSynthChecks(checks, scopedResult, env);
|
|
303
|
-
|
|
315
|
+
const { diagnostics: activeDiags, suppressed } = applyConfiguredSeverity(postDiags, config.lint?.rules);
|
|
316
|
+
suppressedPostSynthCount += suppressed.length;
|
|
317
|
+
for (const diag of activeDiags) {
|
|
304
318
|
const prefix = diag.entity ? `[${diag.entity}] ` : "";
|
|
305
319
|
const lexiconSuffix = diag.lexicon ? ` (${diag.lexicon})` : "";
|
|
306
320
|
if (diag.severity === "error") {
|
|
@@ -320,6 +334,15 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
|
|
|
320
334
|
// result to a post-merge sandboxed child, which imports the policy modules
|
|
321
335
|
// and runs their checks there, and only plain `PostSynthDiagnostic`s come
|
|
322
336
|
// back. Unsandboxed, it is the same in-process load-and-run as before.
|
|
337
|
+
//
|
|
338
|
+
// #1138 — `applyConfiguredSeverity` runs HERE, in the parent, after the
|
|
339
|
+
// sandboxed child (when armed) has already returned — never inside it.
|
|
340
|
+
// The child only knows the policy paths and the encoded build result, not
|
|
341
|
+
// this project's resolved `lint.rules`, and by design nothing about the
|
|
342
|
+
// suppression surface needs to cross that boundary: both the plain and
|
|
343
|
+
// the `--sandbox` path funnel through this identical call with the
|
|
344
|
+
// identical `config.lint?.rules`, so a sandboxed and an unsandboxed build
|
|
345
|
+
// of the same project apply the same config to the same diagnostics.
|
|
323
346
|
if (policies.length > 0) {
|
|
324
347
|
const policyDiags = await runProjectPolicies({
|
|
325
348
|
policies,
|
|
@@ -328,7 +351,9 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
|
|
|
328
351
|
env,
|
|
329
352
|
preloaded: preloadedPolicyChecks,
|
|
330
353
|
});
|
|
331
|
-
|
|
354
|
+
const { diagnostics: activePolicyDiags, suppressed } = applyConfiguredSeverity(policyDiags, config.lint?.rules);
|
|
355
|
+
suppressedPostSynthCount += suppressed.length;
|
|
356
|
+
for (const diag of activePolicyDiags) {
|
|
332
357
|
const prefix = diag.entity ? `[${diag.entity}] ` : "";
|
|
333
358
|
const where = diag.lexicon ? ` (${diag.lexicon})` : "";
|
|
334
359
|
const msg = `[policy:${diag.checkId}] ${prefix}${diag.message}${where}`;
|
|
@@ -336,6 +361,14 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
|
|
|
336
361
|
else warnings.push(formatWarning({ message: msg }));
|
|
337
362
|
}
|
|
338
363
|
}
|
|
364
|
+
|
|
365
|
+
if (suppressedPostSynthCount > 0) {
|
|
366
|
+
warnings.push(
|
|
367
|
+
formatWarning({
|
|
368
|
+
message: `${suppressedPostSynthCount} post-synth finding(s) suppressed via lint.rules (severity "off")`,
|
|
369
|
+
}),
|
|
370
|
+
);
|
|
371
|
+
}
|
|
339
372
|
}
|
|
340
373
|
|
|
341
374
|
// Empty-output guard: source files were discovered but no lexicon produced
|
package/src/cli/commands/lint.ts
CHANGED
|
@@ -18,8 +18,7 @@ import { formatError, formatInfo } from "../format";
|
|
|
18
18
|
import { GENERATED_MARKER } from "../../discovery/files";
|
|
19
19
|
|
|
20
20
|
// Import config loader
|
|
21
|
-
import { loadConfig, resolveRulesForFile,
|
|
22
|
-
import type { RuleConfig } from "../../lint/rule";
|
|
21
|
+
import { loadConfig, resolveRulesForFile, resolveConfiguredSeverity, findProjectRoot } from "../../lint/config";
|
|
23
22
|
|
|
24
23
|
/**
|
|
25
24
|
* Type guard to check if a value conforms to the LintRule interface.
|
|
@@ -263,28 +262,22 @@ function getDefaultRules(
|
|
|
263
262
|
const ruleOptions = new Map<string, Record<string, unknown>>();
|
|
264
263
|
|
|
265
264
|
for (const [ruleId, rule] of allRules) {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
continue;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
const parsed = parseRuleConfig(configValue);
|
|
265
|
+
// chant #1138 — the same resolution post-synth checks and COMP* checks
|
|
266
|
+
// now go through too (`resolveConfiguredSeverity`, ../../lint/config.ts),
|
|
267
|
+
// so `lint.rules: { ID: "off" }` suppresses a rule id identically no
|
|
268
|
+
// matter which phase produced it.
|
|
269
|
+
const { severity, options } = resolveConfiguredSeverity(effectiveRules, ruleId, rule.severity);
|
|
275
270
|
|
|
276
271
|
// Skip rules that are explicitly turned off
|
|
277
|
-
if (
|
|
272
|
+
if (severity === "off") continue;
|
|
278
273
|
|
|
279
|
-
// Override severity from config
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
severity: parsed.severity as "error" | "warning" | "info",
|
|
283
|
-
});
|
|
274
|
+
// Override severity from config (a no-op when the rule wasn't mentioned —
|
|
275
|
+
// `severity` is then just `rule.severity` again)
|
|
276
|
+
rules.push({ ...rule, severity });
|
|
284
277
|
|
|
285
278
|
// Store options if present
|
|
286
|
-
if (
|
|
287
|
-
ruleOptions.set(ruleId,
|
|
279
|
+
if (options) {
|
|
280
|
+
ruleOptions.set(ruleId, options);
|
|
288
281
|
}
|
|
289
282
|
}
|
|
290
283
|
|
|
@@ -394,12 +387,11 @@ async function runComponentCheckDiagnostics(
|
|
|
394
387
|
// Discovery errors (COMP000) always surface at error severity — not user-configurable.
|
|
395
388
|
let severity = d.severity;
|
|
396
389
|
if (d.checkId !== "COMP000") {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
}
|
|
390
|
+
// chant #1138 — same resolution function AST rules and post-synth
|
|
391
|
+
// checks use (`resolveConfiguredSeverity`, ../../lint/config.ts).
|
|
392
|
+
const resolved = resolveConfiguredSeverity(config.rules, d.checkId, d.severity);
|
|
393
|
+
if (resolved.severity === "off") continue;
|
|
394
|
+
severity = resolved.severity;
|
|
403
395
|
}
|
|
404
396
|
|
|
405
397
|
const disable = fileLevelDisable(d.file, d.checkId);
|
package/src/cli/main.test.ts
CHANGED
|
@@ -4,6 +4,12 @@ import { parseArgs, waitForStreamDrain } from "./main";
|
|
|
4
4
|
import { resolveCommand, type CommandDef, type ParsedArgs } from "./registry";
|
|
5
5
|
|
|
6
6
|
describe("parseArgs", () => {
|
|
7
|
+
test("--fold and --no-fold set the tri-state fold option (#1134)", () => {
|
|
8
|
+
expect(parseArgs(["build", "src"]).fold).toBeUndefined();
|
|
9
|
+
expect(parseArgs(["build", "src", "--fold"]).fold).toBe(true);
|
|
10
|
+
expect(parseArgs(["build", "src", "--no-fold"]).fold).toBe(false);
|
|
11
|
+
});
|
|
12
|
+
|
|
7
13
|
test("parses command as first positional arg", () => {
|
|
8
14
|
const result = parseArgs(["build"]);
|
|
9
15
|
expect(result.command).toBe("build");
|
package/src/cli/main.ts
CHANGED
|
@@ -69,6 +69,7 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
69
69
|
"--bump",
|
|
70
70
|
"--no-release-record",
|
|
71
71
|
"--fold",
|
|
72
|
+
"--no-fold",
|
|
72
73
|
"--sandbox",
|
|
73
74
|
]);
|
|
74
75
|
|
|
@@ -294,6 +295,10 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
294
295
|
result.noReleaseRecord = true;
|
|
295
296
|
} else if (arg === "--fold") {
|
|
296
297
|
result.fold = true;
|
|
298
|
+
} else if (arg === "--no-fold") {
|
|
299
|
+
// chant #1134 — fold is the default build path; this is the explicit
|
|
300
|
+
// opt-out, and like --fold it beats chant.config.ts's build.fold.
|
|
301
|
+
result.fold = false;
|
|
297
302
|
} else if (arg === "--sandbox") {
|
|
298
303
|
result.sandbox = true;
|
|
299
304
|
} else if (arg === "--param") {
|
|
@@ -514,8 +519,11 @@ Options:
|
|
|
514
519
|
to run per-file for anything else outside the fold
|
|
515
520
|
subset (a cross-file-only reference, a re-export,
|
|
516
521
|
\`export default\`, ...). Logs which path each file
|
|
517
|
-
took.
|
|
518
|
-
chant.config.ts
|
|
522
|
+
took. DEFAULT since #1134 — this flag forces it on
|
|
523
|
+
over a chant.config.ts \`build.fold: false\`.
|
|
524
|
+
--no-fold (build) Opt out of folding for this invocation: every
|
|
525
|
+
source module is imported and run, the pre-#1134
|
|
526
|
+
behavior. Beats chant.config.ts's build.fold.
|
|
519
527
|
--sandbox (build) Run run-fallback source files (or every
|
|
520
528
|
file, without --fold) together, isolated, in one
|
|
521
529
|
sandboxed child process instead of in-process
|
package/src/config.test.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { describe, test, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
loadChantConfig,
|
|
4
|
+
DEFAULT_CHANT_CONFIG,
|
|
5
|
+
resolveAutoReleaseDisabled,
|
|
6
|
+
resolveFoldEnabled,
|
|
7
|
+
resolveSbomFormat,
|
|
8
|
+
} from "./config";
|
|
3
9
|
import { writeFileSync, mkdirSync, rmSync } from "fs";
|
|
4
10
|
import { join } from "path";
|
|
5
11
|
|
|
@@ -128,6 +134,27 @@ describe("loadChantConfig", () => {
|
|
|
128
134
|
});
|
|
129
135
|
});
|
|
130
136
|
|
|
137
|
+
describe("resolveFoldEnabled (#1134 — fold is the default build path)", () => {
|
|
138
|
+
test("default (no flag, no config) → fold ON", () => {
|
|
139
|
+
expect(resolveFoldEnabled({})).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("config build.fold: false turns it off; true keeps it on", () => {
|
|
143
|
+
expect(resolveFoldEnabled({ build: { fold: false } })).toBe(false);
|
|
144
|
+
expect(resolveFoldEnabled({ build: { fold: true } })).toBe(true);
|
|
145
|
+
expect(resolveFoldEnabled({ build: {} })).toBe(true);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("--fold (flag true) beats config false", () => {
|
|
149
|
+
expect(resolveFoldEnabled({ build: { fold: false } }, true)).toBe(true);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("--no-fold (flag false) beats config true and the default", () => {
|
|
153
|
+
expect(resolveFoldEnabled({ build: { fold: true } }, false)).toBe(false);
|
|
154
|
+
expect(resolveFoldEnabled({}, false)).toBe(false);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
131
158
|
describe("resolveAutoReleaseDisabled", () => {
|
|
132
159
|
test("default (no flag, no config) → not disabled", () => {
|
|
133
160
|
expect(resolveAutoReleaseDisabled({})).toBe(false);
|
package/src/config.ts
CHANGED
|
@@ -139,12 +139,12 @@ export interface ChantConfig {
|
|
|
139
139
|
*/
|
|
140
140
|
build?: {
|
|
141
141
|
/**
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
142
|
+
* Fold source modules statically instead of importing/running them,
|
|
143
|
+
* falling back to run per-file for anything the folder can't represent.
|
|
144
|
+
* DEFAULT `true` since chant #1134 — set `false` to make this project
|
|
145
|
+
* run every module (the pre-#1134 behavior). The `--fold`/`--no-fold`
|
|
146
|
+
* CLI flags override this per-invocation in either direction. See
|
|
147
|
+
* {@link resolveFoldEnabled}.
|
|
148
148
|
*/
|
|
149
149
|
fold?: boolean;
|
|
150
150
|
|
|
@@ -353,14 +353,18 @@ export function resolveAutoReleaseDisabled(config: ChantConfig, cliFlag?: boolea
|
|
|
353
353
|
|
|
354
354
|
/**
|
|
355
355
|
* Whether `chant build` should use the fold path (#1022, epic #1019)
|
|
356
|
-
* instead of running each source module.
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
*
|
|
356
|
+
* instead of running each source module. DEFAULT-ON since chant #1134: fold
|
|
357
|
+
* is the build path unless something turns it off. Precedence, most specific
|
|
358
|
+
* wins: an explicit CLI flag (`--fold` → true, `--no-fold` → false, arriving
|
|
359
|
+
* here as `cliFlag`), then the project config's `build.fold`, then the
|
|
360
|
+
* default of `true`. The epic's evidence base for the flip — coverage,
|
|
361
|
+
* byte-identity, and the sandbox execution boundary — is recorded on #1134
|
|
362
|
+
* and #1090.
|
|
360
363
|
*/
|
|
361
364
|
export function resolveFoldEnabled(config: ChantConfig, cliFlag?: boolean): boolean {
|
|
362
|
-
if (cliFlag) return
|
|
363
|
-
|
|
365
|
+
if (cliFlag !== undefined) return cliFlag;
|
|
366
|
+
if (config.build?.fold !== undefined) return config.build.fold;
|
|
367
|
+
return true;
|
|
364
368
|
}
|
|
365
369
|
|
|
366
370
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, test, expect, beforeEach, afterEach } from "vitest";
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
2
|
import { mkdir, writeFile, rm, realpath } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
@@ -236,4 +236,58 @@ describe("chant.config.ts evaluation under --sandbox (chant #1113)", () => {
|
|
|
236
236
|
);
|
|
237
237
|
expect(marker()).toBeUndefined();
|
|
238
238
|
});
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* chant #1148 — `chant.config.ts`'s own `console.log`/`console.error` used
|
|
242
|
+
* to go nowhere under `--sandbox`. Same relay as the run-fallback child
|
|
243
|
+
* (`./run.test.ts`), just this child's own prefix.
|
|
244
|
+
*/
|
|
245
|
+
describe("console output forwarding (chant #1148)", () => {
|
|
246
|
+
beforeEach(() => {
|
|
247
|
+
vi.restoreAllMocks();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
afterEach(() => {
|
|
251
|
+
vi.restoreAllMocks();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
/** Spies on `process.stderr.write` and returns the lines captured so far — same shape as `../../cli/handlers/emulator.test.ts`'s `stdout()`/`stderr()` helpers. */
|
|
255
|
+
function captureStderr(): string[] {
|
|
256
|
+
const lines: string[] = [];
|
|
257
|
+
vi.spyOn(process.stderr, "write").mockImplementation((chunk: unknown) => {
|
|
258
|
+
lines.push(String(chunk));
|
|
259
|
+
return true;
|
|
260
|
+
});
|
|
261
|
+
return lines;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// See ./run.test.ts's identical helper doc: the IPC "message" the awaited
|
|
265
|
+
// call resolves on and the child's stdout/stderr pipe data are
|
|
266
|
+
// independent channels, so polling briefly avoids a flaky assertion.
|
|
267
|
+
async function waitFor(lines: string[], matcher: RegExp, timeoutMs = 5000): Promise<void> {
|
|
268
|
+
const start = Date.now();
|
|
269
|
+
while (Date.now() - start < timeoutMs) {
|
|
270
|
+
if (matcher.test(lines.join(""))) return;
|
|
271
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
272
|
+
}
|
|
273
|
+
throw new Error(`stderr never matched ${matcher}. Captured so far:\n${lines.join("")}`);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
test("the config's own console.log/console.error are forwarded, prefixed with [sandbox:config]", async () => {
|
|
277
|
+
const stderr = captureStderr();
|
|
278
|
+
await writeFile(
|
|
279
|
+
join(testDir, "chant.config.ts"),
|
|
280
|
+
`console.log("hello from config stdout");\n` +
|
|
281
|
+
`console.error("hello from config stderr");\n` +
|
|
282
|
+
`export default { lexicons: ["aws"] };\n`,
|
|
283
|
+
);
|
|
284
|
+
armSandboxConfigEvaluation();
|
|
285
|
+
|
|
286
|
+
const { config } = await loadChantConfig(testDir);
|
|
287
|
+
|
|
288
|
+
expect(config.lexicons).toEqual(["aws"]);
|
|
289
|
+
await waitFor(stderr, /^\[sandbox:config\] hello from config stdout$/m);
|
|
290
|
+
await waitFor(stderr, /^\[sandbox:config\] hello from config stderr$/m);
|
|
291
|
+
});
|
|
292
|
+
});
|
|
239
293
|
});
|
|
@@ -108,6 +108,9 @@ export async function evaluateConfigSandboxed(
|
|
|
108
108
|
env,
|
|
109
109
|
timeoutMs: CONFIG_CHILD_TIMEOUT_MS,
|
|
110
110
|
label: `sandboxed evaluation of ${configPath}`,
|
|
111
|
+
// chant #1148 — the config's own console.log/error no longer goes
|
|
112
|
+
// nowhere; see `./fork.ts`'s `outputPrefix` doc.
|
|
113
|
+
outputPrefix: "[sandbox:config]",
|
|
111
114
|
},
|
|
112
115
|
isConfigChildResponse,
|
|
113
116
|
);
|