@intentius/chant 0.44.13 → 0.45.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/discover.d.ts +26 -2
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/audit/report-model.d.ts +10 -0
- package/dist/audit/report-model.d.ts.map +1 -1
- package/dist/audit/rules-doc.d.ts.map +1 -1
- package/dist/cli/build-params-cli.d.ts +16 -7
- package/dist/cli/build-params-cli.d.ts.map +1 -1
- package/dist/cli/commands/audit.d.ts +20 -0
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts +9 -0
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-docs.d.ts +13 -0
- package/dist/cli/commands/check-lexicon-docs.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-examples.d.ts +24 -14
- package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon/templates/codegen.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon/templates/docs.d.ts +5 -0
- package/dist/cli/commands/init-lexicon/templates/docs.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/onboard.d.ts +28 -0
- package/dist/cli/commands/onboard.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/graph.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/search.d.ts +27 -0
- package/dist/cli/handlers/search.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/codegen/docs-pages.d.ts +39 -0
- package/dist/codegen/docs-pages.d.ts.map +1 -0
- package/dist/codegen/docs-sections.d.ts.map +1 -1
- package/dist/codegen/docs-sidebar.d.ts +13 -2
- package/dist/codegen/docs-sidebar.d.ts.map +1 -1
- package/dist/codegen/docs-types.d.ts +30 -10
- package/dist/codegen/docs-types.d.ts.map +1 -1
- package/dist/codegen/docs.d.ts +14 -2
- package/dist/codegen/docs.d.ts.map +1 -1
- package/dist/codegen/surface-snapshot.d.ts +27 -3
- package/dist/codegen/surface-snapshot.d.ts.map +1 -1
- package/dist/codegen/validate.d.ts +11 -3
- package/dist/codegen/validate.d.ts.map +1 -1
- package/dist/components/capability-plugin.d.ts +13 -2
- package/dist/components/capability-plugin.d.ts.map +1 -1
- package/dist/components/driver.d.ts +38 -0
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/starter-plugin.d.ts.map +1 -1
- package/dist/config.d.ts +57 -5
- package/dist/config.d.ts.map +1 -1
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/fold/fold.d.ts +100 -1
- package/dist/fold/fold.d.ts.map +1 -1
- package/dist/graph-ir.d.ts +12 -0
- package/dist/graph-ir.d.ts.map +1 -1
- package/dist/graph-ops.d.ts +17 -0
- package/dist/graph-ops.d.ts.map +1 -0
- package/dist/lifecycle/change-set.d.ts +23 -2
- package/dist/lifecycle/change-set.d.ts.map +1 -1
- package/dist/lifecycle/deep-observe.d.ts.map +1 -1
- package/dist/lifecycle/observe.d.ts +8 -0
- package/dist/lifecycle/observe.d.ts.map +1 -1
- package/dist/lifecycle/replay.d.ts.map +1 -1
- package/dist/lifecycle/snapshot.d.ts.map +1 -1
- package/dist/lifecycle/types.d.ts +7 -0
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/lint/policy.d.ts.map +1 -1
- package/dist/managed-fields.d.ts +33 -21
- package/dist/managed-fields.d.ts.map +1 -1
- package/dist/observation.d.ts +23 -1
- package/dist/observation.d.ts.map +1 -1
- package/dist/op/index.d.ts +1 -1
- package/dist/op/index.d.ts.map +1 -1
- package/dist/yaml.d.ts +0 -8
- package/dist/yaml.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__snapshots__/okf.test.ts.snap +20 -0
- package/src/audit/discover.test.ts +55 -1
- package/src/audit/discover.ts +70 -2
- package/src/audit/edge-init-safety.test.ts +149 -18
- package/src/audit/report-model.ts +12 -1
- package/src/audit/rules-doc.ts +1 -0
- package/src/build-params.test.ts +52 -0
- package/src/cli/build-params-cli.test.ts +25 -8
- package/src/cli/build-params-cli.ts +37 -23
- package/src/cli/commands/__fixtures__/audit-coverage/.github/workflows/ci.yml +5 -0
- package/src/cli/commands/__fixtures__/audit-coverage/Dockerfile +2 -0
- package/src/cli/commands/__fixtures__/audit-coverage/infra/main.tf +3 -0
- package/src/cli/commands/__fixtures__/audit-coverage/infra/stack.json +6 -0
- package/src/cli/commands/__fixtures__/audit-coverage/k8s/deploy.yaml +12 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/pages/getting-started.mdx +16 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content.config.ts +10 -2
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate-cli.ts +3 -1
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate.ts +2 -1
- package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +2 -1
- package/src/cli/commands/audit.test.ts +83 -1
- package/src/cli/commands/audit.ts +121 -28
- package/src/cli/commands/build.test.ts +144 -5
- package/src/cli/commands/build.ts +66 -14
- package/src/cli/commands/check-lexicon-docs.ts +30 -0
- package/src/cli/commands/check-lexicon-examples.test.ts +73 -7
- package/src/cli/commands/check-lexicon-examples.ts +66 -17
- package/src/cli/commands/check-lexicon.test.ts +1 -1
- package/src/cli/commands/check-lexicon.ts +19 -4
- package/src/cli/commands/init-lexicon/templates/codegen.ts +5 -2
- package/src/cli/commands/init-lexicon/templates/docs.ts +35 -2
- package/src/cli/commands/init-lexicon.test.ts +42 -1
- package/src/cli/commands/init-lexicon.ts +3 -1
- package/src/cli/commands/lexicon-rolling-upgrade.ts +1 -1
- package/src/cli/commands/lexicon-surface-diff.test.ts +1 -1
- package/src/cli/commands/lexicon-surface-diff.ts +1 -1
- package/src/cli/commands/onboard.test.ts +319 -225
- package/src/cli/commands/onboard.ts +144 -58
- package/src/cli/handlers/build.test.ts +3 -3
- package/src/cli/handlers/build.ts +2 -0
- package/src/cli/handlers/components.ts +2 -2
- package/src/cli/handlers/graph.test.ts +44 -0
- package/src/cli/handlers/graph.ts +22 -2
- package/src/cli/handlers/lifecycle.test.ts +117 -0
- package/src/cli/handlers/lifecycle.ts +16 -0
- package/src/cli/handlers/run.test.ts +5 -5
- package/src/cli/handlers/run.ts +1 -0
- package/src/cli/handlers/search-live.test.ts +217 -0
- package/src/cli/handlers/search.test.ts +41 -1
- package/src/cli/handlers/search.ts +55 -6
- package/src/cli/main.ts +5 -3
- package/src/cli/security-boundary.test.ts +2 -1
- package/src/codegen/docs-pages.test.ts +194 -0
- package/src/codegen/docs-pages.ts +138 -0
- package/src/codegen/docs-sections.ts +0 -8
- package/src/codegen/docs-sidebar.ts +50 -37
- package/src/codegen/docs-types.ts +32 -4
- package/src/codegen/docs.ts +78 -40
- package/src/codegen/lexicon-regen.ts +3 -1
- package/src/codegen/rolling-upgrade.test.ts +1 -1
- package/src/codegen/rolling-upgrade.ts +1 -1
- package/src/codegen/surface-snapshot.test.ts +139 -0
- package/src/codegen/surface-snapshot.ts +297 -122
- package/src/codegen/validate.test.ts +16 -3
- package/src/codegen/validate.ts +17 -7
- package/src/components/auto-release.ts +1 -1
- package/src/components/capability-plugin.ts +23 -2
- package/src/components/driver.test.ts +45 -0
- package/src/components/driver.ts +64 -27
- package/src/components/index.ts +2 -0
- package/src/components/starter-plugin.ts +5 -2
- package/src/config.test.ts +56 -1
- package/src/config.ts +152 -8
- package/src/discovery/fold-import.test.ts +427 -2
- package/src/discovery/fold-import.ts +163 -7
- package/src/discovery/sandbox/driver.test.ts +11 -0
- package/src/fold/fold.test.ts +41 -0
- package/src/fold/fold.ts +372 -2
- package/src/graph-ir-live.test.ts +42 -0
- package/src/graph-ir.ts +42 -2
- package/src/graph-ops.test.ts +69 -0
- package/src/graph-ops.ts +42 -0
- package/src/lifecycle/change-set.test.ts +41 -0
- package/src/lifecycle/change-set.ts +30 -2
- package/src/lifecycle/deep-observe.ts +4 -0
- package/src/lifecycle/observe.ts +29 -2
- package/src/lifecycle/replay.ts +3 -0
- package/src/lifecycle/snapshot.ts +3 -0
- package/src/lifecycle/types.ts +7 -0
- package/src/lint/policy.ts +14 -3
- package/src/managed-fields.test.ts +15 -42
- package/src/managed-fields.ts +43 -26
- package/src/observation.test.ts +24 -8
- package/src/observation.ts +48 -4
- package/src/op/builders-exports.test.ts +60 -0
- package/src/op/index.ts +1 -1
- package/src/ownership.test.ts +63 -1
- package/src/yaml.test.ts +15 -0
- package/src/yaml.ts +19 -2
|
@@ -62,7 +62,8 @@ export async function generate(options?: { verbose?: boolean; force?: boolean })
|
|
|
62
62
|
* Write generated files to the package directory.
|
|
63
63
|
*/
|
|
64
64
|
export function writeGeneratedFiles(result: GenerateResult, pkgDir?: string): void {
|
|
65
|
-
|
|
65
|
+
// This file lives in src/codegen/, so the package root is three levels up.
|
|
66
|
+
const dir = pkgDir ?? dirname(dirname(dirname(fileURLToPath(import.meta.url))));
|
|
66
67
|
writeGeneratedArtifacts({
|
|
67
68
|
baseDir: dir,
|
|
68
69
|
files: {
|
|
@@ -65,7 +65,8 @@ export async function generate(options?: { verbose?: boolean; force?: boolean })
|
|
|
65
65
|
* Write generated files to the package directory.
|
|
66
66
|
*/
|
|
67
67
|
export function writeGeneratedFiles(result: GenerateResult, pkgDir?: string): void {
|
|
68
|
-
|
|
68
|
+
// This file lives in src/codegen/, so the package root is three levels up.
|
|
69
|
+
const dir = pkgDir ?? dirname(dirname(dirname(fileURLToPath(import.meta.url))));
|
|
69
70
|
writeGeneratedArtifacts({
|
|
70
71
|
baseDir: dir,
|
|
71
72
|
files: {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, test, expect } from "vitest";
|
|
2
2
|
import { fileURLToPath } from "url";
|
|
3
|
-
import { auditCommand, tokenForHost, coverageNotes } from "./audit";
|
|
3
|
+
import { auditCommand, tokenForHost, coverageNotes, installLine, NO_LEXICONS_EXIT_CODE } from "./audit";
|
|
4
4
|
import { discoverByDetection, loadAuditPlugins } from "../../audit/discover";
|
|
5
5
|
import { MissingLexiconError, type AuditInput, type AuditLexicon } from "../../audit/core";
|
|
6
6
|
|
|
@@ -190,6 +190,88 @@ describe("auditCommand", () => {
|
|
|
190
190
|
expect(result.error).toMatch(/npm i @intentius\/chant-lexicon-github/);
|
|
191
191
|
});
|
|
192
192
|
|
|
193
|
+
describe("lexicon coverage (#1623)", () => {
|
|
194
|
+
const MIXED = fileURLToPath(new URL("./__fixtures__/audit-coverage", import.meta.url));
|
|
195
|
+
|
|
196
|
+
test("zero lexicons: says it had nothing to look with, names each file's lexicon, prints the npx line, exits 2", async () => {
|
|
197
|
+
const result = await auditCommand({ path: MIXED, plugins: [] });
|
|
198
|
+
expect(result.success).toBe(true);
|
|
199
|
+
expect(result.status).toBe("no-lexicons");
|
|
200
|
+
expect(result.exitCode).toBe(NO_LEXICONS_EXIT_CODE);
|
|
201
|
+
expect(result.stream).toBe("stderr");
|
|
202
|
+
expect(result.findings).toEqual([]);
|
|
203
|
+
expect(result.output).toContain("nothing to look with");
|
|
204
|
+
expect(result.output).not.toMatch(/Audited \d+ files/);
|
|
205
|
+
expect(result.output).toContain(".github/workflows/ci.yml -> github");
|
|
206
|
+
expect(result.output).toContain("k8s/deploy.yaml -> k8s");
|
|
207
|
+
expect(result.output).toContain("infra/stack.json -> aws");
|
|
208
|
+
expect(result.output).toContain("Dockerfile -> docker");
|
|
209
|
+
expect(result.output).toContain("infra/main.tf -> terraform (not audited; see chant carve)");
|
|
210
|
+
// The install line names only the lexicons the files wanted, never terraform.
|
|
211
|
+
expect(result.output).toContain(
|
|
212
|
+
`npx -p @intentius/chant -p @intentius/chant-lexicon-github -p @intentius/chant-lexicon-docker -p @intentius/chant-lexicon-aws -p @intentius/chant-lexicon-k8s chant audit ${MIXED}`,
|
|
213
|
+
);
|
|
214
|
+
expect(result.output).not.toContain("chant-lexicon-terraform");
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("zero lexicons with --json: status no-lexicons plus the unclaimed list and install line", async () => {
|
|
218
|
+
const result = await auditCommand({ path: MIXED, plugins: [], format: "json" });
|
|
219
|
+
expect(result.exitCode).toBe(NO_LEXICONS_EXIT_CODE);
|
|
220
|
+
expect(result.stream).toBe("stdout");
|
|
221
|
+
const json = JSON.parse(result.output);
|
|
222
|
+
expect(json.status).toBe("no-lexicons");
|
|
223
|
+
expect(json.findings).toEqual([]);
|
|
224
|
+
expect(json.missingLexicons).toEqual(["github", "docker", "aws", "k8s"]);
|
|
225
|
+
expect(json.unclaimed).toContainEqual({ path: "k8s/deploy.yaml", lexicon: "k8s" });
|
|
226
|
+
expect(json.unclaimed).toContainEqual({ path: "infra/main.tf", lexicon: "terraform" });
|
|
227
|
+
expect(json.install).toMatch(/^npx -p @intentius\/chant /);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("zero lexicons on an empty dir still refuses to report clean", async () => {
|
|
231
|
+
const tmp = join(tmpdir(), `chant-audit-nolex-${process.pid}`);
|
|
232
|
+
const { mkdirSync } = await import("fs");
|
|
233
|
+
mkdirSync(tmp, { recursive: true });
|
|
234
|
+
const result = await auditCommand({ path: tmp, plugins: [] });
|
|
235
|
+
expect(result.status).toBe("no-lexicons");
|
|
236
|
+
expect(result.exitCode).toBe(NO_LEXICONS_EXIT_CODE);
|
|
237
|
+
expect(result.output).toContain("nothing to look with");
|
|
238
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("partial: github loaded, k8s/aws/docker absent -> audit runs and a one-line hint names the gap", async () => {
|
|
242
|
+
const all = await loadAuditPlugins();
|
|
243
|
+
const github = all.filter((p) => p.name === "github");
|
|
244
|
+
expect(github).toHaveLength(1);
|
|
245
|
+
const result = await auditCommand({ path: MIXED, plugins: github });
|
|
246
|
+
expect(result.status).toBe("ok");
|
|
247
|
+
expect(result.exitCode).toBe(0);
|
|
248
|
+
expect(result.scanned).toEqual([".github/workflows/ci.yml"]);
|
|
249
|
+
expect(result.output).toMatch(/^Note: 3 files look like docker\/aws\/k8s but those lexicons are not installed, so they were skipped \(npm i @intentius\/chant-lexicon-docker @intentius\/chant-lexicon-aws @intentius\/chant-lexicon-k8s\)\./);
|
|
250
|
+
expect(result.output).toContain("1 Terraform file skipped; the audit does not read HCL (see chant carve).");
|
|
251
|
+
expect(result.unclaimed).toContainEqual({ path: "k8s/deploy.yaml", lexicon: "k8s" });
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("partial with --json: status ok and the unclaimed files ride along", async () => {
|
|
255
|
+
const all = await loadAuditPlugins();
|
|
256
|
+
const result = await auditCommand({ path: MIXED, plugins: all.filter((p) => p.name === "github"), format: "json" });
|
|
257
|
+
const json = JSON.parse(result.output);
|
|
258
|
+
expect(json.status).toBe("ok");
|
|
259
|
+
expect(json.unclaimed.map((u: { lexicon: string }) => u.lexicon).sort()).toEqual(["aws", "docker", "k8s", "terraform"]);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("all lexicons loaded: no coverage note, no unclaimed (terraform aside)", async () => {
|
|
263
|
+
const result = await auditCommand({ path: MIXED, plugins: await loadAuditPlugins() });
|
|
264
|
+
expect(result.status).toBe("ok");
|
|
265
|
+
expect(result.unclaimed).toEqual([{ path: "infra/main.tf", lexicon: "terraform" }]);
|
|
266
|
+
expect(result.output).not.toContain("not installed");
|
|
267
|
+
expect(result.scanned.sort()).toEqual([".github/workflows/ci.yml", "Dockerfile", "infra/stack.json", "k8s/deploy.yaml"]);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("installLine puts every lexicon on the same npx -p path", () => {
|
|
271
|
+
expect(installLine(["github", "gitlab"], ".")).toBe("npx -p @intentius/chant -p @intentius/chant-lexicon-github -p @intentius/chant-lexicon-gitlab chant audit .");
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
193
275
|
test("a path with no CI files succeeds with a clear message", async () => {
|
|
194
276
|
const tmp = join(tmpdir(), `chant-audit-empty-${process.pid}`);
|
|
195
277
|
const { mkdirSync } = await import("fs");
|
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
|
|
8
8
|
import { existsSync, statSync, writeFileSync } from "fs";
|
|
9
9
|
import { auditFiles, type AuditInput, type AuditFinding, type ChecksProvider } from "../../audit/core";
|
|
10
|
-
import {
|
|
10
|
+
import { AUDIT_LEXICONS, classifyFiles, collectCandidates, loadAuditPlugins, unclaimedFiles, type DetectPlugin, type RepoFile, type UnclaimedFile } from "../../audit/discover";
|
|
11
11
|
import { RULE_CATALOG, resolveAuditCatalog, type RuleMeta } from "../../audit/catalog";
|
|
12
12
|
import { renderMarkdown } from "../../audit/report";
|
|
13
13
|
import { renderHtml, type ReportTheme } from "../../audit/report-html";
|
|
14
|
-
import { buildReportJson, type AuditSnapshot } from "../../audit/report-model";
|
|
14
|
+
import { buildReportJson, REPORT_SCHEMA_VERSION, type AuditSnapshot } from "../../audit/report-model";
|
|
15
15
|
import { fetchRepoFiles, resolveActionSha, resolveImageDigest, resolveRepoCommit, parseRepoUrl, FetchError } from "../../audit/fetch";
|
|
16
16
|
import { extractUnpinnedActions, extractUnpinnedImages } from "../../audit/proof";
|
|
17
17
|
import type { ProveOptions } from "../../audit/proof";
|
|
@@ -45,6 +45,8 @@ export interface AuditCommandOptions {
|
|
|
45
45
|
now?: string;
|
|
46
46
|
/** Tool version recorded in the HTML snapshot. */
|
|
47
47
|
toolVersion?: string;
|
|
48
|
+
/** Injectable detection plugins (testing); defaults to every installed audit lexicon. */
|
|
49
|
+
plugins?: DetectPlugin[];
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
export interface AuditCommandResult {
|
|
@@ -58,6 +60,94 @@ export interface AuditCommandResult {
|
|
|
58
60
|
error?: string;
|
|
59
61
|
/** Set when the report was written to a file (via `output`). */
|
|
60
62
|
wroteTo?: string;
|
|
63
|
+
/**
|
|
64
|
+
* `"no-lexicons"` when not a single audit lexicon resolved, so nothing was
|
|
65
|
+
* inspected (#1623). `output` then carries the diagnostic, not a report.
|
|
66
|
+
*/
|
|
67
|
+
status?: "ok" | "no-lexicons";
|
|
68
|
+
/** Candidate files that looked like they wanted a lexicon that is not installed. */
|
|
69
|
+
unclaimed?: UnclaimedFile[];
|
|
70
|
+
/** Where `output` belongs; diagnostics go to stderr, reports to stdout (default). */
|
|
71
|
+
stream?: "stdout" | "stderr";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Exit code when the audit had no lexicons to look with. Distinct from 1 (findings / failure). */
|
|
75
|
+
export const NO_LEXICONS_EXIT_CODE = 2;
|
|
76
|
+
|
|
77
|
+
/** The npm package that provides an audit lexicon's detection and checks. */
|
|
78
|
+
function lexiconPackage(name: string): string {
|
|
79
|
+
return `@intentius/chant-lexicon-${name}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Missing audit lexicons the unclaimed files pointed at, in first-seen order. `terraform` is not installable. */
|
|
83
|
+
function wantedLexicons(unclaimed: UnclaimedFile[]): string[] {
|
|
84
|
+
return [...new Set(unclaimed.map((u) => u.lexicon))].filter((l) => l !== "terraform");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The exact one-liner that gives a bare `npx @intentius/chant audit` the
|
|
89
|
+
* lexicons it needs. Every wanted lexicon is a `-p` package so npx puts all of
|
|
90
|
+
* them on the same resolution path.
|
|
91
|
+
*/
|
|
92
|
+
export function installLine(lexicons: string[], target: string): string {
|
|
93
|
+
const pkgs = ["@intentius/chant", ...lexicons.map(lexiconPackage)];
|
|
94
|
+
return `npx ${pkgs.map((p) => `-p ${p}`).join(" ")} chant audit ${target}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** One-line coverage hint for the partial case: some lexicons loaded, others wanted by files on disk. */
|
|
98
|
+
function missingLexiconHint(unclaimed: UnclaimedFile[]): string | undefined {
|
|
99
|
+
const wanted = wantedLexicons(unclaimed);
|
|
100
|
+
const tf = unclaimed.filter((u) => u.lexicon === "terraform").length;
|
|
101
|
+
const parts: string[] = [];
|
|
102
|
+
if (wanted.length > 0) {
|
|
103
|
+
const n = unclaimed.length - tf;
|
|
104
|
+
parts.push(
|
|
105
|
+
`${n} file${n === 1 ? " looks" : "s look"} like ${wanted.join("/")} but ${wanted.length === 1 ? "that lexicon is" : "those lexicons are"} not installed, so ${n === 1 ? "it was" : "they were"} skipped` +
|
|
106
|
+
` (npm i ${wanted.map(lexiconPackage).join(" ")}).`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
if (tf > 0) parts.push(`${tf} Terraform file${tf === 1 ? "" : "s"} skipped; the audit does not read HCL (see chant carve).`);
|
|
110
|
+
return parts.length > 0 ? parts.join(" ") : undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Human-readable diagnostic for the zero-lexicon case. */
|
|
114
|
+
function renderNoLexicons(target: string, unclaimed: UnclaimedFile[]): string {
|
|
115
|
+
const lines: string[] = [];
|
|
116
|
+
lines.push(`chant audit had nothing to look with: no audit lexicon is installed, so nothing under ${target} was inspected.`);
|
|
117
|
+
lines.push("This is not a clean result. Detection and checks live in the lexicon packages.");
|
|
118
|
+
const wanted = wantedLexicons(unclaimed);
|
|
119
|
+
if (unclaimed.length > 0) {
|
|
120
|
+
lines.push("", "Files that wanted a lexicon:");
|
|
121
|
+
for (const u of unclaimed) {
|
|
122
|
+
const note = u.lexicon === "terraform" ? "terraform (not audited; see chant carve)" : u.lexicon;
|
|
123
|
+
lines.push(` ${u.path} -> ${note}`);
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
lines.push("", "No file under the target looked like CI, Kubernetes, Helm, Docker, CloudFormation, ARM, or Config Connector either.");
|
|
127
|
+
}
|
|
128
|
+
lines.push("", "Run it with the lexicons those files need:");
|
|
129
|
+
lines.push(` ${installLine(wanted.length > 0 ? wanted : [...AUDIT_LEXICONS], target)}`);
|
|
130
|
+
return lines.join("\n");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Machine-readable form of the zero-lexicon diagnostic (`status: "no-lexicons"`). */
|
|
134
|
+
function renderNoLexiconsJson(target: string, unclaimed: UnclaimedFile[]): string {
|
|
135
|
+
const wanted = wantedLexicons(unclaimed);
|
|
136
|
+
return JSON.stringify(
|
|
137
|
+
{
|
|
138
|
+
schemaVersion: REPORT_SCHEMA_VERSION,
|
|
139
|
+
tool: { name: "chant-audit" },
|
|
140
|
+
status: "no-lexicons",
|
|
141
|
+
target,
|
|
142
|
+
summary: { total: 0 },
|
|
143
|
+
findings: [],
|
|
144
|
+
unclaimed,
|
|
145
|
+
missingLexicons: wanted,
|
|
146
|
+
install: installLine(wanted.length > 0 ? wanted : [...AUDIT_LEXICONS], target),
|
|
147
|
+
},
|
|
148
|
+
null,
|
|
149
|
+
2,
|
|
150
|
+
);
|
|
61
151
|
}
|
|
62
152
|
|
|
63
153
|
/**
|
|
@@ -205,20 +295,21 @@ export async function auditCommand(options: AuditCommandOptions): Promise<AuditC
|
|
|
205
295
|
|
|
206
296
|
const isUrl = /^https?:\/\//.test(options.path);
|
|
207
297
|
|
|
208
|
-
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
|
|
298
|
+
// Detection lives in the lexicon plugins (each one's `detectTemplate`), so a
|
|
299
|
+
// lexicon that isn't installed can't claim its files. Every branch below
|
|
300
|
+
// therefore also computes which candidate files looked like they wanted an
|
|
301
|
+
// absent lexicon, so "nothing found" and "had nothing to look with" never
|
|
302
|
+
// render the same way (#1623).
|
|
303
|
+
const plugins = options.plugins ?? (await loadAuditPlugins());
|
|
304
|
+
let candidates: RepoFile[];
|
|
213
305
|
if (isUrl) {
|
|
214
306
|
try {
|
|
215
307
|
// Fetch the whole repo's candidate files (all lexicons, not just CI) and
|
|
216
308
|
// run them through the same classifier the local path uses (#420).
|
|
217
|
-
|
|
309
|
+
candidates = await fetchRepoFiles(options.path, {
|
|
218
310
|
token: options.token ?? tokenForHost(options.path),
|
|
219
311
|
fetchImpl: options.fetchImpl,
|
|
220
312
|
});
|
|
221
|
-
inputs = classifyFiles(files, await loadAuditPlugins());
|
|
222
313
|
} catch (err) {
|
|
223
314
|
const msg = err instanceof FetchError ? err.message : err instanceof Error ? err.message : String(err);
|
|
224
315
|
return { success: false, output: "", findings: [], scanned: [], exitCode: 1, error: msg };
|
|
@@ -227,25 +318,25 @@ export async function auditCommand(options: AuditCommandOptions): Promise<AuditC
|
|
|
227
318
|
if (!existsSync(options.path)) {
|
|
228
319
|
return { success: false, output: "", findings: [], scanned: [], exitCode: 1, error: `Path not found: ${options.path}` };
|
|
229
320
|
}
|
|
230
|
-
// One walk, plugin-delegated detection.
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
|
|
234
|
-
const plugins = await loadAuditPlugins();
|
|
235
|
-
const loaded = new Set(plugins.map((p) => p.name));
|
|
236
|
-
const missing = AUDIT_LEXICONS.filter((n) => !loaded.has(n));
|
|
237
|
-
if (missing.length > 0) {
|
|
238
|
-
missingHint =
|
|
239
|
-
` Some lexicon packages are not installed, so files of those types were not detected: ${missing.join(", ")}.` +
|
|
240
|
-
` Install what you need, e.g. npm i ${missing.map((n) => `@intentius/chant-lexicon-${n}`).join(" ")}`;
|
|
241
|
-
}
|
|
242
|
-
inputs = discoverByDetection(options.path, plugins);
|
|
321
|
+
// One walk, plugin-delegated detection. CI (path), Dockerfiles (name), and
|
|
322
|
+
// Helm charts (bundle) are special-cased by the classifier since content
|
|
323
|
+
// shape alone can't disambiguate them.
|
|
324
|
+
candidates = collectCandidates(options.path);
|
|
243
325
|
}
|
|
244
|
-
|
|
326
|
+
const inputs = classifyFiles(candidates, plugins);
|
|
327
|
+
const unclaimed = unclaimedFiles(candidates, inputs, plugins);
|
|
245
328
|
const scanned = inputs.map((i) => i.path);
|
|
246
329
|
|
|
330
|
+
if (plugins.length === 0) {
|
|
331
|
+
const output = format === "json" ? renderNoLexiconsJson(options.path, unclaimed) : renderNoLexicons(options.path, unclaimed);
|
|
332
|
+
return { success: true, status: "no-lexicons", output, findings: [], scanned: [], unclaimed, exitCode: NO_LEXICONS_EXIT_CODE, stream: format === "json" ? "stdout" : "stderr" };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const missingLexiconNote = missingLexiconHint(unclaimed);
|
|
336
|
+
|
|
247
337
|
if (inputs.length === 0) {
|
|
248
|
-
|
|
338
|
+
const output = `No auditable files found under ${options.path}.${missingLexiconNote ? ` ${missingLexiconNote}` : ""}`;
|
|
339
|
+
return { success: true, status: "ok", output, findings: [], scanned: [], unclaimed, exitCode: 0 };
|
|
249
340
|
}
|
|
250
341
|
|
|
251
342
|
let findings: AuditFinding[];
|
|
@@ -262,6 +353,7 @@ export async function auditCommand(options: AuditCommandOptions): Promise<AuditC
|
|
|
262
353
|
|
|
263
354
|
if (tier === "merge-worthy") findings = findings.filter((f) => isMergeWorthy(f, catalog));
|
|
264
355
|
const notes = coverageNotes(inputs);
|
|
356
|
+
if (missingLexiconNote) notes.push(missingLexiconNote);
|
|
265
357
|
|
|
266
358
|
// Diff-bearing renderers (markdown, html) need action SHAs / image digests
|
|
267
359
|
// resolved up front (sync maps so rendering stays synchronous).
|
|
@@ -304,7 +396,7 @@ export async function auditCommand(options: AuditCommandOptions): Promise<AuditC
|
|
|
304
396
|
switch (format) {
|
|
305
397
|
case "json": {
|
|
306
398
|
const snapshot = await buildSnapshot(options, scanned, isUrl);
|
|
307
|
-
output = JSON.stringify(buildReportJson(findings, { snapshot, toolVersion: options.toolVersion, catalog }), null, 2);
|
|
399
|
+
output = JSON.stringify(buildReportJson(findings, { snapshot, toolVersion: options.toolVersion, catalog, unclaimed }), null, 2);
|
|
308
400
|
break;
|
|
309
401
|
}
|
|
310
402
|
case "sarif":
|
|
@@ -329,10 +421,10 @@ export async function auditCommand(options: AuditCommandOptions): Promise<AuditC
|
|
|
329
421
|
} catch (err) {
|
|
330
422
|
return { success: false, output, findings, scanned, exitCode: 1, error: `Failed to write ${options.output}: ${err instanceof Error ? err.message : String(err)}` };
|
|
331
423
|
}
|
|
332
|
-
return { success: true, output, findings, scanned, exitCode, wroteTo: options.output };
|
|
424
|
+
return { success: true, status: "ok", output, findings, scanned, unclaimed, exitCode, wroteTo: options.output };
|
|
333
425
|
}
|
|
334
426
|
|
|
335
|
-
return { success: true, output, findings, scanned, exitCode };
|
|
427
|
+
return { success: true, status: "ok", output, findings, scanned, unclaimed, exitCode };
|
|
336
428
|
}
|
|
337
429
|
|
|
338
430
|
/** Print an audit result to stdout. */
|
|
@@ -345,5 +437,6 @@ export function printAuditResult(result: AuditCommandResult): void {
|
|
|
345
437
|
console.error(`Wrote report to ${result.wroteTo}`);
|
|
346
438
|
return;
|
|
347
439
|
}
|
|
348
|
-
console.
|
|
440
|
+
if (result.stream === "stderr") console.error(result.output);
|
|
441
|
+
else console.log(result.output);
|
|
349
442
|
}
|
|
@@ -169,6 +169,7 @@ export const testEntity = {
|
|
|
169
169
|
format: "json",
|
|
170
170
|
serializers: [awsSerializer],
|
|
171
171
|
fold: true,
|
|
172
|
+
verbose: true,
|
|
172
173
|
});
|
|
173
174
|
|
|
174
175
|
expect(result.success).toBe(true);
|
|
@@ -183,6 +184,131 @@ export const testEntity = {
|
|
|
183
184
|
}
|
|
184
185
|
});
|
|
185
186
|
|
|
187
|
+
describe("#1396 — ownership.env resolves from the env build parameter", () => {
|
|
188
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
189
|
+
const runtimePath = resolvePath(thisDir, "../../runtime");
|
|
190
|
+
const paramsPath = resolvePath(thisDir, "../../params");
|
|
191
|
+
|
|
192
|
+
// A label-stamping serializer in miniature: the resource's own `env` prop
|
|
193
|
+
// (from params.env) next to the ownership marker's env (from context).
|
|
194
|
+
const labelSerializer: Serializer = {
|
|
195
|
+
name: "aws",
|
|
196
|
+
rulePrefix: "TEST",
|
|
197
|
+
serialize: (entities, _outputs, context) =>
|
|
198
|
+
JSON.stringify(
|
|
199
|
+
[...entities.values()].map((e) => ({
|
|
200
|
+
label: (e as unknown as { props: { env: string } }).props.env,
|
|
201
|
+
marker: context?.ownership,
|
|
202
|
+
})),
|
|
203
|
+
),
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
async function writeProject(ownershipEnv: string) {
|
|
207
|
+
await writeFile(
|
|
208
|
+
join(testDir, "chant.config.ts"),
|
|
209
|
+
`
|
|
210
|
+
export default {
|
|
211
|
+
ownership: { stack: "fountain", env: ${ownershipEnv} },
|
|
212
|
+
buildParams: {
|
|
213
|
+
env: { type: "string", default: "dev", env: "CHANT_TEST_1396_ENV" },
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
`,
|
|
217
|
+
);
|
|
218
|
+
await writeFile(
|
|
219
|
+
join(testDir, "resources.ts"),
|
|
220
|
+
`
|
|
221
|
+
import { createResource } from ${JSON.stringify(runtimePath)};
|
|
222
|
+
export const Bucket = createResource("Test::Bucket", "aws", { arn: "Arn" });
|
|
223
|
+
`,
|
|
224
|
+
);
|
|
225
|
+
await writeFile(
|
|
226
|
+
join(testDir, "main.ts"),
|
|
227
|
+
`
|
|
228
|
+
import { Bucket } from "./resources";
|
|
229
|
+
import { params } from ${JSON.stringify(paramsPath)};
|
|
230
|
+
export const bucket = new Bucket({ env: params.env });
|
|
231
|
+
`,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function built(): Array<{ label: string; marker?: { stack: string; env?: string } }> {
|
|
236
|
+
return JSON.parse(readFileSync(outputFile, "utf-8"));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
test("--param env=prod drives both the label and the ownership marker", async () => {
|
|
240
|
+
await writeProject(`{ param: "env" }`);
|
|
241
|
+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
242
|
+
try {
|
|
243
|
+
const result = await buildCommand({
|
|
244
|
+
path: testDir,
|
|
245
|
+
output: outputFile,
|
|
246
|
+
format: "json",
|
|
247
|
+
serializers: [labelSerializer],
|
|
248
|
+
params: { env: "prod" },
|
|
249
|
+
});
|
|
250
|
+
expect(result.errors).toEqual([]);
|
|
251
|
+
expect(result.success).toBe(true);
|
|
252
|
+
expect(built()).toEqual([{ label: "prod", marker: { stack: "fountain", env: "prod" } }]);
|
|
253
|
+
} finally {
|
|
254
|
+
errorSpy.mockRestore();
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("with no parameter supplied, both follow the declared default", async () => {
|
|
259
|
+
await writeProject(`{ param: "env" }`);
|
|
260
|
+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
261
|
+
try {
|
|
262
|
+
const result = await buildCommand({
|
|
263
|
+
path: testDir,
|
|
264
|
+
output: outputFile,
|
|
265
|
+
format: "json",
|
|
266
|
+
serializers: [labelSerializer],
|
|
267
|
+
});
|
|
268
|
+
expect(result.success).toBe(true);
|
|
269
|
+
expect(built()).toEqual([{ label: "dev", marker: { stack: "fountain", env: "dev" } }]);
|
|
270
|
+
} finally {
|
|
271
|
+
errorSpy.mockRestore();
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("a literal ownership.env that disagrees with --param env is warned about", async () => {
|
|
276
|
+
await writeProject(`"dev"`);
|
|
277
|
+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
278
|
+
try {
|
|
279
|
+
const result = await buildCommand({
|
|
280
|
+
path: testDir,
|
|
281
|
+
output: outputFile,
|
|
282
|
+
format: "json",
|
|
283
|
+
serializers: [labelSerializer],
|
|
284
|
+
params: { env: "prod" },
|
|
285
|
+
});
|
|
286
|
+
expect(result.success).toBe(true);
|
|
287
|
+
expect(built()).toEqual([{ label: "prod", marker: { stack: "fountain", env: "dev" } }]);
|
|
288
|
+
expect(result.warnings.some((w) => w.includes('ownership.env is "dev"') && w.includes('"prod"'))).toBe(true);
|
|
289
|
+
} finally {
|
|
290
|
+
errorSpy.mockRestore();
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("a reference to an undeclared parameter fails the build", async () => {
|
|
295
|
+
await writeProject(`{ param: "environment" }`);
|
|
296
|
+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
297
|
+
try {
|
|
298
|
+
const result = await buildCommand({
|
|
299
|
+
path: testDir,
|
|
300
|
+
output: outputFile,
|
|
301
|
+
format: "json",
|
|
302
|
+
serializers: [labelSerializer],
|
|
303
|
+
});
|
|
304
|
+
expect(result.success).toBe(false);
|
|
305
|
+
expect(result.errors.some((e) => e.includes('build parameter "environment"'))).toBe(true);
|
|
306
|
+
} finally {
|
|
307
|
+
errorSpy.mockRestore();
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
186
312
|
test("#1064 — a declared build-time parameter binds to params.<name> and folds to a literal", async () => {
|
|
187
313
|
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
188
314
|
const runtimePath = resolvePath(thisDir, "../../runtime");
|
|
@@ -235,10 +361,10 @@ export const testEntity = {
|
|
|
235
361
|
expect(result.resourceCount).toBe(1);
|
|
236
362
|
expect(result.buildParams).toEqual([{ name: "tier", value: "production", source: "cli" }]);
|
|
237
363
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
expect(
|
|
364
|
+
// #1424 — without --verbose the echo is a one-line count by source.
|
|
365
|
+
const logged = errorSpy.mock.calls.map((call) => String(call[0]));
|
|
366
|
+
expect(logged.some((line) => line.includes("1 build parameter resolved (1 from cli)"))).toBe(true);
|
|
367
|
+
expect(logged.some((line) => line.includes("[param] tier"))).toBe(false);
|
|
242
368
|
} finally {
|
|
243
369
|
errorSpy.mockRestore();
|
|
244
370
|
}
|
|
@@ -346,6 +472,19 @@ export const testEntity = {
|
|
|
346
472
|
|
|
347
473
|
expect(result.success).toBe(true);
|
|
348
474
|
expect(result.resourceCount).toBe(1);
|
|
475
|
+
// #1424 — the default report is one summary line, not one line per file.
|
|
476
|
+
const logged = errorSpy.mock.calls.map((call) => String(call[0]));
|
|
477
|
+
expect(logged.some((line) => /^.*fold: \d+ files? folded, \d+ ran/.test(line))).toBe(true);
|
|
478
|
+
expect(logged.some((line) => line.includes("[fold:"))).toBe(false);
|
|
479
|
+
|
|
480
|
+
errorSpy.mockClear();
|
|
481
|
+
const verboseResult = await buildCommand({
|
|
482
|
+
path: testDir,
|
|
483
|
+
format: "json",
|
|
484
|
+
serializers: [mockSerializer],
|
|
485
|
+
verbose: true,
|
|
486
|
+
});
|
|
487
|
+
expect(verboseResult.success).toBe(true);
|
|
349
488
|
const anyFoldLine = errorSpy.mock.calls.map((call) => String(call[0])).some((line) => line.includes("[fold:"));
|
|
350
489
|
expect(anyFoldLine).toBe(true);
|
|
351
490
|
|
|
@@ -360,7 +499,7 @@ export const testEntity = {
|
|
|
360
499
|
expect(runResult.resourceCount).toBe(1);
|
|
361
500
|
const anyFoldLineOff = errorSpy.mock.calls
|
|
362
501
|
.map((call) => String(call[0]))
|
|
363
|
-
.some((line) => line.includes("[fold:"));
|
|
502
|
+
.some((line) => line.includes("[fold:") || line.includes("fold: "));
|
|
364
503
|
expect(anyFoldLineOff).toBe(false);
|
|
365
504
|
} finally {
|
|
366
505
|
errorSpy.mockRestore();
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { build } from "../../build";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
loadChantConfigUpward,
|
|
4
|
+
resolveOwnershipMarker,
|
|
5
|
+
resolveOwnershipEnv,
|
|
6
|
+
ownershipEnvDisagreement,
|
|
7
|
+
resolveFoldEnabled,
|
|
8
|
+
resolveSandboxEnabled,
|
|
9
|
+
} from "../../config";
|
|
10
|
+
import type { OwnershipMarker } from "../../ownership";
|
|
3
11
|
import { resolveCliBuildParams } from "../build-params-cli";
|
|
4
12
|
import type { Serializer, SerializerResult } from "../../serializer";
|
|
5
13
|
import type { LexiconPlugin } from "../../lexicon";
|
|
@@ -141,10 +149,9 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
|
|
|
141
149
|
// its default.
|
|
142
150
|
const loaded = await loadChantConfigUpward(infraPath);
|
|
143
151
|
const config = loaded.config;
|
|
144
|
-
|
|
152
|
+
// The ownership marker is resolved further down, after build parameters —
|
|
153
|
+
// `ownership.env` may reference one (#1396).
|
|
145
154
|
|
|
146
|
-
// Environment for policy evaluation: explicit --env wins, else ownership.env.
|
|
147
|
-
const env = options.env ?? config.ownership?.env;
|
|
148
155
|
// Project-authored organizational policy checks (lint.policies), run over the
|
|
149
156
|
// resolved resources during build. Resolve paths relative to the config dir.
|
|
150
157
|
const configDir = loaded.configPath ? dirname(loaded.configPath) : infraPath;
|
|
@@ -197,11 +204,13 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
|
|
|
197
204
|
// missing required value, a type/enum mismatch) is reported as a chant
|
|
198
205
|
// build error naming the parameter, never a thrown error from inside user
|
|
199
206
|
// source (which is what loomster's hand-rolled `tierFromEnv()`-style
|
|
200
|
-
// validators did before migrating to this mechanism). Also logs
|
|
201
|
-
// resolved value (`[param] name = value
|
|
207
|
+
// validators did before migrating to this mechanism). Also logs a one-line
|
|
208
|
+
// count on success, or every resolved value (`[param] name = value
|
|
209
|
+
// (source)`) under --verbose (#1424).
|
|
202
210
|
const paramsResolution = resolveCliBuildParams(config.buildParams, {
|
|
203
211
|
cli: options.params,
|
|
204
212
|
paramsFile: options.paramsFile,
|
|
213
|
+
verbose: options.verbose,
|
|
205
214
|
});
|
|
206
215
|
if (!paramsResolution.success) {
|
|
207
216
|
errors.push(...paramsResolution.errors);
|
|
@@ -228,6 +237,27 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
|
|
|
228
237
|
);
|
|
229
238
|
}
|
|
230
239
|
|
|
240
|
+
// #1396 — opt-in ownership marking, resolved AFTER build parameters so
|
|
241
|
+
// `ownership.env: { param: "env" }` takes the value `--param env=prod` (or
|
|
242
|
+
// the declared env mapping, or the default) bound to params.env — one
|
|
243
|
+
// source for the label and the marker. A reference that cannot be
|
|
244
|
+
// satisfied is a build error, not a marker silently stamped without an env.
|
|
245
|
+
let ownership: OwnershipMarker | undefined;
|
|
246
|
+
// Environment for policy evaluation: explicit --env wins, else ownership.env
|
|
247
|
+
// (read even when marking itself is off, so policy can still branch on it).
|
|
248
|
+
let env: string | undefined;
|
|
249
|
+
try {
|
|
250
|
+
ownership = resolveOwnershipMarker(config, paramsResolution.provenance);
|
|
251
|
+
env = options.env ?? resolveOwnershipEnv(config, paramsResolution.provenance);
|
|
252
|
+
} catch (err) {
|
|
253
|
+
errors.push(formatError({ message: err instanceof Error ? err.message : String(err) }));
|
|
254
|
+
return { success: false, resourceCount: 0, fileCount: 0, errors, warnings };
|
|
255
|
+
}
|
|
256
|
+
// A literal `ownership.env` next to an `env` build parameter that resolved
|
|
257
|
+
// to something else is the silent divergence #1396 reports — say so.
|
|
258
|
+
const disagreement = ownershipEnvDisagreement(config, paramsResolution.provenance);
|
|
259
|
+
if (disagreement) warnings.push(formatWarning({ message: disagreement }));
|
|
260
|
+
|
|
231
261
|
// #1039 — thread each loaded plugin's registered intrinsics (e.g. AWS's
|
|
232
262
|
// `Sub`) through to the fold path, so a file using a registered intrinsic
|
|
233
263
|
// tagged template folds instead of unconditionally falling back to run.
|
|
@@ -272,14 +302,22 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
|
|
|
272
302
|
});
|
|
273
303
|
|
|
274
304
|
// #1022 — report per-file fold vs run so it's visible what still runs.
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
305
|
+
// #1424 — one line by default; the per-file lines and their reasons are
|
|
306
|
+
// --verbose. A file that ran is the expected outcome for source that
|
|
307
|
+
// computes values in functions, and a dozen "is not foldable" notes read
|
|
308
|
+
// like errors to someone meeting the project for the first time.
|
|
309
|
+
if (fold && result.foldDecisions.length > 0) {
|
|
310
|
+
if (options.verbose) {
|
|
311
|
+
for (const decision of result.foldDecisions) {
|
|
312
|
+
const rel = relative(infraPath, decision.file) || decision.file;
|
|
313
|
+
const detail =
|
|
314
|
+
decision.mode === "fold"
|
|
315
|
+
? `${decision.resourceCount ?? 0} resource(s), no module execution`
|
|
316
|
+
: (decision.reason ?? "fell back to run");
|
|
317
|
+
console.error(formatInfo(`[fold:${decision.mode}] ${rel} — ${detail}`));
|
|
318
|
+
}
|
|
319
|
+
} else {
|
|
320
|
+
console.error(formatInfo(summarizeFoldDecisions(result.foldDecisions)));
|
|
283
321
|
}
|
|
284
322
|
}
|
|
285
323
|
|
|
@@ -587,6 +625,20 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
|
|
|
587
625
|
};
|
|
588
626
|
}
|
|
589
627
|
|
|
628
|
+
/**
|
|
629
|
+
* `fold: 8 files folded, 13 ran (--verbose for reasons)` — the non-verbose
|
|
630
|
+
* report of a fold build's per-file decisions (#1424). The "(--verbose for
|
|
631
|
+
* reasons)" hint appears only when something ran, since that is the only case
|
|
632
|
+
* with a reason to read.
|
|
633
|
+
*/
|
|
634
|
+
export function summarizeFoldDecisions(decisions: readonly { mode: string }[]): string {
|
|
635
|
+
const folded = decisions.filter((d) => d.mode === "fold").length;
|
|
636
|
+
const ran = decisions.length - folded;
|
|
637
|
+
const files = (n: number) => `${n} file${n === 1 ? "" : "s"}`;
|
|
638
|
+
const hint = ran > 0 ? " (--verbose for reasons)" : "";
|
|
639
|
+
return `fold: ${files(folded)} folded, ${ran} ran${hint}`;
|
|
640
|
+
}
|
|
641
|
+
|
|
590
642
|
/**
|
|
591
643
|
* Simple JSON to YAML converter
|
|
592
644
|
*/
|