@intentius/chant 0.46.0 → 0.50.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 +30 -3
- package/dist/audit/core.d.ts.map +1 -1
- package/dist/audit/discover.d.ts +9 -2
- 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 +3 -3
- 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/commands/lint.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 +12 -1
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts +32 -0
- package/dist/cli/handlers/operator.d.ts.map +1 -0
- package/dist/cli/handlers/scenario.d.ts +39 -0
- package/dist/cli/handlers/scenario.d.ts.map +1 -0
- package/dist/cli/main.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/tools/explain.d.ts +6 -0
- package/dist/cli/mcp/tools/explain.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/plugins.d.ts +1 -1
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +14 -2
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/cli/reporters/stylish.d.ts +15 -1
- package/dist/cli/reporters/stylish.d.ts.map +1 -1
- package/dist/codegen/docs-rule-scanning.d.ts.map +1 -1
- package/dist/components/auto-release.d.ts +4 -0
- package/dist/components/auto-release.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/starter-plugin.d.ts +2 -0
- package/dist/components/starter-plugin.d.ts.map +1 -1
- package/dist/components/verbs/ensure-secret.d.ts +50 -0
- package/dist/components/verbs/ensure-secret.d.ts.map +1 -0
- package/dist/components/verbs/index.d.ts +13 -0
- package/dist/components/verbs/index.d.ts.map +1 -1
- package/dist/components/verbs/r2-sync.d.ts +76 -0
- package/dist/components/verbs/r2-sync.d.ts.map +1 -0
- 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/components/verbs/wrangler.d.ts +108 -0
- package/dist/components/verbs/wrangler.d.ts.map +1 -0
- package/dist/composite.d.ts +6 -1
- package/dist/composite.d.ts.map +1 -1
- package/dist/config.d.ts +26 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/deep-observation.d.ts +14 -0
- package/dist/deep-observation.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/effect-receipt.d.ts +177 -0
- package/dist/effect-receipt.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 +20 -0
- package/dist/fold/subset.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.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 +137 -3
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/change-set.d.ts +33 -5
- 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/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/observation-baseline.d.ts +21 -3
- package/dist/lifecycle/observation-baseline.d.ts.map +1 -1
- package/dist/lifecycle/receipt-plan.d.ts +62 -0
- package/dist/lifecycle/receipt-plan.d.ts.map +1 -0
- package/dist/lifecycle/release-ledger.d.ts +20 -0
- package/dist/lifecycle/release-ledger.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/lifecycle/teardown.d.ts +6 -4
- package/dist/lifecycle/teardown.d.ts.map +1 -1
- package/dist/lifecycle/unobserved-gate.d.ts +67 -0
- package/dist/lifecycle/unobserved-gate.d.ts.map +1 -0
- package/dist/lint/knowledge-checks.d.ts +48 -0
- package/dist/lint/knowledge-checks.d.ts.map +1 -0
- package/dist/lint/output-checks.d.ts +5 -0
- package/dist/lint/output-checks.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/pipeline-change-gate.d.ts +101 -0
- package/dist/lint/pipeline-change-gate.d.ts.map +1 -0
- package/dist/lint/post-synth.d.ts +41 -0
- package/dist/lint/post-synth.d.ts.map +1 -1
- package/dist/lint/receipt-checks.d.ts +9 -0
- package/dist/lint/receipt-checks.d.ts.map +1 -0
- 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/lint/rules/cor022-receipt-leaf.d.ts +13 -0
- package/dist/lint/rules/cor022-receipt-leaf.d.ts.map +1 -0
- package/dist/lint/rules/cor024-receipt-secret-pointer.d.ts +3 -0
- package/dist/lint/rules/cor024-receipt-secret-pointer.d.ts.map +1 -0
- package/dist/lint/rules/evl001-non-literal-expression.d.ts.map +1 -1
- package/dist/lint/rules/index.d.ts +3 -1
- package/dist/lint/rules/index.d.ts.map +1 -1
- package/dist/lsp/lexicon-providers.d.ts +7 -0
- package/dist/lsp/lexicon-providers.d.ts.map +1 -1
- package/dist/okf-read.d.ts +78 -0
- package/dist/okf-read.d.ts.map +1 -0
- 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 +140 -3
- 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 +18 -2
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/local-executor.d.ts +2 -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/receipt-store.d.ts +138 -0
- package/dist/op/receipt-store.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 +49 -2
- 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/secret-materialization.d.ts +138 -0
- package/dist/secret-materialization.d.ts.map +1 -0
- package/dist/secret-provenance.d.ts +218 -0
- package/dist/secret-provenance.d.ts.map +1 -0
- package/dist/serializer.d.ts +29 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/toml.d.ts +40 -5
- package/dist/toml.d.ts.map +1 -1
- package/dist/yaml.d.ts.map +1 -1
- package/package.json +4 -1
- package/src/audit/catalog.test.ts +1 -1
- package/src/audit/catalog.ts +75 -3
- package/src/audit/core.test.ts +57 -0
- package/src/audit/core.ts +0 -0
- package/src/audit/detect-bundle.test.ts +1 -1
- package/src/audit/discover.test.ts +24 -0
- package/src/audit/discover.ts +40 -4
- 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 +13 -1
- 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.test.ts +41 -0
- package/src/build.ts +39 -6
- package/src/cli/command-group.ts +1 -1
- package/src/cli/commands/__fixtures__/audit-fountain/agents/fleet.yaml +27 -0
- package/src/cli/commands/__fixtures__/audit-fountain/k8s/deploy.yaml +16 -0
- package/src/cli/commands/__fixtures__/audit-fountain-clean/fleet.yaml +20 -0
- package/src/cli/commands/__fixtures__/schemas/sarif-2.1.0.schema.json +2882 -0
- package/src/cli/commands/audit.test.ts +268 -1
- package/src/cli/commands/audit.ts +87 -18
- package/src/cli/commands/build.test.ts +245 -0
- package/src/cli/commands/build.ts +210 -21
- package/src/cli/commands/lint.ts +15 -3
- 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/explain.test.ts +70 -1
- package/src/cli/handlers/graph.test.ts +20 -0
- package/src/cli/handlers/graph.ts +12 -3
- package/src/cli/handlers/lifecycle.test.ts +115 -1
- package/src/cli/handlers/lifecycle.ts +96 -15
- package/src/cli/handlers/operator.test.ts +255 -0
- package/src/cli/handlers/operator.ts +240 -0
- package/src/cli/handlers/scenario.test.ts +456 -0
- package/src/cli/handlers/scenario.ts +330 -0
- package/src/cli/main.test.ts +23 -0
- package/src/cli/main.ts +72 -1
- package/src/cli/mcp/resource-handlers.ts +38 -1
- package/src/cli/mcp/server.test.ts +323 -3
- package/src/cli/mcp/server.ts +84 -7
- package/src/cli/mcp/tools/explain.ts +51 -2
- package/src/cli/mcp/types.ts +27 -1
- package/src/cli/plugins.ts +4 -2
- package/src/cli/registry.ts +14 -2
- package/src/cli/reporters/stylish.test.ts +154 -0
- package/src/cli/reporters/stylish.ts +154 -33
- 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/auto-release.ts +6 -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/registry.test.ts +7 -2
- package/src/components/starter-plugin.ts +17 -0
- package/src/components/verbs/ensure-secret.test.ts +130 -0
- package/src/components/verbs/ensure-secret.ts +79 -0
- package/src/components/verbs/index.ts +13 -0
- package/src/components/verbs/r2-sync.test.ts +107 -0
- package/src/components/verbs/r2-sync.ts +124 -0
- 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/components/verbs/wrangler.test.ts +170 -0
- package/src/components/verbs/wrangler.ts +241 -0
- package/src/composite.ts +31 -2
- package/src/config.test.ts +15 -0
- package/src/config.ts +30 -0
- package/src/deep-observation.test.ts +19 -0
- package/src/deep-observation.ts +17 -0
- 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/effect-receipt-exclusion.test.ts +190 -0
- package/src/effect-receipt.test.ts +419 -0
- package/src/effect-receipt.ts +412 -0
- package/src/fold/fold.test.ts +6 -2
- package/src/fold/fold.ts +184 -3
- package/src/fold/subset.test.ts +95 -6
- package/src/fold/subset.ts +66 -2
- package/src/index.ts +4 -0
- package/src/lexicon-schema.ts +3 -0
- package/src/lexicon.ts +151 -5
- package/src/lifecycle/change-set.ts +46 -7
- 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/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/observation-baseline.test.ts +46 -0
- package/src/lifecycle/observation-baseline.ts +33 -1
- package/src/lifecycle/receipt-plan.test.ts +250 -0
- package/src/lifecycle/receipt-plan.ts +249 -0
- package/src/lifecycle/release-ledger.ts +20 -0
- 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/lifecycle/teardown.test.ts +31 -0
- package/src/lifecycle/teardown.ts +6 -4
- package/src/lifecycle/unobserved-gate.test.ts +109 -0
- package/src/lifecycle/unobserved-gate.ts +102 -0
- package/src/lint/knowledge-checks.test.ts +80 -0
- package/src/lint/knowledge-checks.ts +74 -0
- package/src/lint/output-checks.test.ts +85 -0
- package/src/lint/output-checks.ts +99 -0
- package/src/lint/output-docs.test.ts +220 -0
- package/src/lint/output-docs.ts +204 -0
- package/src/lint/pipeline-change-gate.test.ts +144 -0
- package/src/lint/pipeline-change-gate.ts +153 -0
- package/src/lint/post-synth.test.ts +97 -0
- package/src/lint/post-synth.ts +60 -0
- package/src/lint/receipt-checks.test.ts +101 -0
- package/src/lint/receipt-checks.ts +93 -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/cor022-receipt-leaf.test.ts +116 -0
- package/src/lint/rules/cor022-receipt-leaf.ts +130 -0
- package/src/lint/rules/cor024-receipt-secret-pointer.test.ts +121 -0
- package/src/lint/rules/cor024-receipt-secret-pointer.ts +218 -0
- package/src/lint/rules/evl001-non-literal-expression.test.ts +35 -3
- package/src/lint/rules/evl001-non-literal-expression.ts +7 -0
- package/src/lint/rules/index.ts +7 -1
- package/src/lsp/lexicon-providers.test.ts +44 -0
- package/src/lsp/lexicon-providers.ts +11 -1
- package/src/okf-read.test.ts +149 -0
- package/src/okf-read.ts +197 -0
- 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 +198 -6
- package/src/op/converge-rule.test.ts +179 -0
- package/src/op/converge-rule.ts +311 -0
- package/src/op/effect-step.test.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 +40 -3
- package/src/op/local-executor.test.ts +92 -0
- package/src/op/local-executor.ts +212 -29
- package/src/op/op-verb-class.test.ts +126 -0
- package/src/op/op-verb-class.ts +115 -0
- package/src/op/op.test.ts +25 -2
- package/src/op/operator.test.ts +346 -0
- package/src/op/operator.ts +213 -0
- package/src/op/receipt-store.ts +211 -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 +51 -2
- package/src/provenance.test.ts +151 -4
- package/src/provenance.ts +118 -4
- package/src/runtime-adapter.ts +31 -10
- package/src/secret-materialization.test.ts +199 -0
- package/src/secret-materialization.ts +235 -0
- package/src/secret-provenance.test.ts +388 -0
- package/src/secret-provenance.ts +475 -0
- package/src/serializer.ts +30 -0
- package/src/toml.test.ts +157 -384
- package/src/toml.ts +371 -5
- package/src/yaml.test.ts +88 -0
- package/src/yaml.ts +76 -6
|
@@ -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: [] };
|
|
@@ -26,20 +26,23 @@
|
|
|
26
26
|
* `componentBom` fall back to `null` exactly as before — no regression for
|
|
27
27
|
* ledgers with no persisted manifests at all.
|
|
28
28
|
*/
|
|
29
|
-
import { resolve } from "node:path";
|
|
29
|
+
import { resolve, join, dirname } from "node:path";
|
|
30
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
30
31
|
import { getHeadCommit, fetchLifecycle, pushLifecycle, StaleLifecycleBranchError } from "../../lifecycle/git";
|
|
31
32
|
import {
|
|
32
33
|
appendReleaseRecord,
|
|
33
34
|
readReleaseLedger,
|
|
34
35
|
listReleaseEnvironments,
|
|
36
|
+
latestPerComponent,
|
|
35
37
|
InvalidReleaseRecordError,
|
|
36
38
|
} from "../../lifecycle/release-ledger";
|
|
37
39
|
import { reconcileStatus, liveEvidenceFromChangeSet, compareAcrossEnvironments, mergeLiveEvidence, type LiveComponentEvidence } from "../../lifecycle/status";
|
|
38
40
|
import { commandBuildParams } from "../build-params-cli";
|
|
39
41
|
import { buildChangeSet } from "../../lifecycle/change-set";
|
|
40
42
|
import { buildLedgerEntries, componentBomSummary, type BuildLedgerEntry } from "../../lifecycle/build-ledger";
|
|
41
|
-
import { findBuildManifestByArtifactDigest } from "../../lifecycle/build-ledger-store";
|
|
43
|
+
import { findBuildManifestByArtifactDigest, readBuildManifest } from "../../lifecycle/build-ledger-store";
|
|
42
44
|
import type { ComponentBomSummary } from "../../lifecycle/build-ledger";
|
|
45
|
+
import type { BuildArchiveManifest } from "../../components/verbs/build-archive";
|
|
43
46
|
import { loadChantConfig } from "../../config";
|
|
44
47
|
import { applyLiveEndpoint } from "../../live-endpoint";
|
|
45
48
|
import { isResourceDeclarable } from "../../declarable";
|
|
@@ -51,6 +54,7 @@ import type { LexiconPlugin } from "../../lexicon";
|
|
|
51
54
|
import { normalizeObservation, mergeObservations, unobservedAll, type NormalizedObservation } from "../../observation";
|
|
52
55
|
import type { Phase, Component } from "../../components/component";
|
|
53
56
|
import { deployUnits } from "../../components/deploy-units";
|
|
57
|
+
import { sortedJsonReplacer } from "../../utils";
|
|
54
58
|
|
|
55
59
|
/**
|
|
56
60
|
* chant components release <env> --component <name> --digest <sha256:...>
|
|
@@ -147,6 +151,159 @@ export async function runComponentsReleaseRecord(ctx: CommandContext): Promise<n
|
|
|
147
151
|
}
|
|
148
152
|
}
|
|
149
153
|
|
|
154
|
+
/** One entry of `chant components export`'s per-entry result (text and `--json` output). */
|
|
155
|
+
interface ExportEntryResult {
|
|
156
|
+
kind: string;
|
|
157
|
+
path: string;
|
|
158
|
+
digest: string;
|
|
159
|
+
status: "materialized" | "missing";
|
|
160
|
+
/** Set only when `status` is "missing" — why the source file couldn't be read. */
|
|
161
|
+
reason?: string;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* chant components export <env> --component <name> [--digest <manifestDigest>]
|
|
166
|
+
* -o <dir> [--json]
|
|
167
|
+
*
|
|
168
|
+
* Materializes a persisted `BuildArchiveManifest` (#609,
|
|
169
|
+
* ../../lifecycle/build-ledger-store.ts) to a portable directory: every
|
|
170
|
+
* `image`/`template`/`asset`/`sbom` entry is copied byte-for-byte from where
|
|
171
|
+
* the build that produced it left it on disk (its `path`, resolved against
|
|
172
|
+
* the current working directory — the same directory a component's
|
|
173
|
+
* `docker-build`/`addArchiveTemplate`/etc. step wrote it into) under that
|
|
174
|
+
* same archive-relative path in `-o <dir>`, plus a `manifest.json` so the
|
|
175
|
+
* directory is self-describing without chant. No re-synthesis: this reads
|
|
176
|
+
* bytes a prior `chant build`/`run --components` already produced, never
|
|
177
|
+
* regenerates them (#929) — the "byte-identical, promote-by-digest" contract
|
|
178
|
+
* the build archive is built on (see ../../components/verbs/build-archive.ts).
|
|
179
|
+
*
|
|
180
|
+
* Manifest resolution:
|
|
181
|
+
* - `--digest <manifestDigest>` reads it directly (`readBuildManifest`),
|
|
182
|
+
* independent of any environment/release record.
|
|
183
|
+
* - Otherwise resolves "the manifest behind this component's most recent
|
|
184
|
+
* successful run" in `<env>`: the latest release record's own
|
|
185
|
+
* `manifestDigest` when it was recorded, else a reverse lookup by its
|
|
186
|
+
* promoted artifact digest (`findBuildManifestByArtifactDigest`) — the
|
|
187
|
+
* same join `chant components status` already performs.
|
|
188
|
+
*
|
|
189
|
+
* A source file missing on disk (the build that produced the manifest ran
|
|
190
|
+
* somewhere else, or its output was since cleaned) is reported per entry
|
|
191
|
+
* rather than silently dropped; the command exits non-zero so an incomplete
|
|
192
|
+
* export is never mistaken for a complete one.
|
|
193
|
+
*/
|
|
194
|
+
export async function runComponentsExport(ctx: CommandContext): Promise<number> {
|
|
195
|
+
const { args } = ctx;
|
|
196
|
+
const environment = args.extraPositional;
|
|
197
|
+
const component = args.component;
|
|
198
|
+
const outDir = args.output;
|
|
199
|
+
const usage = "chant components export <env> --component <name> [--digest <manifestDigest>] -o <dir>";
|
|
200
|
+
|
|
201
|
+
if (!outDir) {
|
|
202
|
+
console.error(formatError({ message: "-o/--output <dir> is required", hint: usage }));
|
|
203
|
+
return 1;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
await fetchLifecycle();
|
|
207
|
+
|
|
208
|
+
let manifest: BuildArchiveManifest;
|
|
209
|
+
|
|
210
|
+
if (args.digest) {
|
|
211
|
+
const resolved = await readBuildManifest(args.digest);
|
|
212
|
+
if (!resolved) {
|
|
213
|
+
console.error(formatError({ message: `No persisted build manifest found for digest ${args.digest}` }));
|
|
214
|
+
return 1;
|
|
215
|
+
}
|
|
216
|
+
if (component && resolved.component !== component) {
|
|
217
|
+
console.error(formatWarning({
|
|
218
|
+
message: `--component ${component} does not match the resolved manifest's component (${resolved.component}) — exporting the manifest at --digest ${args.digest} anyway`,
|
|
219
|
+
}));
|
|
220
|
+
}
|
|
221
|
+
manifest = resolved;
|
|
222
|
+
} else {
|
|
223
|
+
if (!environment || !component) {
|
|
224
|
+
console.error(formatError({
|
|
225
|
+
message: "Environment and --component are required unless --digest <manifestDigest> is given",
|
|
226
|
+
hint: usage,
|
|
227
|
+
}));
|
|
228
|
+
return 1;
|
|
229
|
+
}
|
|
230
|
+
const ledger = await readReleaseLedger(environment);
|
|
231
|
+
const latest = latestPerComponent(ledger.records).get(component);
|
|
232
|
+
if (!latest) {
|
|
233
|
+
console.error(formatError({
|
|
234
|
+
message: `No release record for component "${component}" in environment "${environment}"`,
|
|
235
|
+
}));
|
|
236
|
+
return 1;
|
|
237
|
+
}
|
|
238
|
+
const resolved = latest.manifestDigest
|
|
239
|
+
? await readBuildManifest(latest.manifestDigest)
|
|
240
|
+
: await findBuildManifestByArtifactDigest(latest.digest);
|
|
241
|
+
if (!resolved) {
|
|
242
|
+
console.error(formatError({
|
|
243
|
+
message: `No persisted build manifest found for ${component}@${environment}'s recorded digest ${latest.digest}`,
|
|
244
|
+
hint: "The build predates #609, or was recorded via `chant components release` alone with no corresponding `chant build`/`run --components`.",
|
|
245
|
+
}));
|
|
246
|
+
return 1;
|
|
247
|
+
}
|
|
248
|
+
manifest = resolved;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const targetDir = resolve(outDir);
|
|
252
|
+
const sourceRoot = resolve(".");
|
|
253
|
+
await mkdir(targetDir, { recursive: true });
|
|
254
|
+
|
|
255
|
+
const entries = [...manifest.contents].sort((a, b) => a.path.localeCompare(b.path));
|
|
256
|
+
const results: ExportEntryResult[] = [];
|
|
257
|
+
for (const entry of entries) {
|
|
258
|
+
try {
|
|
259
|
+
const bytes = await readFile(resolve(sourceRoot, entry.path));
|
|
260
|
+
const target = join(targetDir, entry.path);
|
|
261
|
+
await mkdir(dirname(target), { recursive: true });
|
|
262
|
+
await writeFile(target, bytes);
|
|
263
|
+
results.push({ kind: entry.kind, path: entry.path, digest: entry.digest, status: "materialized" });
|
|
264
|
+
} catch (err) {
|
|
265
|
+
results.push({
|
|
266
|
+
kind: entry.kind,
|
|
267
|
+
path: entry.path,
|
|
268
|
+
digest: entry.digest,
|
|
269
|
+
status: "missing",
|
|
270
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
await writeFile(join(targetDir, "manifest.json"), JSON.stringify(manifest, sortedJsonReplacer, 2));
|
|
276
|
+
|
|
277
|
+
const missing = results.filter((r) => r.status === "missing");
|
|
278
|
+
|
|
279
|
+
if (args.json) {
|
|
280
|
+
console.log(JSON.stringify({
|
|
281
|
+
component: manifest.component,
|
|
282
|
+
manifestDigest: manifest.manifestDigest,
|
|
283
|
+
outDir: targetDir,
|
|
284
|
+
entries: results,
|
|
285
|
+
}, null, 2));
|
|
286
|
+
} else {
|
|
287
|
+
console.log(formatBold(`${manifest.component} (${manifest.manifestDigest}) -> ${targetDir}`));
|
|
288
|
+
for (const r of results) {
|
|
289
|
+
const status = r.status === "materialized" ? formatSuccess("ok") : formatError({ message: r.reason ?? "missing" });
|
|
290
|
+
console.log(` ${r.kind.padEnd(10)}${r.path.padEnd(40)}${status}`);
|
|
291
|
+
}
|
|
292
|
+
console.log(` ${"manifest".padEnd(10)}${"manifest.json".padEnd(40)}${formatSuccess("ok")}`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (missing.length > 0) {
|
|
296
|
+
console.error(formatError({
|
|
297
|
+
message: `${missing.length} of ${results.length} archive entr${missing.length === 1 ? "y" : "ies"} not found on disk — export incomplete`,
|
|
298
|
+
hint: `Run this from the checkout where the build that produced ${manifest.manifestDigest} still has its output on disk.`,
|
|
299
|
+
}));
|
|
300
|
+
return 1;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
console.error(formatSuccess(`Exported ${results.length} entr${results.length === 1 ? "y" : "ies"} + manifest.json to ${targetDir}`));
|
|
304
|
+
return 0;
|
|
305
|
+
}
|
|
306
|
+
|
|
150
307
|
/** One row of `chant components status` JSON output. */
|
|
151
308
|
interface StatusJsonRow {
|
|
152
309
|
component: string;
|
|
@@ -595,7 +752,7 @@ export async function runComponentsStatus(ctx: CommandContext): Promise<number>
|
|
|
595
752
|
export async function runComponentsUnknown(ctx: CommandContext): Promise<number> {
|
|
596
753
|
console.error(formatError({
|
|
597
754
|
message: `Unknown components subcommand: ${ctx.args.path}`,
|
|
598
|
-
hint: "Available: chant components status [env], chant components release <env>",
|
|
755
|
+
hint: "Available: chant components status [env], chant components release <env>, chant components export <env>",
|
|
599
756
|
}));
|
|
600
757
|
return 1;
|
|
601
758
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, test, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { mkdtemp, readFile, rm, stat } from "node:fs/promises";
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { DECLARABLE_MARKER, type Declarable } from "../../declarable";
|
|
@@ -90,4 +90,73 @@ describe("runExplain", () => {
|
|
|
90
90
|
const code = await runExplain(ctx({ format: "okf" }));
|
|
91
91
|
expect(code).toBe(1);
|
|
92
92
|
});
|
|
93
|
+
|
|
94
|
+
describe("knowledge sections (#1867)", () => {
|
|
95
|
+
let projectDir: string;
|
|
96
|
+
|
|
97
|
+
beforeEach(async () => {
|
|
98
|
+
projectDir = await mkdtemp(join(tmpdir(), "chant-explain-knowledge-"));
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
afterEach(async () => {
|
|
102
|
+
await rm(projectDir, { recursive: true, force: true });
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("omits the Knowledge section cleanly when no bundle exists", async () => {
|
|
106
|
+
const code = await runExplain(ctx({ path: projectDir }));
|
|
107
|
+
expect(code).toBe(0);
|
|
108
|
+
expect(logSpy.mock.calls[0][0]).not.toContain("## Knowledge");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("json omits the knowledge field cleanly when no bundle exists", async () => {
|
|
112
|
+
const code = await runExplain(ctx({ path: projectDir, format: "json" }));
|
|
113
|
+
expect(code).toBe(0);
|
|
114
|
+
const parsed = JSON.parse(logSpy.mock.calls[0][0] as string);
|
|
115
|
+
expect(parsed.knowledge).toBeUndefined();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("markdown includes a per-entity Knowledge section for bound concepts", async () => {
|
|
119
|
+
await mkdir(join(projectDir, "knowledge", "decisions"), { recursive: true });
|
|
120
|
+
await writeFile(
|
|
121
|
+
join(projectDir, "knowledge", "decisions", "public-vpc.md"),
|
|
122
|
+
"---\ntype: decision\ntitle: Public VPC\nbinds: vpc\n---\nWhy the VPC is public.\n",
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const code = await runExplain(ctx({ path: projectDir }));
|
|
126
|
+
expect(code).toBe(0);
|
|
127
|
+
const out = logSpy.mock.calls[0][0] as string;
|
|
128
|
+
expect(out).toContain("## Knowledge");
|
|
129
|
+
expect(out).toContain("### `vpc`");
|
|
130
|
+
expect(out).toContain("decision: Public VPC (`decisions/public-vpc.md`)");
|
|
131
|
+
expect(out).not.toContain("### `subnet`");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("json carries type/title/path per bound entity", async () => {
|
|
135
|
+
await mkdir(join(projectDir, "knowledge", "decisions"), { recursive: true });
|
|
136
|
+
await writeFile(
|
|
137
|
+
join(projectDir, "knowledge", "decisions", "public-vpc.md"),
|
|
138
|
+
"---\ntype: decision\ntitle: Public VPC\nbinds: vpc\n---\nWhy the VPC is public.\n",
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
const code = await runExplain(ctx({ path: projectDir, format: "json" }));
|
|
142
|
+
expect(code).toBe(0);
|
|
143
|
+
const parsed = JSON.parse(logSpy.mock.calls[0][0] as string);
|
|
144
|
+
expect(parsed.knowledge).toEqual({
|
|
145
|
+
vpc: [{ type: "decision", title: "Public VPC", path: "decisions/public-vpc.md" }],
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("--format okf output is untouched by a present bundle", async () => {
|
|
150
|
+
await mkdir(join(projectDir, "knowledge"), { recursive: true });
|
|
151
|
+
await writeFile(
|
|
152
|
+
join(projectDir, "knowledge", "note.md"),
|
|
153
|
+
"---\ntype: decision\ntitle: Note\nbinds: vpc\n---\nBody.\n",
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const code = await runExplain(ctx({ path: projectDir, format: "okf" }));
|
|
157
|
+
expect(code).toBe(0);
|
|
158
|
+
const parsed = JSON.parse(logSpy.mock.calls[0][0] as string);
|
|
159
|
+
expect(Object.keys(parsed.files).sort()).toEqual(["gcp/subnet.md", "gcp/vpc.md", "index.md"]);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
93
162
|
});
|
|
@@ -431,6 +431,26 @@ describe("runGraph", () => {
|
|
|
431
431
|
expect(discoverMock).not.toHaveBeenCalled();
|
|
432
432
|
});
|
|
433
433
|
|
|
434
|
+
test("--components --format ir carries declared composites (#1492); undeclared components have no composites attr", async () => {
|
|
435
|
+
lintMock.mockResolvedValue({ success: true });
|
|
436
|
+
componentGraphMock.mockResolvedValue({
|
|
437
|
+
success: true,
|
|
438
|
+
order: ["shared-foundation", "loom-backend"],
|
|
439
|
+
waves: [["shared-foundation"], ["loom-backend"]],
|
|
440
|
+
edges: [{ from: "loom-backend", to: "shared-foundation" }],
|
|
441
|
+
files: {},
|
|
442
|
+
composites: { "shared-foundation": ["ArtifactBucket", "OperatorRole"] },
|
|
443
|
+
});
|
|
444
|
+
const exit = await runGraph({ args: makeArgs({ format: "ir", components: true }), plugins: [], serializers: [] });
|
|
445
|
+
expect(exit).toBe(0);
|
|
446
|
+
const ir = JSON.parse(stdoutBuf.join("\n"));
|
|
447
|
+
expect(ir.nodes.find((n: { id: string }) => n.id === "shared-foundation").attrs.composites).toEqual([
|
|
448
|
+
"ArtifactBucket",
|
|
449
|
+
"OperatorRole",
|
|
450
|
+
]);
|
|
451
|
+
expect("composites" in ir.nodes.find((n: { id: string }) => n.id === "loom-backend").attrs).toBe(false);
|
|
452
|
+
});
|
|
453
|
+
|
|
434
454
|
test("--components --format mermaid lanes the components by wave", async () => {
|
|
435
455
|
lintMock.mockResolvedValue({ success: true });
|
|
436
456
|
componentGraphClean();
|
|
@@ -77,7 +77,7 @@ async function graphBuildParams(
|
|
|
77
77
|
async function graphBuildRootContributors(
|
|
78
78
|
ctx: CommandContext,
|
|
79
79
|
projectPath: string,
|
|
80
|
-
): Promise<Array<
|
|
80
|
+
): Promise<Array<import("../../lexicon").BuildRootContributor>> {
|
|
81
81
|
const { config, configPath } = await loadChantConfigUpward(projectPath).catch(
|
|
82
82
|
() => ({ config: {} as ChantConfig, configPath: undefined }),
|
|
83
83
|
);
|
|
@@ -109,7 +109,7 @@ async function graphBuildRootContributors(
|
|
|
109
109
|
*/
|
|
110
110
|
async function mergeGraphBuildRoots(
|
|
111
111
|
entities: Map<string, import("../../declarable").Declarable>,
|
|
112
|
-
contributors: Array<
|
|
112
|
+
contributors: Array<import("../../lexicon").BuildRootContributor>,
|
|
113
113
|
): Promise<boolean> {
|
|
114
114
|
if (contributors.length === 0) return true;
|
|
115
115
|
const merged = await mergeBuildRootEntities(entities, contributors);
|
|
@@ -580,7 +580,16 @@ async function runComponentGraphView(
|
|
|
580
580
|
// `liveNames` (#1491): the resources this component owns — declared, or
|
|
581
581
|
// the contract's `[name]` identity fallback — so a consumer can join
|
|
582
582
|
// the component DAG to the resource graph instead of guessing at kinds.
|
|
583
|
-
|
|
583
|
+
// `composites` (#1492): the composite kind(s) this component's
|
|
584
|
+
// resources come from, when the component declared them explicitly —
|
|
585
|
+
// omitted from attrs (not defaulted) when it didn't, so a consumer
|
|
586
|
+
// knows to fall back to its own naming-convention guess rather than
|
|
587
|
+
// reading "no composites" from an empty array.
|
|
588
|
+
attrs: {
|
|
589
|
+
wave: waveOf.get(name) ?? null,
|
|
590
|
+
liveNames: graph.liveNames?.[name] ?? [name],
|
|
591
|
+
...(graph.composites?.[name] ? { composites: graph.composites[name] } : {}),
|
|
592
|
+
},
|
|
584
593
|
// Deep-link the node to its `*.component.ts` (behold's inspect panel).
|
|
585
594
|
...(graph.files?.[name] ? { sourceLoc: { file: graph.files[name] } } : {}),
|
|
586
595
|
})),
|