@intentius/chant 0.58.0 → 0.60.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/audit/core.d.ts +17 -1
- package/dist/audit/core.d.ts.map +1 -1
- package/dist/audit/discover.d.ts +15 -4
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/build-params.d.ts +2 -2
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/carve-bridge.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts +13 -0
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/lint.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/main.d.ts +0 -16
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts +22 -0
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +14 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +4 -1
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/component.d.ts +19 -4
- package/dist/components/component.d.ts.map +1 -1
- package/dist/components/driver.d.ts +8 -2
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/pilots/alb-ecs.pilot.d.ts +2 -2
- package/dist/components/verbs/run-agent.d.ts +1 -7
- package/dist/components/verbs/run-agent.d.ts.map +1 -1
- package/dist/config.d.ts +4 -4
- package/dist/detectLexicon.d.ts +13 -0
- package/dist/detectLexicon.d.ts.map +1 -1
- package/dist/lexicon.d.ts +213 -3
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.d.ts +9 -1
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/lifecycle/observe.d.ts +4 -4
- package/dist/lint/rules/comp/comp004-gate-needs-durable-runtime.d.ts.map +1 -1
- package/dist/op/activities/index.d.ts +2 -2
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/reconcile.d.ts +134 -2
- package/dist/op/activities/reconcile.d.ts.map +1 -1
- package/dist/op/builders.d.ts +2 -2
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/change-signal.d.ts +91 -0
- package/dist/op/change-signal.d.ts.map +1 -0
- package/dist/op/composites/apply-op.d.ts +7 -2
- package/dist/op/composites/apply-op.d.ts.map +1 -1
- package/dist/op/composites/reconcile-op.d.ts.map +1 -1
- package/dist/op/gate-name.d.ts +40 -0
- package/dist/op/gate-name.d.ts.map +1 -0
- package/dist/op/gate-summary.d.ts +55 -0
- package/dist/op/gate-summary.d.ts.map +1 -0
- package/dist/op/index.d.ts +6 -2
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/op-ir.d.ts +2 -1
- package/dist/op/op-ir.d.ts.map +1 -1
- package/dist/op/operator.d.ts +63 -0
- package/dist/op/operator.d.ts.map +1 -1
- package/dist/op/types.d.ts +18 -3
- package/dist/op/types.d.ts.map +1 -1
- package/dist/params.d.ts +1 -1
- package/dist/project-root.d.ts +2 -2
- package/dist/terraform/bridge.d.ts +26 -7
- package/dist/terraform/bridge.d.ts.map +1 -1
- package/dist/terraform/carve-provider.d.ts +28 -2
- package/dist/terraform/carve-provider.d.ts.map +1 -1
- package/dist/terraform/data-source-shape.d.ts +77 -0
- package/dist/terraform/data-source-shape.d.ts.map +1 -0
- package/dist/terraform/graph.d.ts.map +1 -1
- package/dist/terraform/providers/kubernetes.d.ts +5 -0
- package/dist/terraform/providers/kubernetes.d.ts.map +1 -1
- package/dist/terraform/tier-map.d.ts +16 -4
- package/dist/terraform/tier-map.d.ts.map +1 -1
- package/dist/terraform/types.d.ts +8 -0
- package/dist/terraform/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/audit/core.ts +25 -3
- package/src/audit/discover.ts +122 -14
- package/src/build-params.ts +2 -2
- package/src/cli/commands/audit.test.ts +5 -3
- package/src/cli/commands/audit.ts +5 -1
- package/src/cli/commands/build.test.ts +39 -1
- package/src/cli/commands/build.ts +22 -8
- package/src/cli/commands/carve-bridge.test.ts +237 -14
- package/src/cli/commands/carve-bridge.ts +21 -17
- package/src/cli/commands/carve-emit-k8s.test.ts +17 -10
- package/src/cli/commands/lint.test.ts +297 -1
- package/src/cli/commands/lint.ts +119 -11
- package/src/cli/handlers/graph.test.ts +4 -4
- package/src/cli/handlers/graph.ts +11 -11
- package/src/cli/handlers/lint.test.ts +107 -0
- package/src/cli/handlers/lint.ts +30 -0
- package/src/cli/handlers/operator.ts +81 -0
- package/src/cli/handlers/run.test.ts +132 -3
- package/src/cli/handlers/run.ts +87 -3
- package/src/cli/main.test.ts +9 -11
- package/src/cli/main.ts +5 -27
- package/src/cli/plugins.test.ts +68 -2
- package/src/cli/plugins.ts +39 -0
- package/src/cli/registry.ts +14 -0
- package/src/components/README.md +2 -2
- package/src/components/SPRAWL-VALIDATION.md +5 -5
- package/src/components/__fixtures__/neo4j-fanout.json +1 -1
- package/src/components/cli-support.test.ts +18 -7
- package/src/components/cli-support.ts +8 -3
- package/src/components/component-schema.test.ts +18 -2
- package/src/components/component.schema.json +17 -4
- package/src/components/component.test.ts +2 -2
- package/src/components/component.ts +25 -5
- package/src/components/config-defaults.test.ts +2 -2
- package/src/components/driver.test.ts +20 -1
- package/src/components/driver.ts +12 -5
- package/src/components/pilots/README.md +1 -1
- package/src/components/pilots/alb-ecs.pilot.ts +2 -2
- package/src/components/pilots/neo4j-fanout.pilot.ts +3 -3
- package/src/components/verbs/run-agent.test.ts +19 -0
- package/src/components/verbs/run-agent.ts +1 -7
- package/src/config.ts +4 -4
- package/src/detectLexicon.ts +18 -1
- package/src/discovery/fold-import.test.ts +2 -2
- package/src/discovery/fold-import.ts +3 -3
- package/src/fold/foldable-helpers.ts +1 -1
- package/src/graph-ops.test.ts +1 -1
- package/src/lexicon.ts +221 -3
- package/src/lifecycle/gate-ledger.ts +12 -1
- package/src/lifecycle/observe.test.ts +2 -2
- package/src/lifecycle/observe.ts +8 -8
- package/src/lifecycle/release-ledger.test.ts +2 -2
- package/src/lint/pipeline-change-gate.test.ts +2 -2
- package/src/lint/rules/comp/comp.test.ts +26 -0
- package/src/lint/rules/comp/comp004-gate-needs-durable-runtime.ts +4 -2
- package/src/lint/rules/op/ops014-converge-rule-refusals.test.ts +1 -1
- package/src/op/activities/index.ts +9 -2
- package/src/op/activities/reconcile.test.ts +320 -2
- package/src/op/activities/reconcile.ts +423 -2
- package/src/op/builders.ts +3 -3
- package/src/op/change-signal.test.ts +117 -0
- package/src/op/change-signal.ts +169 -0
- package/src/op/composites/apply-op.ts +14 -4
- package/src/op/composites/composites.test.ts +17 -4
- package/src/op/composites/reconcile-op.ts +7 -4
- package/src/op/effect-step.test.ts +3 -3
- package/src/op/gate-name.test.ts +65 -0
- package/src/op/gate-name.ts +60 -0
- package/src/op/gate-summary.test.ts +62 -0
- package/src/op/gate-summary.ts +96 -0
- package/src/op/index.ts +9 -2
- package/src/op/local-executor.test.ts +16 -1
- package/src/op/local-executor.ts +4 -3
- package/src/op/op-ir.test.ts +12 -1
- package/src/op/op-ir.ts +5 -3
- package/src/op/op-verb-class.test.ts +2 -2
- package/src/op/op.test.ts +2 -2
- package/src/op/operator.test.ts +368 -0
- package/src/op/operator.ts +141 -5
- package/src/op/runtimes/local.test.ts +1 -1
- package/src/op/types.ts +23 -3
- package/src/params.ts +1 -1
- package/src/project-root.ts +2 -2
- package/src/terraform/aws-resources.test.ts +13 -4
- package/src/terraform/bridge.test.ts +22 -9
- package/src/terraform/bridge.ts +89 -28
- package/src/terraform/carve-provider.ts +38 -2
- package/src/terraform/data-source-shape.ts +95 -0
- package/src/terraform/graph.ts +35 -7
- package/src/terraform/providers/kubernetes.ts +48 -2
- package/src/terraform/tier-map.ts +21 -6
- package/src/terraform/types.ts +8 -0
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { describe, test, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { lintCommand, isLintRule, loadPluginRules, type LintOptions } from "./lint";
|
|
2
|
+
import { lintCommand, isLintRule, loadPluginRules, LEXICON_RESOLUTION_RULE_ID, type LintOptions } from "./lint";
|
|
3
|
+
import { loadPlugins, resolveProjectLexicons } from "../plugins";
|
|
3
4
|
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
4
5
|
import { join, resolve } from "node:path";
|
|
5
6
|
import { tmpdir } from "node:os";
|
|
6
7
|
import { execFileSync } from "node:child_process";
|
|
8
|
+
import { params, setBuildParams } from "../../params";
|
|
7
9
|
|
|
8
10
|
describe("lintCommand", () => {
|
|
9
11
|
let testDir: string;
|
|
@@ -117,6 +119,69 @@ export default {
|
|
|
117
119
|
).toBe(true);
|
|
118
120
|
});
|
|
119
121
|
|
|
122
|
+
// #2251 — `chant lint` imports every `*.op.ts` file to read the Op it
|
|
123
|
+
// declares, and an Op that takes a step argument from `params.<name>`
|
|
124
|
+
// (`@intentius/chant/params`) evaluates that read at module load. Before
|
|
125
|
+
// this, nothing populated the shared parameters object first, so every such
|
|
126
|
+
// argument was `undefined` and OPS012 reported the activity contract
|
|
127
|
+
// violated on source that builds and runs. The fixture imports the real
|
|
128
|
+
// params module by absolute path — `@intentius/chant/params` maps to the
|
|
129
|
+
// same file (packages/core/package.json's `exports`), so it is the one
|
|
130
|
+
// module record `lintCommand` mutates.
|
|
131
|
+
describe("build parameters reach an Op's step arguments (#2251)", () => {
|
|
132
|
+
const paramsModule = resolve(import.meta.dirname, "../../params.ts");
|
|
133
|
+
|
|
134
|
+
async function writeParamReadingOp(): Promise<void> {
|
|
135
|
+
await writeFile(
|
|
136
|
+
join(testDir, "mini.op.ts"),
|
|
137
|
+
`
|
|
138
|
+
import { params } from ${JSON.stringify(paramsModule)};
|
|
139
|
+
|
|
140
|
+
export default {
|
|
141
|
+
[Symbol.for("chant.declarable")]: true,
|
|
142
|
+
entityType: "Chant::Op",
|
|
143
|
+
lexicon: "chant",
|
|
144
|
+
kind: "resource",
|
|
145
|
+
props: {
|
|
146
|
+
name: "mini",
|
|
147
|
+
overview: "test",
|
|
148
|
+
phases: [
|
|
149
|
+
{ name: "Greet", steps: [
|
|
150
|
+
{ kind: "activity", fn: "shellCmd", args: { cmd: params.greeting } },
|
|
151
|
+
] },
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
test("OPS012 does not fire when the invocation's parameters are supplied", async () => {
|
|
160
|
+
await writeParamReadingOp();
|
|
161
|
+
|
|
162
|
+
const result = await lintCommand({
|
|
163
|
+
path: testDir,
|
|
164
|
+
format: "stylish",
|
|
165
|
+
buildParams: [{ name: "greeting", value: "echo hi", source: "default" }],
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
expect(result.diagnostics.filter((d) => d.ruleId === "OPS012")).toEqual([]);
|
|
169
|
+
expect(result.success).toBe(true);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("OPS012 fires on the same source when no parameters are supplied", async () => {
|
|
173
|
+
await writeParamReadingOp();
|
|
174
|
+
|
|
175
|
+
const result = await lintCommand({ path: testDir, format: "stylish" });
|
|
176
|
+
|
|
177
|
+
expect(
|
|
178
|
+
result.diagnostics.some(
|
|
179
|
+
(d) => d.ruleId === "OPS012" && d.message.includes("args.cmd"),
|
|
180
|
+
),
|
|
181
|
+
).toBe(true);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
120
185
|
test("formats output as JSON", async () => {
|
|
121
186
|
await writeFile(
|
|
122
187
|
join(testDir, "nested.ts"),
|
|
@@ -541,3 +606,234 @@ describe("lintCommand — git-ignored files are not linted", () => {
|
|
|
541
606
|
expect(result.diagnostics.some((d) => d.file.includes("authored.ts") && d.ruleId === "EVL003")).toBe(true);
|
|
542
607
|
});
|
|
543
608
|
});
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* chant #2222 (epic #2215 sub-issue 7) — `chant lint` used to print "No
|
|
612
|
+
* problems found" and exit 0 on a project whose declared lexicon could not be
|
|
613
|
+
* resolved, the state `chant build` refuses outright. These pin both halves:
|
|
614
|
+
* the failure is now reported, and the one case the old `catch` legitimately
|
|
615
|
+
* covered (a project that declares no lexicon at all) still lints quietly.
|
|
616
|
+
*/
|
|
617
|
+
describe("lintCommand — a declared lexicon that cannot be resolved (#2222)", () => {
|
|
618
|
+
let testDir: string;
|
|
619
|
+
|
|
620
|
+
beforeEach(async () => {
|
|
621
|
+
testDir = join(tmpdir(), `chant-lint-lexicon-${Date.now()}-${Math.random()}`);
|
|
622
|
+
await mkdir(join(testDir, "src"), { recursive: true });
|
|
623
|
+
await writeFile(join(testDir, "src", "clean.ts"), `export const x = 1;\n`);
|
|
624
|
+
process.env.NO_COLOR = "1";
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
afterEach(async () => {
|
|
628
|
+
await rm(testDir, { recursive: true, force: true });
|
|
629
|
+
delete process.env.NO_COLOR;
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
/** The message `chant build` prints after `error: ` for the same project. */
|
|
633
|
+
async function buildsMessageFor(dir: string): Promise<string> {
|
|
634
|
+
try {
|
|
635
|
+
await loadPlugins(await resolveProjectLexicons(dir));
|
|
636
|
+
} catch (err) {
|
|
637
|
+
return err instanceof Error ? err.message : String(err);
|
|
638
|
+
}
|
|
639
|
+
throw new Error(`expected ${dir} to fail lexicon resolution`);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
test("a declared lexicon whose package is not installed fails the lint", async () => {
|
|
643
|
+
await writeFile(
|
|
644
|
+
join(testDir, "chant.config.ts"),
|
|
645
|
+
`export default { lexicons: ["not-a-real-lexicon"] };\n`,
|
|
646
|
+
);
|
|
647
|
+
|
|
648
|
+
const result = await lintCommand({ path: testDir, format: "stylish" });
|
|
649
|
+
|
|
650
|
+
expect(result.success).toBe(false);
|
|
651
|
+
expect(result.errorCount).toBe(1);
|
|
652
|
+
const diag = result.diagnostics.find((d) => d.ruleId === LEXICON_RESOLUTION_RULE_ID);
|
|
653
|
+
expect(diag).toBeDefined();
|
|
654
|
+
expect(diag!.severity).toBe("error");
|
|
655
|
+
expect(diag!.file).toBe(join(testDir, "chant.config.ts"));
|
|
656
|
+
// Byte-for-byte what `chant build` reports, not a second wording.
|
|
657
|
+
expect(diag!.message).toBe(await buildsMessageFor(testDir));
|
|
658
|
+
expect(result.output).not.toContain("No problems found");
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
test("a chant.config that cannot be loaded at all fails the lint with its own error", async () => {
|
|
662
|
+
// The shape found in the wild: the config imports the lexicon package, so
|
|
663
|
+
// it throws before `lexicons` is ever read.
|
|
664
|
+
await writeFile(
|
|
665
|
+
join(testDir, "chant.config.ts"),
|
|
666
|
+
`import { thing } from "@intentius/chant-lexicon-not-a-real-lexicon";\n` +
|
|
667
|
+
`export default { lexicons: ["aws"], thing };\n`,
|
|
668
|
+
);
|
|
669
|
+
|
|
670
|
+
const result = await lintCommand({ path: testDir, format: "stylish" });
|
|
671
|
+
|
|
672
|
+
expect(result.success).toBe(false);
|
|
673
|
+
const diag = result.diagnostics.find((d) => d.ruleId === LEXICON_RESOLUTION_RULE_ID);
|
|
674
|
+
expect(diag).toBeDefined();
|
|
675
|
+
expect(diag!.message).toBe(await buildsMessageFor(testDir));
|
|
676
|
+
expect(diag!.message).toContain("@intentius/chant-lexicon-not-a-real-lexicon");
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
test("the failure reaches --format json", async () => {
|
|
680
|
+
await writeFile(
|
|
681
|
+
join(testDir, "chant.config.ts"),
|
|
682
|
+
`export default { lexicons: ["not-a-real-lexicon"] };\n`,
|
|
683
|
+
);
|
|
684
|
+
|
|
685
|
+
const result = await lintCommand({ path: testDir, format: "json" });
|
|
686
|
+
|
|
687
|
+
const parsed = JSON.parse(result.output) as Array<Record<string, unknown>>;
|
|
688
|
+
const entry = parsed.find((d) => d.ruleId === LEXICON_RESOLUTION_RULE_ID);
|
|
689
|
+
expect(entry).toBeDefined();
|
|
690
|
+
expect(entry!.severity).toBe("error");
|
|
691
|
+
expect(entry!.message).toBe(await buildsMessageFor(testDir));
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
test("the failure reaches --format sarif as an error-level result", async () => {
|
|
695
|
+
await writeFile(
|
|
696
|
+
join(testDir, "chant.config.ts"),
|
|
697
|
+
`export default { lexicons: ["not-a-real-lexicon"] };\n`,
|
|
698
|
+
);
|
|
699
|
+
|
|
700
|
+
const result = await lintCommand({ path: testDir, format: "sarif" });
|
|
701
|
+
|
|
702
|
+
const sarif = JSON.parse(result.output) as {
|
|
703
|
+
runs: Array<{ results: Array<{ ruleId: string; level: string; message: { text: string } }> }>;
|
|
704
|
+
};
|
|
705
|
+
const found = sarif.runs[0].results.find((r) => r.ruleId === LEXICON_RESOLUTION_RULE_ID);
|
|
706
|
+
expect(found).toBeDefined();
|
|
707
|
+
expect(found!.level).toBe("error");
|
|
708
|
+
expect(found!.message.text).toBe(await buildsMessageFor(testDir));
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
test("--fix does not drop the diagnostic", async () => {
|
|
712
|
+
await writeFile(
|
|
713
|
+
join(testDir, "chant.config.ts"),
|
|
714
|
+
`export default { lexicons: ["not-a-real-lexicon"] };\n`,
|
|
715
|
+
);
|
|
716
|
+
|
|
717
|
+
const result = await lintCommand({ path: testDir, format: "stylish", fix: true });
|
|
718
|
+
|
|
719
|
+
expect(result.success).toBe(false);
|
|
720
|
+
expect(result.diagnostics.some((d) => d.ruleId === LEXICON_RESOLUTION_RULE_ID)).toBe(true);
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
// The case the swallowed `catch` was written for, and the one that must not
|
|
724
|
+
// change: no `chant.config.*`, no lexicon import in any source file. The
|
|
725
|
+
// detection fallback throws "No lexicon detected in infrastructure files",
|
|
726
|
+
// which is a project that lints under the core rules alone.
|
|
727
|
+
test("a project with no chant.config at all still lints clean", async () => {
|
|
728
|
+
const result = await lintCommand({ path: testDir, format: "stylish" });
|
|
729
|
+
|
|
730
|
+
expect(result.success).toBe(true);
|
|
731
|
+
expect(result.diagnostics.some((d) => d.ruleId === LEXICON_RESOLUTION_RULE_ID)).toBe(false);
|
|
732
|
+
expect(result.output).toContain("No problems found");
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
test("a chant.config that declares no lexicons still lints clean", async () => {
|
|
736
|
+
await writeFile(join(testDir, "chant.config.ts"), `export default {};\n`);
|
|
737
|
+
|
|
738
|
+
const result = await lintCommand({ path: testDir, format: "stylish" });
|
|
739
|
+
|
|
740
|
+
expect(result.success).toBe(true);
|
|
741
|
+
expect(result.diagnostics.some((d) => d.ruleId === LEXICON_RESOLUTION_RULE_ID)).toBe(false);
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
test("a declared lexicon that does resolve lints clean", async () => {
|
|
745
|
+
await writeFile(join(testDir, "chant.config.ts"), `export default { lexicons: ["aws"] };\n`);
|
|
746
|
+
|
|
747
|
+
const result = await lintCommand({ path: testDir, format: "stylish" });
|
|
748
|
+
|
|
749
|
+
expect(result.success).toBe(true);
|
|
750
|
+
expect(result.diagnostics.some((d) => d.ruleId === LEXICON_RESOLUTION_RULE_ID)).toBe(false);
|
|
751
|
+
});
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* chant #2249 — `lintCommand` resolves the project's own declared
|
|
756
|
+
* `buildParams` when its caller supplied none, so an in-process lint agrees
|
|
757
|
+
* with `chant lint` on the command line.
|
|
758
|
+
*
|
|
759
|
+
* #2251 taught the CLI handler (../handlers/lint.ts) to resolve
|
|
760
|
+
* `--param`/`--params-file`/declared defaults and pass them down, because
|
|
761
|
+
* the OPS* checks import every `*.op.ts` file and an Op reading
|
|
762
|
+
* `params.<name>` evaluates that read at module load. Every other caller of
|
|
763
|
+
* `lintCommand` has no flags to read and passed nothing, so `params` stayed
|
|
764
|
+
* empty and OPS012 reported the activity contract violated for source that
|
|
765
|
+
* lints clean from a shell. That is how the root-examples gate
|
|
766
|
+
* (examples/root-examples-gate.test.ts) went red on github-pr-preview while
|
|
767
|
+
* `chant lint .` in the same directory exited 0, and it applied equally to
|
|
768
|
+
* `handleLint` over MCP and to test-utils' example harness.
|
|
769
|
+
*
|
|
770
|
+
* These drive the real `lintCommand` against a temp project holding one
|
|
771
|
+
* `*.op.ts` file (enough for the OPS* pass to run its parameter binding) and
|
|
772
|
+
* read the shared `params` object the binding populates.
|
|
773
|
+
*/
|
|
774
|
+
describe("lintCommand build-time parameters (#2249)", () => {
|
|
775
|
+
let testDir: string;
|
|
776
|
+
|
|
777
|
+
beforeEach(async () => {
|
|
778
|
+
testDir = join(tmpdir(), `chant-lint-params-${Date.now()}-${Math.random()}`);
|
|
779
|
+
await mkdir(testDir, { recursive: true });
|
|
780
|
+
// The OPS* pass binds parameters only when there is an Op file to import.
|
|
781
|
+
// A plain default export is enough: it is skipped as not-an-Op after the
|
|
782
|
+
// binding has already happened.
|
|
783
|
+
await writeFile(join(testDir, "noop.op.ts"), `export default { props: {} };\n`);
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
afterEach(async () => {
|
|
787
|
+
await rm(testDir, { recursive: true, force: true });
|
|
788
|
+
delete process.env.CHANT_TEST_ENV_2249;
|
|
789
|
+
setBuildParams({});
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
test("a declared default reaches params with no caller-supplied provenance", async () => {
|
|
793
|
+
await writeFile(
|
|
794
|
+
join(testDir, "chant.config.json"),
|
|
795
|
+
JSON.stringify({ buildParams: { env: { type: "string", default: "local" } } }),
|
|
796
|
+
);
|
|
797
|
+
|
|
798
|
+
await lintCommand({ path: testDir, format: "stylish" });
|
|
799
|
+
|
|
800
|
+
expect(params.env).toBe("local");
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
test("a declared env mapping reaches params the same way chant lint resolves it", async () => {
|
|
804
|
+
await writeFile(
|
|
805
|
+
join(testDir, "chant.config.json"),
|
|
806
|
+
JSON.stringify({
|
|
807
|
+
buildParams: { env: { type: "string", default: "local", env: "CHANT_TEST_ENV_2249" } },
|
|
808
|
+
}),
|
|
809
|
+
);
|
|
810
|
+
process.env.CHANT_TEST_ENV_2249 = "pr-42";
|
|
811
|
+
|
|
812
|
+
await lintCommand({ path: testDir, format: "stylish" });
|
|
813
|
+
|
|
814
|
+
expect(params.env).toBe("pr-42");
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
test("caller-supplied parameters win over the declared defaults", async () => {
|
|
818
|
+
await writeFile(
|
|
819
|
+
join(testDir, "chant.config.json"),
|
|
820
|
+
JSON.stringify({ buildParams: { env: { type: "string", default: "local" } } }),
|
|
821
|
+
);
|
|
822
|
+
|
|
823
|
+
await lintCommand({
|
|
824
|
+
path: testDir,
|
|
825
|
+
format: "stylish",
|
|
826
|
+
buildParams: [{ name: "env", value: "pr-42", source: "cli" }],
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
expect(params.env).toBe("pr-42");
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
test("a project declaring none binds an empty parameter set", async () => {
|
|
833
|
+
await writeFile(join(testDir, "chant.config.json"), JSON.stringify({}));
|
|
834
|
+
|
|
835
|
+
await lintCommand({ path: testDir, format: "stylish" });
|
|
836
|
+
|
|
837
|
+
expect(params).toEqual({});
|
|
838
|
+
});
|
|
839
|
+
});
|
package/src/cli/commands/lint.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { readFileSync, writeFileSync, readdirSync, statSync } from "fs";
|
|
|
4
4
|
import { execFileSync } from "child_process";
|
|
5
5
|
import { runLint, parseDisableComments } from "../../lint/engine";
|
|
6
6
|
import type { LintRule, LintDiagnostic, LintFix } from "../../lint/rule";
|
|
7
|
-
import type { IntrinsicDef } from "../../lexicon";
|
|
7
|
+
import type { IntrinsicDef, LexiconPlugin } from "../../lexicon";
|
|
8
8
|
import { loadPlugins, resolveProjectLexicons } from "../plugins";
|
|
9
9
|
import { formatStylish, formatJson, formatSarif } from "../reporters/stylish";
|
|
10
10
|
import { loadLocalRules } from "../../lint/rule-loader";
|
|
@@ -20,10 +20,14 @@ import { rule } from "../../lint/declarative";
|
|
|
20
20
|
import { watchDirectory, formatTimestamp, formatChangedFiles } from "../watch";
|
|
21
21
|
import { formatError, formatInfo } from "../format";
|
|
22
22
|
import { GENERATED_MARKER } from "../../discovery/files";
|
|
23
|
+
import { buildParamValues, resolveBuildParams } from "../../build-params";
|
|
24
|
+
import { setBuildParams } from "../../params";
|
|
25
|
+
import { isNoLexiconDetected } from "../../detectLexicon";
|
|
23
26
|
|
|
24
27
|
// Import config loader
|
|
25
28
|
import { loadConfig, resolveRulesForFile, resolveConfiguredSeverity, findProjectRoot } from "../../lint/config";
|
|
26
29
|
import { loadChantConfig, resolveKnowledgeDir } from "../../config";
|
|
30
|
+
import { findProjectConfig } from "../../project-root";
|
|
27
31
|
import type { LintProjectConfig } from "../../lint/rule";
|
|
28
32
|
import { loadOkfBundle, type OkfBundle } from "../../okf-read";
|
|
29
33
|
|
|
@@ -91,6 +95,40 @@ export async function loadPluginRules(
|
|
|
91
95
|
return pluginRules;
|
|
92
96
|
}
|
|
93
97
|
|
|
98
|
+
/**
|
|
99
|
+
* The diagnostic id `chant lint` reports a lexicon it could not resolve under
|
|
100
|
+
* (chant #2222).
|
|
101
|
+
*
|
|
102
|
+
* Deliberately outside the COR/EVL/COMP/OPS families: those are *rules*, each
|
|
103
|
+
* with a `check()` the engine runs per file, a documented page, a configurable
|
|
104
|
+
* severity and a `chant-disable` escape hatch. This is none of those. It is
|
|
105
|
+
* the lint run reporting that it could not assemble the rule set the project
|
|
106
|
+
* asked for, so it is not silenceable through `rules: { ... : "off" }` or a
|
|
107
|
+
* disable comment. Silencing it would put back the exact "green on a broken
|
|
108
|
+
* project" this id exists to prevent.
|
|
109
|
+
*/
|
|
110
|
+
export const LEXICON_RESOLUTION_RULE_ID = "LEX001";
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Turn a lexicon-resolution failure into the error diagnostic `chant lint`
|
|
114
|
+
* reports it as. The message is the underlying error's own, unwrapped, so it
|
|
115
|
+
* is character-for-character what `chant build` prints after `error: ` for the
|
|
116
|
+
* same project (`loadPluginsOrExit`, ../main.ts): one failure, one wording.
|
|
117
|
+
*
|
|
118
|
+
* Attributed to the project's `chant.config.*` where there is one, since that
|
|
119
|
+
* is the file that names the lexicon; to the project root otherwise.
|
|
120
|
+
*/
|
|
121
|
+
function lexiconResolutionDiagnostic(projectRoot: string, error: Error): LintDiagnostic {
|
|
122
|
+
return {
|
|
123
|
+
file: findProjectConfig(projectRoot).configPath ?? projectRoot,
|
|
124
|
+
line: 1,
|
|
125
|
+
column: 1,
|
|
126
|
+
ruleId: LEXICON_RESOLUTION_RULE_ID,
|
|
127
|
+
severity: "error",
|
|
128
|
+
message: error.message,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
94
132
|
/**
|
|
95
133
|
* Load all lint rules: core COR/EVL rules, then lexicon plugin rules.
|
|
96
134
|
*
|
|
@@ -103,7 +141,7 @@ export async function loadPluginRules(
|
|
|
103
141
|
*/
|
|
104
142
|
async function loadAllPluginRules(
|
|
105
143
|
projectPath: string,
|
|
106
|
-
): Promise<{ rules: Map<string, LintRule>; intrinsics: IntrinsicDef[] }> {
|
|
144
|
+
): Promise<{ rules: Map<string, LintRule>; intrinsics: IntrinsicDef[]; lexiconError?: Error }> {
|
|
107
145
|
const rules = new Map<string, LintRule>();
|
|
108
146
|
|
|
109
147
|
// Load core COR/EVL rules directly
|
|
@@ -111,16 +149,47 @@ async function loadAllPluginRules(
|
|
|
111
149
|
rules.set(r.id, r);
|
|
112
150
|
}
|
|
113
151
|
|
|
114
|
-
// Resolve project lexicons (e.g. ["aws"]) from config or detection
|
|
152
|
+
// Resolve project lexicons (e.g. ["aws"]) from config or detection, then
|
|
153
|
+
// load their plugins.
|
|
154
|
+
//
|
|
155
|
+
// chant #2222: a failure in either step is handed back to `lintCommand`
|
|
156
|
+
// (which reports it as {@link LEXICON_RESOLUTION_RULE_ID}) rather than
|
|
157
|
+
// swallowed or thrown. Both steps fail on the same project state: a
|
|
158
|
+
// `chant.config.ts` that names a lexicon whose package is not installed
|
|
159
|
+
// throws out of `loadPlugins`, and one that *imports* that package throws
|
|
160
|
+
// out of `resolveProjectLexicons` before the names are ever read. Before
|
|
161
|
+
// this, the first case crashed `chant lint` with a stack trace and the
|
|
162
|
+
// second was caught here and discarded, so `chant lint` printed "No
|
|
163
|
+
// problems found" and exited 0 on a project `chant build` refuses to
|
|
164
|
+
// build. A CI job that lints before it builds reported green on a project
|
|
165
|
+
// whose lexicon was missing.
|
|
166
|
+
//
|
|
167
|
+
// The one failure that stays quiet is the one the original `catch` was
|
|
168
|
+
// written for and named in its comment: a project that declares no
|
|
169
|
+
// `lexicons` and imports none from its source files, where the detection
|
|
170
|
+
// fallback throws `NO_LEXICON_DETECTED_MESSAGE`. That project lints under
|
|
171
|
+
// the core rules alone and always has. It is told apart by the exported
|
|
172
|
+
// sentinel rather than by a string literal copied to this file, so the two
|
|
173
|
+
// cannot drift apart.
|
|
115
174
|
let lexiconNames: string[] = [];
|
|
175
|
+
let lexiconError: Error | undefined;
|
|
116
176
|
try {
|
|
117
177
|
lexiconNames = await resolveProjectLexicons(projectPath);
|
|
118
|
-
} catch {
|
|
119
|
-
|
|
178
|
+
} catch (err) {
|
|
179
|
+
if (!isNoLexiconDetected(err)) {
|
|
180
|
+
lexiconError = err instanceof Error ? err : new Error(String(err));
|
|
181
|
+
}
|
|
120
182
|
}
|
|
121
183
|
|
|
122
184
|
// Load only project lexicon plugins (no "chant" injection)
|
|
123
|
-
|
|
185
|
+
let plugins: LexiconPlugin[] = [];
|
|
186
|
+
if (!lexiconError) {
|
|
187
|
+
try {
|
|
188
|
+
plugins = await loadPlugins(lexiconNames);
|
|
189
|
+
} catch (err) {
|
|
190
|
+
lexiconError = err instanceof Error ? err : new Error(String(err));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
124
193
|
|
|
125
194
|
// chant #1106 — the same plugins' registered intrinsics (`Ref`, `GetAtt`,
|
|
126
195
|
// ...), so EVL001 can answer "does this call fold?" exactly like fold()
|
|
@@ -150,7 +219,7 @@ async function loadAllPluginRules(
|
|
|
150
219
|
rules.set(r.id, r);
|
|
151
220
|
}
|
|
152
221
|
|
|
153
|
-
return { rules, intrinsics };
|
|
222
|
+
return { rules, intrinsics, ...(lexiconError ? { lexiconError } : {}) };
|
|
154
223
|
}
|
|
155
224
|
|
|
156
225
|
/**
|
|
@@ -468,11 +537,21 @@ async function runComponentCheckDiagnostics(
|
|
|
468
537
|
async function runOpCheckDiagnostics(
|
|
469
538
|
infraPath: string,
|
|
470
539
|
files: string[],
|
|
540
|
+
buildParams?: BuildParamProvenance[],
|
|
471
541
|
): Promise<{ diagnostics: LintDiagnostic[]; suppressed: Array<LintDiagnostic & { reason?: string }> }> {
|
|
472
542
|
const config = loadConfig(findProjectRoot(infraPath));
|
|
473
543
|
const opFiles = files.filter((f) => f.endsWith(".op.ts"));
|
|
474
544
|
if (opFiles.length === 0) return { diagnostics: [], suppressed: [] };
|
|
475
545
|
|
|
546
|
+
// chant #2251 — the same step `discover()` runs before it imports or folds a
|
|
547
|
+
// project file (../../discovery/index.ts): populate the shared build-time
|
|
548
|
+
// parameters object BEFORE the imports below, so an Op that takes a step
|
|
549
|
+
// argument from `params.<name>` reads the value this invocation resolved
|
|
550
|
+
// rather than `undefined`. Unconditional, so a stale value from a prior
|
|
551
|
+
// lint in the same process (a test, `--watch`) never leaks into one that
|
|
552
|
+
// resolved none.
|
|
553
|
+
setBuildParams(buildParamValues(buildParams ?? []));
|
|
554
|
+
|
|
476
555
|
const entities = new Map<string, unknown>();
|
|
477
556
|
const fileByOpName = new Map<string, string>();
|
|
478
557
|
for (const filePath of opFiles) {
|
|
@@ -573,10 +652,31 @@ export async function lintCommand(options: LintOptions): Promise<LintResult> {
|
|
|
573
652
|
// than a failure — `loadOkfBundle` already treats a missing directory as
|
|
574
653
|
// an empty bundle.
|
|
575
654
|
let knowledgeBundle: OkfBundle | undefined;
|
|
655
|
+
/**
|
|
656
|
+
* chant #2249 — this invocation's build-time parameters, falling back to
|
|
657
|
+
* the project's own declared `buildParams` when the caller passed none.
|
|
658
|
+
*
|
|
659
|
+
* `chant lint` resolves them in ./handlers/lint.ts (#2251) because only
|
|
660
|
+
* the CLI knows about `--param`/`--params-file`, and passes them here. An
|
|
661
|
+
* in-process caller has no such flags to read, and before this fell
|
|
662
|
+
* through to the empty parameter object: the OPS* checks import each
|
|
663
|
+
* `*.op.ts`, an Op taking a step argument from `params.<name>` read
|
|
664
|
+
* `undefined` at module load, and OPS012 reported the activity contract
|
|
665
|
+
* violated for source `chant lint` on the command line passes. That made
|
|
666
|
+
* `lintCommand` disagree with its own CLI (the root-examples gate,
|
|
667
|
+
* `handleLint` over MCP, test-utils' example harness). Resolving the
|
|
668
|
+
* declared defaults + `env` mappings here is what the command line
|
|
669
|
+
* already gets; anything a flag would override still arrives in
|
|
670
|
+
* `options.buildParams` and wins. Best-effort like `projectConfig`:
|
|
671
|
+
* `resolveBuildParams` collects failures per parameter without throwing,
|
|
672
|
+
* and the ones that did resolve are still better than none.
|
|
673
|
+
*/
|
|
674
|
+
let buildParams = options.buildParams;
|
|
576
675
|
try {
|
|
577
676
|
const chantConfig = (await loadChantConfig(projectRoot)).config;
|
|
578
677
|
projectConfig = chantConfig as LintProjectConfig;
|
|
579
678
|
knowledgeBundle = await loadOkfBundle(resolveKnowledgeDir(chantConfig, projectRoot));
|
|
679
|
+
buildParams ??= resolveBuildParams(chantConfig.buildParams, { env: process.env }).provenance;
|
|
580
680
|
} catch {
|
|
581
681
|
projectConfig = undefined;
|
|
582
682
|
knowledgeBundle = undefined;
|
|
@@ -629,14 +729,14 @@ export async function lintCommand(options: LintOptions): Promise<LintResult> {
|
|
|
629
729
|
// structurally distinct check family (whole-project, post-discovery,
|
|
630
730
|
// see ../../lint/component-checks.ts) but the same `chant lint` output and
|
|
631
731
|
// the same error-severity gating as every COR/EVL diagnostic.
|
|
632
|
-
const componentResult = await runComponentCheckDiagnostics(infraPath, options.sandbox,
|
|
732
|
+
const componentResult = await runComponentCheckDiagnostics(infraPath, options.sandbox, buildParams);
|
|
633
733
|
diagnostics.push(...componentResult.diagnostics);
|
|
634
734
|
suppressed.push(...componentResult.suppressed);
|
|
635
735
|
|
|
636
736
|
// Run the OPS* Op-model post-synth checks (#2122) over every `*.op.ts`
|
|
637
737
|
// file under the lint target — see runOpCheckDiagnostics's doc for why
|
|
638
738
|
// this needs no lexicon or build to fire.
|
|
639
|
-
const opResult = await runOpCheckDiagnostics(infraPath, files);
|
|
739
|
+
const opResult = await runOpCheckDiagnostics(infraPath, files, buildParams);
|
|
640
740
|
diagnostics.push(...opResult.diagnostics);
|
|
641
741
|
suppressed.push(...opResult.suppressed);
|
|
642
742
|
|
|
@@ -684,17 +784,25 @@ export async function lintCommand(options: LintOptions): Promise<LintResult> {
|
|
|
684
784
|
// `*.component.ts` file on their behalf), but a fix applied to another
|
|
685
785
|
// rule could still be in the same file a component was discovered from —
|
|
686
786
|
// re-run for consistency with the AST re-lint above.
|
|
687
|
-
const postComponentResult = await runComponentCheckDiagnostics(infraPath, options.sandbox,
|
|
787
|
+
const postComponentResult = await runComponentCheckDiagnostics(infraPath, options.sandbox, buildParams);
|
|
688
788
|
diagnostics.push(...postComponentResult.diagnostics);
|
|
689
789
|
suppressed.push(...postComponentResult.suppressed);
|
|
690
790
|
|
|
691
791
|
// OPS* checks have no `.fix` either; re-run for the same consistency
|
|
692
792
|
// reason as the COMP* re-run just above.
|
|
693
|
-
const postOpResult = await runOpCheckDiagnostics(infraPath, files);
|
|
793
|
+
const postOpResult = await runOpCheckDiagnostics(infraPath, files, buildParams);
|
|
694
794
|
diagnostics.push(...postOpResult.diagnostics);
|
|
695
795
|
suppressed.push(...postOpResult.suppressed);
|
|
696
796
|
}
|
|
697
797
|
|
|
798
|
+
// chant #2222: a lexicon the project declared but this run could not
|
|
799
|
+
// resolve. Appended here, after the `--fix` re-lint block above has finished
|
|
800
|
+
// reassigning `diagnostics`, so it survives every path; it is a property of
|
|
801
|
+
// the run, not of any file a fix could touch.
|
|
802
|
+
if (loaded.lexiconError) {
|
|
803
|
+
diagnostics.push(lexiconResolutionDiagnostic(projectRoot, loaded.lexiconError));
|
|
804
|
+
}
|
|
805
|
+
|
|
698
806
|
// Count errors and warnings
|
|
699
807
|
let errorCount = 0;
|
|
700
808
|
let warningCount = 0;
|
|
@@ -710,10 +710,10 @@ describe("runGraph", () => {
|
|
|
710
710
|
});
|
|
711
711
|
});
|
|
712
712
|
|
|
713
|
-
// The bug this branch fixes (#57): a multi-stack, per-component project
|
|
714
|
-
//
|
|
715
|
-
//
|
|
716
|
-
//
|
|
713
|
+
// The bug this branch fixes (#57): a multi-stack, per-component project has
|
|
714
|
+
// no stack literally named after the environment, so the live graph must
|
|
715
|
+
// resolve each component's own `cfn-deploy` stack(s) and pass them through
|
|
716
|
+
// to `observeResources` for the per-stack union.
|
|
717
717
|
test("multi-stack component project: resolves each component's cfn-deploy stack(s) and passes them to observeResources", async () => {
|
|
718
718
|
resolveLexMock.mockResolvedValue(["aws"]);
|
|
719
719
|
loadPluginsMock.mockResolvedValue([
|
|
@@ -253,17 +253,17 @@ async function runGraphLive(
|
|
|
253
253
|
return 1;
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
-
// Multi-stack, per-component projects (
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
//
|
|
263
|
-
//
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
//
|
|
256
|
+
// Multi-stack, per-component projects (#57): AWS's single-stack convention
|
|
257
|
+
// (`describeResources` defaults to a stack named after the environment,
|
|
258
|
+
// lexicons/aws/src/plugin.ts) never matches a per-component layout (e.g.
|
|
259
|
+
// `loom-local-a-<component>`), so the plain single call always observes zero
|
|
260
|
+
// nodes. Resolve every discovered component's `cfn-deploy` stack(s) — the
|
|
261
|
+
// same walk `chant components status --live` uses (`cfnDeployStacks`,
|
|
262
|
+
// ./components.ts) — and hand them to `observeResources`, which queries
|
|
263
|
+
// `describeResources` once per stack and unions the results. A project with
|
|
264
|
+
// no components (or whose discovery errors) yields no stacks, so
|
|
265
|
+
// `observeResources` falls back to its original single-stack call —
|
|
266
|
+
// unchanged.
|
|
267
267
|
const componentsDiscovery = await discoverComponents(resolve(args.src ?? config.sourceDir ?? "."), {
|
|
268
268
|
sandbox: args.sandbox,
|
|
269
269
|
});
|