@intentius/chant 0.49.0 → 0.51.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/catalog.d.ts +13 -3
- package/dist/audit/catalog.d.ts.map +1 -1
- package/dist/audit/core.d.ts +9 -0
- package/dist/audit/core.d.ts.map +1 -1
- package/dist/audit/discover.d.ts +6 -0
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/audit/fetch.d.ts.map +1 -1
- package/dist/audit/report-html.d.ts.map +1 -1
- package/dist/audit/report-model.d.ts +6 -0
- package/dist/audit/report-model.d.ts.map +1 -1
- package/dist/audit/report.d.ts.map +1 -1
- package/dist/audit/rules-doc.d.ts.map +1 -1
- package/dist/audit/secrets.d.ts +95 -0
- package/dist/audit/secrets.d.ts.map +1 -0
- package/dist/audit/wrangler.d.ts +33 -0
- package/dist/audit/wrangler.d.ts.map +1 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/cli/commands/audit.d.ts +7 -0
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts +23 -0
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/components.d.ts +31 -0
- package/dist/cli/handlers/components.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts +11 -0
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/op-progress.d.ts +57 -0
- package/dist/cli/handlers/op-progress.d.ts.map +1 -0
- package/dist/cli/handlers/operator.d.ts +32 -0
- package/dist/cli/handlers/operator.d.ts.map +1 -0
- package/dist/cli/handlers/run-client.d.ts +21 -1
- package/dist/cli/handlers/run-client.d.ts.map +1 -1
- package/dist/cli/handlers/run-report.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/scenario.d.ts +39 -0
- package/dist/cli/handlers/scenario.d.ts.map +1 -0
- package/dist/cli/handlers/search.d.ts +22 -0
- package/dist/cli/handlers/search.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/op-tools.d.ts.map +1 -1
- package/dist/cli/mcp/resource-handlers.d.ts.map +1 -1
- package/dist/cli/mcp/server.d.ts +35 -2
- package/dist/cli/mcp/server.d.ts.map +1 -1
- package/dist/cli/mcp/types.d.ts +29 -1
- package/dist/cli/mcp/types.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +47 -3
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/codegen/docs-rule-scanning.d.ts.map +1 -1
- package/dist/components/capability.d.ts +17 -2
- package/dist/components/capability.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +7 -0
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/component.d.ts +15 -0
- package/dist/components/component.d.ts.map +1 -1
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/run-progress.d.ts +7 -5
- package/dist/components/run-progress.d.ts.map +1 -1
- package/dist/components/verbs/index.d.ts +6 -1
- package/dist/components/verbs/index.d.ts.map +1 -1
- package/dist/components/verbs/run-agent.d.ts +499 -0
- package/dist/components/verbs/run-agent.d.ts.map +1 -0
- package/dist/components/verbs/sign.d.ts +30 -0
- package/dist/components/verbs/sign.d.ts.map +1 -1
- package/dist/composite.d.ts +6 -1
- package/dist/composite.d.ts.map +1 -1
- package/dist/discovery/collect.d.ts.map +1 -1
- package/dist/discovery/fold-import.d.ts +15 -1
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/fold-rank.d.ts +66 -0
- package/dist/discovery/fold-rank.d.ts.map +1 -0
- package/dist/discovery/index.d.ts +15 -0
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/discovery/param-deps.d.ts +17 -0
- package/dist/discovery/param-deps.d.ts.map +1 -0
- package/dist/fold/fold.d.ts +55 -2
- package/dist/fold/fold.d.ts.map +1 -1
- package/dist/fold/subset.d.ts +21 -14
- package/dist/fold/subset.d.ts.map +1 -1
- package/dist/lexicon-schema.d.ts +2 -0
- package/dist/lexicon-schema.d.ts.map +1 -1
- package/dist/lexicon.d.ts +134 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/assert-live.d.ts +77 -0
- package/dist/lifecycle/assert-live.d.ts.map +1 -0
- package/dist/lifecycle/change-set.d.ts +17 -0
- package/dist/lifecycle/change-set.d.ts.map +1 -1
- package/dist/lifecycle/converge-ledger.d.ts +90 -0
- package/dist/lifecycle/converge-ledger.d.ts.map +1 -0
- package/dist/lifecycle/deep-diff.d.ts +18 -0
- package/dist/lifecycle/deep-diff.d.ts.map +1 -1
- package/dist/lifecycle/deep-observe.d.ts +9 -1
- package/dist/lifecycle/deep-observe.d.ts.map +1 -1
- package/dist/lifecycle/disruption.d.ts +96 -0
- package/dist/lifecycle/disruption.d.ts.map +1 -0
- package/dist/lifecycle/gate-ledger.d.ts +33 -0
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -0
- package/dist/lifecycle/git.d.ts +145 -21
- package/dist/lifecycle/git.d.ts.map +1 -1
- package/dist/lifecycle/index.d.ts +6 -0
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/lease.d.ts +113 -0
- package/dist/lifecycle/lease.d.ts.map +1 -0
- package/dist/lifecycle/replay.d.ts +2 -0
- package/dist/lifecycle/replay.d.ts.map +1 -1
- package/dist/lifecycle/scenario-eval.d.ts +42 -0
- package/dist/lifecycle/scenario-eval.d.ts.map +1 -0
- package/dist/lifecycle/scenario.d.ts +163 -0
- package/dist/lifecycle/scenario.d.ts.map +1 -0
- package/dist/lifecycle/symptoms.d.ts +63 -0
- package/dist/lifecycle/symptoms.d.ts.map +1 -0
- package/dist/lint/output-docs.d.ts +94 -0
- package/dist/lint/output-docs.d.ts.map +1 -0
- package/dist/lint/post-synth.d.ts +29 -0
- package/dist/lint/post-synth.d.ts.map +1 -1
- package/dist/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.d.ts +11 -0
- package/dist/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.d.ts.map +1 -0
- package/dist/lsp/lexicon-providers.d.ts +7 -0
- package/dist/lsp/lexicon-providers.d.ts.map +1 -1
- package/dist/op/activity-contract.d.ts +139 -0
- package/dist/op/activity-contract.d.ts.map +1 -0
- package/dist/op/builders.d.ts +42 -2
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/converge-rule.d.ts +161 -0
- package/dist/op/converge-rule.d.ts.map +1 -0
- package/dist/op/generate-pipeline.d.ts +39 -0
- package/dist/op/generate-pipeline.d.ts.map +1 -0
- package/dist/op/index.d.ts +14 -0
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/local-executor.d.ts +7 -1
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/op-verb-class.d.ts +42 -0
- package/dist/op/op-verb-class.d.ts.map +1 -0
- package/dist/op/operator.d.ts +128 -0
- package/dist/op/operator.d.ts.map +1 -0
- package/dist/op/step-output-ref.d.ts +187 -0
- package/dist/op/step-output-ref.d.ts.map +1 -0
- package/dist/op/types.d.ts +18 -1
- package/dist/op/types.d.ts.map +1 -1
- package/dist/provenance.d.ts +73 -3
- package/dist/provenance.d.ts.map +1 -1
- package/dist/runtime-adapter.d.ts +7 -1
- package/dist/runtime-adapter.d.ts.map +1 -1
- package/dist/serializer.d.ts +18 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/testing.d.ts +23 -2
- package/dist/testing.d.ts.map +1 -1
- package/dist/toml.d.ts +40 -5
- package/dist/toml.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/audit/catalog.test.ts +1 -1
- package/src/audit/catalog.ts +75 -3
- package/src/audit/core.ts +9 -0
- package/src/audit/discover.ts +29 -2
- package/src/audit/fetch.test.ts +216 -3
- package/src/audit/fetch.ts +270 -59
- package/src/audit/report-html.ts +5 -2
- package/src/audit/report-model.ts +9 -0
- package/src/audit/report.test.ts +22 -0
- package/src/audit/report.ts +3 -2
- package/src/audit/rules-doc.ts +2 -0
- package/src/audit/secrets.test.ts +303 -0
- package/src/audit/secrets.ts +406 -0
- package/src/audit/wrangler.test.ts +230 -0
- package/src/audit/wrangler.ts +290 -0
- package/src/build.ts +8 -3
- package/src/cli/command-group.ts +1 -1
- package/src/cli/commands/__fixtures__/schemas/sarif-2.1.0.schema.json +2882 -0
- package/src/cli/commands/audit.test.ts +215 -1
- package/src/cli/commands/audit.ts +86 -17
- package/src/cli/commands/build.test.ts +167 -2
- package/src/cli/commands/build.ts +114 -23
- package/src/cli/handlers/build.ts +2 -0
- package/src/cli/handlers/components.test.ts +199 -1
- package/src/cli/handlers/components.ts +160 -3
- package/src/cli/handlers/graph.test.ts +20 -0
- package/src/cli/handlers/graph.ts +10 -1
- package/src/cli/handlers/lifecycle.test.ts +90 -0
- package/src/cli/handlers/lifecycle.ts +30 -5
- package/src/cli/handlers/op-progress.test.ts +202 -0
- package/src/cli/handlers/op-progress.ts +192 -0
- package/src/cli/handlers/operator.test.ts +255 -0
- package/src/cli/handlers/operator.ts +240 -0
- package/src/cli/handlers/run-client.test.ts +82 -0
- package/src/cli/handlers/run-client.ts +85 -2
- package/src/cli/handlers/run-report.test.ts +62 -0
- package/src/cli/handlers/run-report.ts +20 -58
- package/src/cli/handlers/run.test.ts +144 -0
- package/src/cli/handlers/run.ts +40 -18
- package/src/cli/handlers/scenario.test.ts +456 -0
- package/src/cli/handlers/scenario.ts +330 -0
- package/src/cli/handlers/search-drift.test.ts +263 -0
- package/src/cli/handlers/search.ts +150 -1
- package/src/cli/main.test.ts +23 -0
- package/src/cli/main.ts +81 -1
- package/src/cli/mcp/op-tools.ts +17 -6
- package/src/cli/mcp/resource-handlers.ts +13 -5
- package/src/cli/mcp/server.test.ts +265 -2
- package/src/cli/mcp/server.ts +84 -7
- package/src/cli/mcp/types.ts +27 -1
- package/src/cli/registry.ts +47 -3
- package/src/codegen/docs-rule-scanning.test.ts +42 -0
- package/src/codegen/docs-rule-scanning.ts +25 -2
- package/src/components/README.md +7 -0
- package/src/components/capability.ts +17 -2
- package/src/components/cli-support.test.ts +17 -0
- package/src/components/cli-support.ts +13 -1
- package/src/components/component-schema.test.ts +32 -0
- package/src/components/component.schema.json +6 -0
- package/src/components/component.test.ts +21 -0
- package/src/components/component.ts +15 -0
- package/src/components/driver.ts +12 -4
- package/src/components/run-progress.ts +9 -5
- package/src/components/verbs/index.ts +6 -1
- package/src/components/verbs/run-agent.test.ts +683 -0
- package/src/components/verbs/run-agent.ts +786 -0
- package/src/components/verbs/sign.test.ts +19 -0
- package/src/components/verbs/sign.ts +34 -2
- package/src/composite.ts +31 -2
- package/src/discovery/collect.ts +11 -2
- package/src/discovery/fold-import.test.ts +54 -0
- package/src/discovery/fold-import.ts +178 -38
- package/src/discovery/fold-rank.test.ts +197 -0
- package/src/discovery/fold-rank.ts +346 -0
- package/src/discovery/index.ts +16 -1
- package/src/discovery/param-deps.test.ts +118 -0
- package/src/discovery/param-deps.ts +170 -0
- package/src/fold/fold.test.ts +6 -2
- package/src/fold/fold.ts +184 -3
- package/src/fold/subset.test.ts +82 -19
- package/src/fold/subset.ts +79 -41
- package/src/lexicon-schema.ts +3 -0
- package/src/lexicon.ts +154 -2
- package/src/lifecycle/assert-live.test.ts +125 -0
- package/src/lifecycle/assert-live.ts +154 -0
- package/src/lifecycle/change-set.ts +35 -3
- package/src/lifecycle/converge-ledger.test.ts +199 -0
- package/src/lifecycle/converge-ledger.ts +179 -0
- package/src/lifecycle/deep-diff.test.ts +79 -1
- package/src/lifecycle/deep-diff.ts +23 -0
- package/src/lifecycle/deep-observe.ts +13 -2
- package/src/lifecycle/disruption.test.ts +186 -0
- package/src/lifecycle/disruption.ts +224 -0
- package/src/lifecycle/gate-ledger.test.ts +103 -0
- package/src/lifecycle/gate-ledger.ts +140 -0
- package/src/lifecycle/git.test.ts +430 -0
- package/src/lifecycle/git.ts +446 -84
- package/src/lifecycle/index.ts +6 -0
- package/src/lifecycle/lease.test.ts +343 -0
- package/src/lifecycle/lease.ts +270 -0
- package/src/lifecycle/replay.test.ts +25 -0
- package/src/lifecycle/replay.ts +11 -3
- package/src/lifecycle/scenario-eval.test.ts +199 -0
- package/src/lifecycle/scenario-eval.ts +158 -0
- package/src/lifecycle/scenario.test.ts +195 -0
- package/src/lifecycle/scenario.ts +321 -0
- package/src/lifecycle/symptoms.test.ts +116 -0
- package/src/lifecycle/symptoms.ts +126 -0
- package/src/lint/output-docs.test.ts +220 -0
- package/src/lint/output-docs.ts +204 -0
- package/src/lint/post-synth.test.ts +97 -0
- package/src/lint/post-synth.ts +45 -0
- package/src/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.ts +26 -0
- package/src/lint/rules/comp/comp.test.ts +49 -1
- package/src/lint/rules/evl001-non-literal-expression.test.ts +8 -3
- package/src/lint/rules/evl001-non-literal-expression.ts +6 -6
- package/src/lsp/lexicon-providers.test.ts +44 -0
- package/src/lsp/lexicon-providers.ts +11 -1
- package/src/op/activity-contract.test.ts +180 -0
- package/src/op/activity-contract.ts +278 -0
- package/src/op/builders-exports.test.ts +17 -1
- package/src/op/builders.ts +59 -5
- package/src/op/converge-rule.test.ts +179 -0
- package/src/op/converge-rule.ts +311 -0
- package/src/op/generate-pipeline.test.ts +53 -0
- package/src/op/generate-pipeline.ts +99 -0
- package/src/op/index.ts +30 -0
- package/src/op/local-executor.test.ts +92 -0
- package/src/op/local-executor.ts +52 -10
- package/src/op/local-output.ts +1 -1
- package/src/op/op-verb-class.test.ts +126 -0
- package/src/op/op-verb-class.ts +115 -0
- package/src/op/operator.test.ts +346 -0
- package/src/op/operator.ts +213 -0
- package/src/op/step-output-ref.test.ts +334 -0
- package/src/op/step-output-ref.ts +453 -0
- package/src/op/types.ts +18 -1
- package/src/provenance.test.ts +151 -4
- package/src/provenance.ts +118 -4
- package/src/runtime-adapter.ts +31 -10
- package/src/serializer.ts +18 -0
- package/src/testing.test.ts +89 -2
- package/src/testing.ts +63 -3
- package/src/toml.test.ts +157 -384
- package/src/toml.ts +371 -5
|
@@ -17,7 +17,6 @@ import type { Serializer, SerializerResult } from "../../serializer";
|
|
|
17
17
|
import type { LexiconPlugin } from "../../lexicon";
|
|
18
18
|
import { resolveLexiconVersions, collectBuildRootContributors } from "../plugins";
|
|
19
19
|
import { runPostSynthChecks } from "../../lint/post-synth";
|
|
20
|
-
import { collectSecretDeclarations } from "../../secret-provenance";
|
|
21
20
|
import { coreReceiptChecks } from "../../lint/receipt-checks";
|
|
22
21
|
import { coreOutputChecks } from "../../lint/output-checks";
|
|
23
22
|
import { coreKnowledgeChecks } from "../../lint/knowledge-checks";
|
|
@@ -25,6 +24,7 @@ import { applyConfiguredSeverity } from "../../lint/config";
|
|
|
25
24
|
import { loadPolicyChecks } from "../../lint/policy";
|
|
26
25
|
import { armSandboxPolicyExecution, runProjectPolicies } from "../../lint/policy-sandbox";
|
|
27
26
|
import { sortedJsonReplacer } from "../../utils";
|
|
27
|
+
import { rankFoldBlockers, toCollapsedFormat, type FoldRankResult } from "../../discovery/fold-rank";
|
|
28
28
|
import { formatError, formatWarning, formatSuccess, formatBold, formatInfo } from "../format";
|
|
29
29
|
import { writeFileSync, mkdirSync } from "fs";
|
|
30
30
|
import { resolve, dirname, join, relative } from "path";
|
|
@@ -85,6 +85,23 @@ export interface BuildOptions {
|
|
|
85
85
|
* after {@link params}.
|
|
86
86
|
*/
|
|
87
87
|
paramsFile?: string;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* chant #1083 — `chant build --fold --fold-rank`: after the build, rank
|
|
91
|
+
* `"run"`-mode files by dominator retained-count over the forward
|
|
92
|
+
* import-failure graph (see `../../discovery/fold-rank.ts`), and report
|
|
93
|
+
* the reverse-taint bucket (#1044) separately. Printed to stderr,
|
|
94
|
+
* independent of `--verbose`'s per-file listing. No-op without `fold`.
|
|
95
|
+
*/
|
|
96
|
+
foldRank?: boolean;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* chant #1083 — `chant build --fold --fold-rank <path>`: as {@link
|
|
100
|
+
* foldRank}, and ALSO writes the ranking as a Brendan Gregg collapsed
|
|
101
|
+
* stack export (weighted by retained count) to this path, so it opens in
|
|
102
|
+
* any flame/icicle viewer with no chant-specific tooling.
|
|
103
|
+
*/
|
|
104
|
+
foldRankCollapsedFile?: string;
|
|
88
105
|
}
|
|
89
106
|
|
|
90
107
|
/**
|
|
@@ -347,6 +364,23 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
|
|
|
347
364
|
}
|
|
348
365
|
}
|
|
349
366
|
|
|
367
|
+
// chant #1083 — rank fold blockers by dominator retained-count over the
|
|
368
|
+
// forward import-failure graph, and report the reverse-taint bucket
|
|
369
|
+
// (#1044) separately. Opt-in (`--fold-rank`) and independent of
|
|
370
|
+
// `--verbose`: the per-file listing above says WHAT ran; this says WHICH
|
|
371
|
+
// of those files is worth fixing first.
|
|
372
|
+
if (fold && (options.foldRank || options.foldRankCollapsedFile) && result.foldDecisions.length > 0) {
|
|
373
|
+
const ranking = await rankFoldBlockers(result.foldDecisions);
|
|
374
|
+
console.error(formatInfo(formatFoldRanking(ranking, infraPath)));
|
|
375
|
+
if (options.foldRankCollapsedFile) {
|
|
376
|
+
const collapsedPath = resolve(options.foldRankCollapsedFile);
|
|
377
|
+
mkdirSync(dirname(collapsedPath), { recursive: true });
|
|
378
|
+
const lines = toCollapsedFormat(ranking, { relativeTo: infraPath });
|
|
379
|
+
writeFileSync(collapsedPath, lines.length > 0 ? `${lines.join("\n")}\n` : "");
|
|
380
|
+
console.error(formatInfo(`[fold-rank] collapsed-format export: ${collapsedPath} (${lines.length} line(s))`));
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
350
384
|
// Format errors — grouped by message, because one refusal can surface once
|
|
351
385
|
// per resource file. A project whose params module throws (a guard like
|
|
352
386
|
// "this seam cannot back that tier") throws in EVERY file that imports it,
|
|
@@ -545,28 +579,55 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
|
|
|
545
579
|
|
|
546
580
|
// Handle output
|
|
547
581
|
if (result.errors.length === 0 && errors.length === 0) {
|
|
548
|
-
// Extract primary content and collect additional files from SerializerResult
|
|
549
|
-
|
|
582
|
+
// Extract primary content and collect additional files from SerializerResult.
|
|
583
|
+
// Each entry remembers which lexicon produced it, so a same-basename
|
|
584
|
+
// collision between two sources (below) can name both.
|
|
585
|
+
const additionalFiles = new Map<string, { content: string; source: string }>();
|
|
550
586
|
|
|
551
|
-
//
|
|
587
|
+
// Basenames (from some SerializerResult.files) that must reach disk
|
|
588
|
+
// BYTE-FOR-BYTE — e.g. committed ciphertext
|
|
552
589
|
// (`declareSecret({ provenance: "committed-encrypted", file })`). The
|
|
553
590
|
// additional-file writer below round-trips anything JSON.parse accepts,
|
|
554
|
-
// key-sorting it and possibly re-emitting it as YAML
|
|
555
|
-
// the `sops` MAC
|
|
556
|
-
//
|
|
557
|
-
//
|
|
591
|
+
// key-sorting it and possibly re-emitting it as YAML, which would break
|
|
592
|
+
// e.g. the `sops` MAC on committed ciphertext. A serializer opts a file
|
|
593
|
+
// out of that round trip structurally, via `SerializerResult.verbatimFiles`
|
|
594
|
+
// (chant#1937) — "the parse happens to fail" is not a safety guarantee.
|
|
558
595
|
const verbatimFiles = new Set<string>();
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
596
|
+
|
|
597
|
+
// A same basename emitted by two sources with different content would
|
|
598
|
+
// otherwise silently overwrite (last-writer-wins, since `additionalFiles`
|
|
599
|
+
// is keyed by basename alone). Identical content from two sources is
|
|
600
|
+
// fine — dedup — but differing content is refused, naming both sources,
|
|
601
|
+
// the same way the committed-encrypted resolution stage refuses a
|
|
602
|
+
// basename collision among its own declarations.
|
|
603
|
+
function addAdditionalFile(filename: string, content: string, source: string): void {
|
|
604
|
+
const existing = additionalFiles.get(filename);
|
|
605
|
+
if (existing === undefined) {
|
|
606
|
+
additionalFiles.set(filename, { content, source });
|
|
607
|
+
return;
|
|
562
608
|
}
|
|
609
|
+
if (existing.content === content) return;
|
|
610
|
+
errors.push(
|
|
611
|
+
formatError({
|
|
612
|
+
message:
|
|
613
|
+
`Additional file "${filename}" collides: "${existing.source}" and "${source}" ` +
|
|
614
|
+
`both emit it with different content — additional files share one flat ` +
|
|
615
|
+
`namespace, so two sources whose filenames match would otherwise silently ` +
|
|
616
|
+
`overwrite each other.`,
|
|
617
|
+
}),
|
|
618
|
+
);
|
|
563
619
|
}
|
|
564
620
|
|
|
565
|
-
function getPrimaryContent(raw: string | SerializerResult): string {
|
|
621
|
+
function getPrimaryContent(raw: string | SerializerResult, source: string): string {
|
|
566
622
|
if (typeof raw === "string") return raw;
|
|
567
623
|
if (raw.files) {
|
|
568
624
|
for (const [filename, content] of Object.entries(raw.files)) {
|
|
569
|
-
|
|
625
|
+
addAdditionalFile(filename, content, source);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
if (raw.verbatimFiles) {
|
|
629
|
+
for (const filename of raw.verbatimFiles) {
|
|
630
|
+
verbatimFiles.add(filename);
|
|
570
631
|
}
|
|
571
632
|
}
|
|
572
633
|
return raw.primary;
|
|
@@ -585,8 +646,8 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
|
|
|
585
646
|
// Multiple lexicons: wrap in lexicon keys
|
|
586
647
|
let output: string = "{}";
|
|
587
648
|
if (result.outputs.size === 1) {
|
|
588
|
-
const [, raw] = [...result.outputs.entries()][0];
|
|
589
|
-
const content = getPrimaryContent(raw);
|
|
649
|
+
const [lexiconName, raw] = [...result.outputs.entries()][0];
|
|
650
|
+
const content = getPrimaryContent(raw, lexiconName);
|
|
590
651
|
const parsed = tryParseJson(content);
|
|
591
652
|
if ("json" in parsed) {
|
|
592
653
|
output = JSON.stringify(parsed.json, sortedJsonReplacer, 2);
|
|
@@ -603,7 +664,7 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
|
|
|
603
664
|
const nonJsonSections: string[] = [];
|
|
604
665
|
const sortedLexiconNames = [...result.outputs.keys()].sort();
|
|
605
666
|
for (const lexiconName of sortedLexiconNames) {
|
|
606
|
-
const content = getPrimaryContent(result.outputs.get(lexiconName)
|
|
667
|
+
const content = getPrimaryContent(result.outputs.get(lexiconName)!, lexiconName);
|
|
607
668
|
const parsed = tryParseJson(content);
|
|
608
669
|
if ("json" in parsed) {
|
|
609
670
|
combined[lexiconName] = parsed.json;
|
|
@@ -634,13 +695,13 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
|
|
|
634
695
|
// `foo.yaml`, so the durable-run worker was never where `run --temporal` looks.
|
|
635
696
|
const projectDist = resolve(options.path ?? ".", "dist");
|
|
636
697
|
let opsWritten = 0;
|
|
637
|
-
for (const [filename,
|
|
698
|
+
for (const [filename, entry] of [...additionalFiles]) {
|
|
638
699
|
if (!filename.startsWith("ops/")) continue;
|
|
639
700
|
additionalFiles.delete(filename);
|
|
640
701
|
try {
|
|
641
702
|
const targetPath = join(projectDist, filename);
|
|
642
703
|
mkdirSync(dirname(targetPath), { recursive: true });
|
|
643
|
-
writeFileSync(targetPath, content);
|
|
704
|
+
writeFileSync(targetPath, entry.content);
|
|
644
705
|
opsWritten += 1;
|
|
645
706
|
} catch (err) {
|
|
646
707
|
errors.push(
|
|
@@ -665,13 +726,13 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
|
|
|
665
726
|
// Write additional files (e.g. nested stack templates) alongside the primary output
|
|
666
727
|
if (additionalFiles.size > 0) {
|
|
667
728
|
const outputDir = dirname(outputPath);
|
|
668
|
-
for (const [filename,
|
|
669
|
-
let fileContent = content;
|
|
729
|
+
for (const [filename, entry] of additionalFiles) {
|
|
730
|
+
let fileContent = entry.content;
|
|
670
731
|
// Format additional files consistently — except the ones declared
|
|
671
732
|
// verbatim, which are copied exactly as they were committed.
|
|
672
733
|
if (!verbatimFiles.has(filename)) {
|
|
673
734
|
try {
|
|
674
|
-
const fileParsed = JSON.parse(content);
|
|
735
|
+
const fileParsed = JSON.parse(entry.content);
|
|
675
736
|
fileContent = JSON.stringify(fileParsed, sortedJsonReplacer, 2);
|
|
676
737
|
if (options.format === "yaml") {
|
|
677
738
|
fileContent = jsonToYaml(JSON.parse(fileContent));
|
|
@@ -711,9 +772,9 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
|
|
|
711
772
|
// Print to stdout
|
|
712
773
|
console.log(output);
|
|
713
774
|
// Log additional files to stderr if any
|
|
714
|
-
for (const [filename,
|
|
775
|
+
for (const [filename, entry] of additionalFiles) {
|
|
715
776
|
console.error(`\n--- ${filename} ---`);
|
|
716
|
-
console.error(content);
|
|
777
|
+
console.error(entry.content);
|
|
717
778
|
}
|
|
718
779
|
}
|
|
719
780
|
}
|
|
@@ -757,6 +818,36 @@ export function summarizeFoldDecisions(decisions: readonly { mode: string }[]):
|
|
|
757
818
|
return `fold: ${files(folded)} folded, ${ran} ran${hint}`;
|
|
758
819
|
}
|
|
759
820
|
|
|
821
|
+
/**
|
|
822
|
+
* `chant build --fold --fold-rank`'s text report (chant #1083): blockers
|
|
823
|
+
* ranked by dominator retained-count, highest first, then the reverse-taint
|
|
824
|
+
* bucket (#1044) called out separately since the tree can't express that
|
|
825
|
+
* edge (see `../../discovery/fold-rank.ts`'s module doc).
|
|
826
|
+
*/
|
|
827
|
+
export function formatFoldRanking(ranking: FoldRankResult, infraPath: string): string {
|
|
828
|
+
const rel = (file: string): string => relative(infraPath, file) || file;
|
|
829
|
+
const lines: string[] = [];
|
|
830
|
+
if (ranking.blockers.length === 0) {
|
|
831
|
+
lines.push("fold-rank: no run-mode files to rank");
|
|
832
|
+
} else {
|
|
833
|
+
lines.push(`fold-rank: ${ranking.blockers.length} blocker(s), ${ranking.totalBlocked} file(s) blocked`);
|
|
834
|
+
for (const b of ranking.blockers) {
|
|
835
|
+
const via = b.topLevel ? "" : ` (behind ${rel(b.dominatedBy!)})`;
|
|
836
|
+
const reason = b.reason ? ` — ${b.reason}` : "";
|
|
837
|
+
lines.push(` retained ${b.retained}: ${rel(b.file)}${via}${reason}`);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
if (ranking.reverseTainted.length > 0) {
|
|
841
|
+
lines.push(
|
|
842
|
+
`fold-rank: ${ranking.reverseTainted.length} file(s) held back only by the reverse rule (#1044) — would fold in isolation, blocked by a run-mode importer, not modeled in the tree above:`,
|
|
843
|
+
);
|
|
844
|
+
for (const t of ranking.reverseTainted) {
|
|
845
|
+
lines.push(` ${rel(t.file)}`);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
return lines.join("\n");
|
|
849
|
+
}
|
|
850
|
+
|
|
760
851
|
/**
|
|
761
852
|
* Simple JSON to YAML converter
|
|
762
853
|
*/
|
|
@@ -148,6 +148,8 @@ export async function runBuild(ctx: CommandContext): Promise<number> {
|
|
|
148
148
|
env: args.env,
|
|
149
149
|
fold: args.fold,
|
|
150
150
|
sandbox: args.sandbox,
|
|
151
|
+
foldRank: args.foldRank,
|
|
152
|
+
foldRankCollapsedFile: args.foldRankCollapsedFile,
|
|
151
153
|
params,
|
|
152
154
|
paramsFile: args.paramsFile,
|
|
153
155
|
});
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { describe, test, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
2
5
|
import { createMockPlugin, staticDescribeResources } from "@intentius/chant-test-utils";
|
|
3
6
|
import type { LexiconPlugin, ResourceMetadata } from "../../lexicon";
|
|
4
7
|
import type { BuildResult } from "../../build";
|
|
8
|
+
import type { BuildArchiveManifest } from "../../components/verbs/build-archive";
|
|
5
9
|
import type { ParsedArgs } from "../registry";
|
|
6
10
|
|
|
7
11
|
/**
|
|
@@ -28,6 +32,7 @@ const loadChantConfigMock = vi.fn();
|
|
|
28
32
|
const buildMock = vi.fn();
|
|
29
33
|
const discoverComponentsMock = vi.fn();
|
|
30
34
|
const findBuildManifestByArtifactDigestMock = vi.fn();
|
|
35
|
+
const readBuildManifestMock = vi.fn();
|
|
31
36
|
|
|
32
37
|
vi.mock("../../lifecycle/git", () => ({
|
|
33
38
|
getHeadCommit: (...args: unknown[]) => getHeadCommitMock(...args),
|
|
@@ -38,6 +43,7 @@ vi.mock("../../lifecycle/git", () => ({
|
|
|
38
43
|
|
|
39
44
|
vi.mock("../../lifecycle/build-ledger-store", () => ({
|
|
40
45
|
findBuildManifestByArtifactDigest: (...args: unknown[]) => findBuildManifestByArtifactDigestMock(...args),
|
|
46
|
+
readBuildManifest: (...args: unknown[]) => readBuildManifestMock(...args),
|
|
41
47
|
}));
|
|
42
48
|
|
|
43
49
|
vi.mock("../../lifecycle/release-ledger", async () => {
|
|
@@ -66,7 +72,7 @@ vi.mock("../../components/discover", () => ({
|
|
|
66
72
|
discoverComponents: (...args: unknown[]) => discoverComponentsMock(...args),
|
|
67
73
|
}));
|
|
68
74
|
|
|
69
|
-
const { runComponentsReleaseRecord, runComponentsStatus, runComponentsUnknown } = await import("./components");
|
|
75
|
+
const { runComponentsReleaseRecord, runComponentsStatus, runComponentsExport, runComponentsUnknown } = await import("./components");
|
|
70
76
|
|
|
71
77
|
function makeArgs(overrides: Partial<ParsedArgs>): ParsedArgs {
|
|
72
78
|
return {
|
|
@@ -126,6 +132,7 @@ describe("components handlers", () => {
|
|
|
126
132
|
buildMock.mockReset().mockResolvedValue(makeBuildResult({}));
|
|
127
133
|
discoverComponentsMock.mockReset().mockResolvedValue({ components: new Map(), sourceFiles: [], errors: [] });
|
|
128
134
|
findBuildManifestByArtifactDigestMock.mockReset().mockResolvedValue(undefined);
|
|
135
|
+
readBuildManifestMock.mockReset().mockResolvedValue(null);
|
|
129
136
|
|
|
130
137
|
delete process.env.GITHUB_RUN_ID;
|
|
131
138
|
delete process.env.CI_PIPELINE_ID;
|
|
@@ -569,6 +576,197 @@ describe("components handlers", () => {
|
|
|
569
576
|
});
|
|
570
577
|
});
|
|
571
578
|
|
|
579
|
+
describe("runComponentsExport", () => {
|
|
580
|
+
const originalCwd = process.cwd();
|
|
581
|
+
let srcDir: string;
|
|
582
|
+
let outDir: string;
|
|
583
|
+
|
|
584
|
+
function makeManifest(overrides: Partial<BuildArchiveManifest> = {}): BuildArchiveManifest {
|
|
585
|
+
return {
|
|
586
|
+
version: 1,
|
|
587
|
+
component: "search-service",
|
|
588
|
+
createdAt: "2026-01-01T00:00:00.000Z",
|
|
589
|
+
manifestDigest: "sha256:manifest1",
|
|
590
|
+
contents: [
|
|
591
|
+
{ kind: "template", path: "search.template.json", digest: "sha256:tmpl1", mediaType: "application/json" },
|
|
592
|
+
{ kind: "asset", path: "dist/search.zip", digest: "sha256:asset1", mediaType: "application/octet-stream" },
|
|
593
|
+
],
|
|
594
|
+
...overrides,
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
beforeEach(async () => {
|
|
599
|
+
srcDir = await mkdtemp(join(tmpdir(), "chant-export-src-"));
|
|
600
|
+
outDir = await mkdtemp(join(tmpdir(), "chant-export-out-"));
|
|
601
|
+
process.chdir(srcDir);
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
afterEach(async () => {
|
|
605
|
+
process.chdir(originalCwd);
|
|
606
|
+
await rm(srcDir, { recursive: true, force: true });
|
|
607
|
+
await rm(outDir, { recursive: true, force: true });
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
test("-o/--output is required", async () => {
|
|
611
|
+
const ctx = { args: makeArgs({ path: "export", digest: "sha256:manifest1" }), plugins: [], serializers: [] };
|
|
612
|
+
const exit = await runComponentsExport(ctx);
|
|
613
|
+
expect(exit).toBe(1);
|
|
614
|
+
expect(stderrBuf.join("\n")).toContain("-o/--output <dir> is required");
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
test("without --digest, requires environment and --component", async () => {
|
|
618
|
+
const ctx = { args: makeArgs({ path: "export", output: outDir }), plugins: [], serializers: [] };
|
|
619
|
+
const exit = await runComponentsExport(ctx);
|
|
620
|
+
expect(exit).toBe(1);
|
|
621
|
+
expect(stderrBuf.join("\n")).toContain("Environment and --component are required");
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
test("--digest resolves the manifest directly and materializes every entry plus manifest.json", async () => {
|
|
625
|
+
await writeFile(join(srcDir, "search.template.json"), '{"Resources":{}}');
|
|
626
|
+
await mkdir(join(srcDir, "dist"), { recursive: true });
|
|
627
|
+
await writeFile(join(srcDir, "dist", "search.zip"), "zip-bytes");
|
|
628
|
+
|
|
629
|
+
readBuildManifestMock.mockResolvedValue(makeManifest());
|
|
630
|
+
|
|
631
|
+
const ctx = {
|
|
632
|
+
args: makeArgs({ path: "export", component: "search-service", digest: "sha256:manifest1", output: outDir }),
|
|
633
|
+
plugins: [],
|
|
634
|
+
serializers: [],
|
|
635
|
+
};
|
|
636
|
+
const exit = await runComponentsExport(ctx);
|
|
637
|
+
|
|
638
|
+
expect(exit).toBe(0);
|
|
639
|
+
expect(readBuildManifestMock).toHaveBeenCalledWith("sha256:manifest1");
|
|
640
|
+
expect(await readFile(join(outDir, "search.template.json"), "utf-8")).toBe('{"Resources":{}}');
|
|
641
|
+
expect(await readFile(join(outDir, "dist", "search.zip"), "utf-8")).toBe("zip-bytes");
|
|
642
|
+
const manifestOut = JSON.parse(await readFile(join(outDir, "manifest.json"), "utf-8"));
|
|
643
|
+
expect(manifestOut.manifestDigest).toBe("sha256:manifest1");
|
|
644
|
+
expect(manifestOut.contents).toHaveLength(2);
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
test("resolves via the release ledger's latest record when --digest is omitted (manifestDigest present)", async () => {
|
|
648
|
+
readReleaseLedgerMock.mockResolvedValue({
|
|
649
|
+
records: [
|
|
650
|
+
{
|
|
651
|
+
version: 1, component: "search-service", env: "prod", digest: "sha256:artifact1",
|
|
652
|
+
gitSha: "abc", runId: "1", timestamp: "2026-01-01T00:00:00.000Z", actor: "alice",
|
|
653
|
+
manifestDigest: "sha256:manifest1",
|
|
654
|
+
},
|
|
655
|
+
],
|
|
656
|
+
malformed: 0,
|
|
657
|
+
});
|
|
658
|
+
readBuildManifestMock.mockResolvedValue(makeManifest({ contents: [] }));
|
|
659
|
+
|
|
660
|
+
const ctx = {
|
|
661
|
+
args: makeArgs({ path: "export", extraPositional: "prod", component: "search-service", output: outDir }),
|
|
662
|
+
plugins: [],
|
|
663
|
+
serializers: [],
|
|
664
|
+
};
|
|
665
|
+
const exit = await runComponentsExport(ctx);
|
|
666
|
+
|
|
667
|
+
expect(exit).toBe(0);
|
|
668
|
+
expect(readBuildManifestMock).toHaveBeenCalledWith("sha256:manifest1");
|
|
669
|
+
expect(findBuildManifestByArtifactDigestMock).not.toHaveBeenCalled();
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
test("falls back to findBuildManifestByArtifactDigest when the release record carries no manifestDigest", async () => {
|
|
673
|
+
readReleaseLedgerMock.mockResolvedValue({
|
|
674
|
+
records: [
|
|
675
|
+
{
|
|
676
|
+
version: 1, component: "search-service", env: "prod", digest: "sha256:artifact1",
|
|
677
|
+
gitSha: "abc", runId: "1", timestamp: "2026-01-01T00:00:00.000Z", actor: "alice",
|
|
678
|
+
},
|
|
679
|
+
],
|
|
680
|
+
malformed: 0,
|
|
681
|
+
});
|
|
682
|
+
findBuildManifestByArtifactDigestMock.mockResolvedValue(makeManifest({ contents: [] }));
|
|
683
|
+
|
|
684
|
+
const ctx = {
|
|
685
|
+
args: makeArgs({ path: "export", extraPositional: "prod", component: "search-service", output: outDir }),
|
|
686
|
+
plugins: [],
|
|
687
|
+
serializers: [],
|
|
688
|
+
};
|
|
689
|
+
const exit = await runComponentsExport(ctx);
|
|
690
|
+
|
|
691
|
+
expect(exit).toBe(0);
|
|
692
|
+
expect(findBuildManifestByArtifactDigestMock).toHaveBeenCalledWith("sha256:artifact1");
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
test("no release record for the component/env -> error", async () => {
|
|
696
|
+
readReleaseLedgerMock.mockResolvedValue({ records: [], malformed: 0 });
|
|
697
|
+
const ctx = {
|
|
698
|
+
args: makeArgs({ path: "export", extraPositional: "prod", component: "search-service", output: outDir }),
|
|
699
|
+
plugins: [],
|
|
700
|
+
serializers: [],
|
|
701
|
+
};
|
|
702
|
+
const exit = await runComponentsExport(ctx);
|
|
703
|
+
expect(exit).toBe(1);
|
|
704
|
+
expect(stderrBuf.join("\n")).toContain("No release record for component");
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
test("--digest with no persisted manifest -> error", async () => {
|
|
708
|
+
readBuildManifestMock.mockResolvedValue(null);
|
|
709
|
+
const ctx = {
|
|
710
|
+
args: makeArgs({ path: "export", digest: "sha256:missing", output: outDir }),
|
|
711
|
+
plugins: [],
|
|
712
|
+
serializers: [],
|
|
713
|
+
};
|
|
714
|
+
const exit = await runComponentsExport(ctx);
|
|
715
|
+
expect(exit).toBe(1);
|
|
716
|
+
expect(stderrBuf.join("\n")).toContain("No persisted build manifest found");
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
test("a source file missing on disk is reported per entry and fails the command", async () => {
|
|
720
|
+
readBuildManifestMock.mockResolvedValue(makeManifest());
|
|
721
|
+
// Neither entry's source file exists in srcDir.
|
|
722
|
+
const ctx = {
|
|
723
|
+
args: makeArgs({ path: "export", digest: "sha256:manifest1", output: outDir }),
|
|
724
|
+
plugins: [],
|
|
725
|
+
serializers: [],
|
|
726
|
+
};
|
|
727
|
+
const exit = await runComponentsExport(ctx);
|
|
728
|
+
expect(exit).toBe(1);
|
|
729
|
+
expect(stderrBuf.join("\n")).toContain("not found on disk");
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
test("--json emits a structured per-entry summary", async () => {
|
|
733
|
+
await writeFile(join(srcDir, "search.template.json"), "{}");
|
|
734
|
+
await mkdir(join(srcDir, "dist"), { recursive: true });
|
|
735
|
+
await writeFile(join(srcDir, "dist", "search.zip"), "zip-bytes");
|
|
736
|
+
readBuildManifestMock.mockResolvedValue(makeManifest());
|
|
737
|
+
|
|
738
|
+
const ctx = {
|
|
739
|
+
args: makeArgs({ path: "export", digest: "sha256:manifest1", output: outDir, json: true }),
|
|
740
|
+
plugins: [],
|
|
741
|
+
serializers: [],
|
|
742
|
+
};
|
|
743
|
+
const exit = await runComponentsExport(ctx);
|
|
744
|
+
expect(exit).toBe(0);
|
|
745
|
+
|
|
746
|
+
const parsed = JSON.parse(stdoutBuf.join(""));
|
|
747
|
+
expect(parsed.component).toBe("search-service");
|
|
748
|
+
expect(parsed.manifestDigest).toBe("sha256:manifest1");
|
|
749
|
+
expect(parsed.entries).toHaveLength(2);
|
|
750
|
+
expect(parsed.entries.every((e: { status: string }) => e.status === "materialized")).toBe(true);
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
test("a --component that mismatches the resolved --digest manifest warns but still exports", async () => {
|
|
754
|
+
await writeFile(join(srcDir, "search.template.json"), "{}");
|
|
755
|
+
await mkdir(join(srcDir, "dist"), { recursive: true });
|
|
756
|
+
await writeFile(join(srcDir, "dist", "search.zip"), "zip-bytes");
|
|
757
|
+
readBuildManifestMock.mockResolvedValue(makeManifest());
|
|
758
|
+
|
|
759
|
+
const ctx = {
|
|
760
|
+
args: makeArgs({ path: "export", component: "other-service", digest: "sha256:manifest1", output: outDir }),
|
|
761
|
+
plugins: [],
|
|
762
|
+
serializers: [],
|
|
763
|
+
};
|
|
764
|
+
const exit = await runComponentsExport(ctx);
|
|
765
|
+
expect(exit).toBe(0);
|
|
766
|
+
expect(stderrBuf.join("\n")).toContain("does not match the resolved manifest's component");
|
|
767
|
+
});
|
|
768
|
+
});
|
|
769
|
+
|
|
572
770
|
describe("runComponentsUnknown", () => {
|
|
573
771
|
test("reports the unknown subcommand", async () => {
|
|
574
772
|
const ctx = { args: makeArgs({ path: "bogus" }), plugins: [], serializers: [] };
|