@intentius/chant 0.80.0 → 0.81.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 +8 -16
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/audit/report-html.d.ts +5 -0
- package/dist/audit/report-html.d.ts.map +1 -1
- package/dist/audit/report-model.d.ts +13 -0
- package/dist/audit/report-model.d.ts.map +1 -1
- package/dist/audit/terraform-state.d.ts +4 -5
- package/dist/audit/terraform-state.d.ts.map +1 -1
- package/dist/cli/commands/audit.d.ts +25 -9
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon.d.ts +8 -0
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/doctor.d.ts.map +1 -1
- package/dist/cli/commands/import-agents.d.ts.map +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/commands/update.d.ts +6 -0
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/commands/vendor.d.ts +5 -0
- package/dist/cli/commands/vendor.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/components.d.ts.map +1 -1
- package/dist/cli/handlers/dev.d.ts.map +1 -1
- package/dist/cli/handlers/fan-out.d.ts.map +1 -1
- package/dist/cli/handlers/init.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts +7 -0
- package/dist/cli/handlers/operator.d.ts.map +1 -1
- package/dist/cli/handlers/promote.d.ts +7 -4
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/run-generate.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts +4 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/vendor.d.ts +8 -3
- package/dist/cli/handlers/vendor.d.ts.map +1 -1
- package/dist/cli/main.d.ts +12 -0
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts +12 -7
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +32 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/codegen/fetch.d.ts +10 -0
- package/dist/codegen/fetch.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +3 -1
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/discover.d.ts.map +1 -1
- package/dist/components/driver.d.ts +16 -2
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/gate-plan.d.ts +40 -0
- package/dist/components/gate-plan.d.ts.map +1 -0
- package/dist/components/promote.d.ts +60 -7
- package/dist/components/promote.d.ts.map +1 -1
- package/dist/components/sandbox/driver.d.ts.map +1 -1
- package/dist/components/sandbox/run.d.ts.map +1 -1
- package/dist/components/verbs/build-archive.d.ts +22 -11
- package/dist/components/verbs/build-archive.d.ts.map +1 -1
- package/dist/components/verbs/run-agent.d.ts.map +1 -1
- package/dist/config-static.d.ts +59 -0
- package/dist/config-static.d.ts.map +1 -0
- package/dist/config.d.ts +32 -10
- package/dist/config.d.ts.map +1 -1
- package/dist/content-digest.d.ts +17 -0
- package/dist/content-digest.d.ts.map +1 -0
- package/dist/discovery/files.d.ts +10 -5
- package/dist/discovery/files.d.ts.map +1 -1
- package/dist/discovery/fold-import.d.ts +20 -4
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/discovery/sandbox/driver.d.ts +20 -0
- package/dist/discovery/sandbox/driver.d.ts.map +1 -1
- package/dist/discovery/sandbox/fork.d.ts +20 -0
- package/dist/discovery/sandbox/fork.d.ts.map +1 -1
- package/dist/discovery/walk.d.ts +119 -0
- package/dist/discovery/walk.d.ts.map +1 -0
- package/dist/graph-ir.d.ts +4 -1
- package/dist/graph-ir.d.ts.map +1 -1
- package/dist/join-key.d.ts +31 -0
- package/dist/join-key.d.ts.map +1 -0
- package/dist/lexicon-module.d.ts +51 -6
- package/dist/lexicon-module.d.ts.map +1 -1
- package/dist/lexicon.d.ts +58 -1
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/build-ledger-store.d.ts.map +1 -1
- package/dist/lifecycle/digest.d.ts +5 -1
- package/dist/lifecycle/digest.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.d.ts +20 -5
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/lifecycle/git.d.ts +64 -0
- package/dist/lifecycle/git.d.ts.map +1 -1
- package/dist/lifecycle/lease.d.ts +8 -1
- package/dist/lifecycle/lease.d.ts.map +1 -1
- package/dist/lifecycle/legacy-digest.d.ts +41 -8
- package/dist/lifecycle/legacy-digest.d.ts.map +1 -1
- package/dist/lifecycle/member-ledger.d.ts +67 -0
- package/dist/lifecycle/member-ledger.d.ts.map +1 -0
- package/dist/lifecycle/plan-digest.d.ts +4 -3
- package/dist/lifecycle/plan-digest.d.ts.map +1 -1
- package/dist/lifecycle/release-ledger.d.ts +18 -2
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lifecycle/status.d.ts +90 -5
- package/dist/lifecycle/status.d.ts.map +1 -1
- package/dist/op/activities/activity-contracts.d.ts +32 -0
- package/dist/op/activities/activity-contracts.d.ts.map +1 -1
- package/dist/op/activities/index.d.ts +2 -0
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/propose-upgrade.d.ts +75 -0
- package/dist/op/activities/propose-upgrade.d.ts.map +1 -0
- package/dist/op/discover.d.ts.map +1 -1
- package/dist/op/gate-name.d.ts +7 -0
- package/dist/op/gate-name.d.ts.map +1 -1
- package/dist/op/gate-summary.d.ts +16 -0
- package/dist/op/gate-summary.d.ts.map +1 -1
- package/dist/op/gate.d.ts +16 -2
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/project-root.d.ts +26 -0
- package/dist/project-root.d.ts.map +1 -1
- package/dist/workspace/__fixtures__/contract-repo.d.ts +37 -0
- package/dist/workspace/__fixtures__/contract-repo.d.ts.map +1 -0
- package/dist/workspace/__fixtures__/two-member-workspace.d.ts +11 -0
- package/dist/workspace/__fixtures__/two-member-workspace.d.ts.map +1 -0
- package/dist/workspace/checks/generated.d.ts +156 -0
- package/dist/workspace/checks/generated.d.ts.map +1 -0
- package/dist/workspace/checks/ledgers.d.ts +78 -0
- package/dist/workspace/checks/ledgers.d.ts.map +1 -0
- package/dist/workspace/checks/links.d.ts +25 -0
- package/dist/workspace/checks/links.d.ts.map +1 -0
- package/dist/workspace/checks/pipelines.d.ts +76 -0
- package/dist/workspace/checks/pipelines.d.ts.map +1 -0
- package/dist/workspace/checks.d.ts +161 -0
- package/dist/workspace/checks.d.ts.map +1 -0
- package/dist/workspace/compose-graph.d.ts +115 -0
- package/dist/workspace/compose-graph.d.ts.map +1 -0
- package/dist/workspace/compose-reports.d.ts +68 -0
- package/dist/workspace/compose-reports.d.ts.map +1 -0
- package/dist/workspace/declaration.d.ts +220 -0
- package/dist/workspace/declaration.d.ts.map +1 -0
- package/dist/workspace/declaration.schema.json +434 -0
- package/dist/workspace/generated-files.d.ts +65 -0
- package/dist/workspace/generated-files.d.ts.map +1 -0
- package/dist/workspace/glob.d.ts +13 -0
- package/dist/workspace/glob.d.ts.map +1 -0
- package/dist/workspace/graph-cli.d.ts +75 -0
- package/dist/workspace/graph-cli.d.ts.map +1 -0
- package/dist/workspace/init.d.ts +96 -0
- package/dist/workspace/init.d.ts.map +1 -0
- package/dist/workspace/jsonc.d.ts +38 -0
- package/dist/workspace/jsonc.d.ts.map +1 -0
- package/dist/workspace/kinds.d.ts +192 -0
- package/dist/workspace/kinds.d.ts.map +1 -0
- package/dist/workspace/lineage-check.d.ts +84 -0
- package/dist/workspace/lineage-check.d.ts.map +1 -0
- package/dist/workspace/lineage-cli.d.ts +31 -0
- package/dist/workspace/lineage-cli.d.ts.map +1 -0
- package/dist/workspace/lineage-init.d.ts +101 -0
- package/dist/workspace/lineage-init.d.ts.map +1 -0
- package/dist/workspace/lineage-lock.d.ts +258 -0
- package/dist/workspace/lineage-lock.d.ts.map +1 -0
- package/dist/workspace/lineage-merge.d.ts +14 -0
- package/dist/workspace/lineage-merge.d.ts.map +1 -0
- package/dist/workspace/lineage-migrations.d.ts +178 -0
- package/dist/workspace/lineage-migrations.d.ts.map +1 -0
- package/dist/workspace/lineage-update.d.ts +59 -0
- package/dist/workspace/lineage-update.d.ts.map +1 -0
- package/dist/workspace/lineage-upgrade-cli.d.ts +40 -0
- package/dist/workspace/lineage-upgrade-cli.d.ts.map +1 -0
- package/dist/workspace/lineage-upgrade.d.ts +146 -0
- package/dist/workspace/lineage-upgrade.d.ts.map +1 -0
- package/dist/workspace/lineage-vendor.d.ts +63 -0
- package/dist/workspace/lineage-vendor.d.ts.map +1 -0
- package/dist/workspace/lineage-version.d.ts +32 -0
- package/dist/workspace/lineage-version.d.ts.map +1 -0
- package/dist/workspace/links.d.ts +150 -0
- package/dist/workspace/links.d.ts.map +1 -0
- package/dist/workspace/ls.d.ts +99 -0
- package/dist/workspace/ls.d.ts.map +1 -0
- package/dist/workspace/member-commands.d.ts +139 -0
- package/dist/workspace/member-commands.d.ts.map +1 -0
- package/dist/workspace/member-handles.d.ts +54 -0
- package/dist/workspace/member-handles.d.ts.map +1 -0
- package/dist/workspace/member-pipeline.d.ts +143 -0
- package/dist/workspace/member-pipeline.d.ts.map +1 -0
- package/dist/workspace/member-run.d.ts +67 -0
- package/dist/workspace/member-run.d.ts.map +1 -0
- package/dist/workspace/reason-codes.d.ts +65 -0
- package/dist/workspace/reason-codes.d.ts.map +1 -0
- package/dist/workspace/records-cli.d.ts +28 -4
- package/dist/workspace/records-cli.d.ts.map +1 -1
- package/dist/workspace/records.d.ts.map +1 -1
- package/dist/workspace/root-refusal.d.ts +53 -0
- package/dist/workspace/root-refusal.d.ts.map +1 -0
- package/dist/workspace/status.d.ts +150 -0
- package/dist/workspace/status.d.ts.map +1 -0
- package/dist/workspace/template-manifest.d.ts +90 -0
- package/dist/workspace/template-manifest.d.ts.map +1 -0
- package/dist/workspace/tree.d.ts +36 -0
- package/dist/workspace/tree.d.ts.map +1 -0
- package/dist/workspace/trust/attestor.d.ts +67 -0
- package/dist/workspace/trust/attestor.d.ts.map +1 -0
- package/dist/workspace/trust/policy.d.ts +111 -0
- package/dist/workspace/trust/policy.d.ts.map +1 -0
- package/dist/workspace/trust/provenance.d.ts +67 -0
- package/dist/workspace/trust/provenance.d.ts.map +1 -0
- package/dist/workspace/trust/ssh-commit.d.ts +43 -0
- package/dist/workspace/trust/ssh-commit.d.ts.map +1 -0
- package/dist/workspace/trust/test-repo.d.ts +42 -0
- package/dist/workspace/trust/test-repo.d.ts.map +1 -0
- package/dist/workspace/trust/verify-cli.d.ts +9 -0
- package/dist/workspace/trust/verify-cli.d.ts.map +1 -0
- package/dist/workspace/trust/verify.d.ts +66 -0
- package/dist/workspace/trust/verify.d.ts.map +1 -0
- package/dist/workspace/which-chant.d.ts +58 -0
- package/dist/workspace/which-chant.d.ts.map +1 -0
- package/dist/workspace/workspace-kinds.schema.json +133 -0
- package/package.json +2 -1
- package/src/audit/discover.ts +66 -78
- package/src/audit/report-html.ts +3 -1
- package/src/audit/report-model.ts +14 -1
- package/src/audit/terraform-state.test.ts +4 -9
- package/src/audit/terraform-state.ts +4 -5
- package/src/cli/commands/audit-truncation.test.ts +167 -0
- package/src/cli/commands/audit.ts +89 -60
- package/src/cli/commands/check-lexicon.ts +24 -0
- package/src/cli/commands/doctor.test.ts +30 -0
- package/src/cli/commands/doctor.ts +18 -0
- package/src/cli/commands/import-agents.ts +6 -2
- package/src/cli/commands/init.ts +22 -3
- package/src/cli/commands/lint.test.ts +17 -23
- package/src/cli/commands/lint.ts +24 -84
- package/src/cli/commands/update.ts +13 -4
- package/src/cli/commands/vendor.ts +11 -6
- package/src/cli/handlers/__snapshots__/build-member.test.ts.snap +38 -0
- package/src/cli/handlers/build-member.test.ts +127 -0
- package/src/cli/handlers/build.ts +35 -1
- package/src/cli/handlers/components.test.ts +127 -0
- package/src/cli/handlers/components.ts +70 -8
- package/src/cli/handlers/dev.ts +5 -3
- package/src/cli/handlers/fan-out.ts +9 -2
- package/src/cli/handlers/init.ts +54 -1
- package/src/cli/handlers/operator.test.ts +68 -0
- package/src/cli/handlers/operator.ts +37 -9
- package/src/cli/handlers/promote.test.ts +118 -2
- package/src/cli/handlers/promote.ts +82 -16
- package/src/cli/handlers/run-generate-member.test.ts +83 -0
- package/src/cli/handlers/run-generate.test.ts +6 -8
- package/src/cli/handlers/run-generate.ts +36 -1
- package/src/cli/handlers/run.test.ts +47 -0
- package/src/cli/handlers/run.ts +28 -5
- package/src/cli/handlers/vendor.ts +147 -43
- package/src/cli/main.test.ts +105 -1
- package/src/cli/main.ts +230 -17
- package/src/cli/plugins.ts +17 -17
- package/src/cli/registry.ts +32 -0
- package/src/cli/static-config-read.test.ts +208 -0
- package/src/codegen/fetch.test.ts +63 -1
- package/src/codegen/fetch.ts +30 -1
- package/src/components/cli-support.ts +6 -1
- package/src/components/discover.test.ts +27 -57
- package/src/components/discover.ts +13 -68
- package/src/components/driver.test.ts +78 -1
- package/src/components/driver.ts +43 -7
- package/src/components/gate-plan.ts +53 -0
- package/src/components/promote.test.ts +276 -2
- package/src/components/promote.ts +119 -15
- package/src/components/sandbox/driver.ts +8 -8
- package/src/components/sandbox/run.ts +4 -1
- package/src/components/verbs/build-archive.ts +27 -21
- package/src/components/verbs/run-agent.ts +3 -5
- package/src/config-static.test.ts +136 -0
- package/src/config-static.ts +232 -0
- package/src/config.test.ts +2 -2
- package/src/config.ts +48 -15
- package/src/content-digest.test.ts +120 -0
- package/src/content-digest.ts +21 -0
- package/src/discovery/files.test.ts +10 -10
- package/src/discovery/files.ts +25 -81
- package/src/discovery/fold-import.ts +49 -5
- package/src/discovery/index.ts +1 -0
- package/src/discovery/sandbox/driver.ts +73 -35
- package/src/discovery/sandbox/fork-diagnostic.test.ts +136 -13
- package/src/discovery/sandbox/fork.ts +83 -43
- package/src/discovery/sandbox/path-lexicon-multifile.test.ts +285 -0
- package/src/discovery/walk.test.ts +263 -0
- package/src/discovery/walk.ts +334 -0
- package/src/effect-receipt.ts +2 -2
- package/src/graph-ir.ts +4 -1
- package/src/join-key.test.ts +25 -0
- package/src/join-key.ts +39 -0
- package/src/lexicon-module.ts +108 -10
- package/src/lexicon.ts +79 -1
- package/src/lifecycle/build-ledger-store.ts +11 -10
- package/src/lifecycle/digest.ts +9 -5
- package/src/lifecycle/gate-ledger.test.ts +20 -0
- package/src/lifecycle/gate-ledger.ts +28 -6
- package/src/lifecycle/git.test.ts +13 -1
- package/src/lifecycle/git.ts +221 -176
- package/src/lifecycle/lease.ts +25 -10
- package/src/lifecycle/legacy-digest.test.ts +115 -23
- package/src/lifecycle/legacy-digest.ts +60 -19
- package/src/lifecycle/member-ledger.test.ts +319 -0
- package/src/lifecycle/member-ledger.ts +145 -0
- package/src/lifecycle/plan-digest.ts +7 -7
- package/src/lifecycle/release-ledger.test.ts +20 -0
- package/src/lifecycle/release-ledger.ts +23 -7
- package/src/lifecycle/status.test.ts +151 -1
- package/src/lifecycle/status.ts +228 -7
- package/src/op/activities/activity-contracts.ts +37 -0
- package/src/op/activities/index.ts +10 -0
- package/src/op/activities/propose-upgrade.ts +206 -0
- package/src/op/discover.test.ts +14 -4
- package/src/op/discover.ts +19 -27
- package/src/op/gate-name.ts +8 -0
- package/src/op/gate-summary.test.ts +14 -0
- package/src/op/gate-summary.ts +25 -2
- package/src/op/gate.ts +36 -7
- package/src/op/generate-pipeline.test.ts +14 -5
- package/src/project-root.test.ts +66 -1
- package/src/project-root.ts +48 -0
- package/src/workspace/__fixtures__/contract-repo.ts +100 -0
- package/src/workspace/__fixtures__/two-member-workspace.ts +35 -0
- package/src/workspace/behold-kinds.test.ts +162 -0
- package/src/workspace/check-contract.test.ts +127 -0
- package/src/workspace/check.schema.json +347 -0
- package/src/workspace/checks/generated.test.ts +287 -0
- package/src/workspace/checks/generated.ts +475 -0
- package/src/workspace/checks/ledgers.test.ts +139 -0
- package/src/workspace/checks/ledgers.ts +246 -0
- package/src/workspace/checks/links.ts +183 -0
- package/src/workspace/checks/pipelines.test.ts +133 -0
- package/src/workspace/checks/pipelines.ts +225 -0
- package/src/workspace/checks-repo.test.ts +71 -0
- package/src/workspace/checks.test.ts +450 -0
- package/src/workspace/checks.ts +461 -0
- package/src/workspace/compose-graph.test.ts +146 -0
- package/src/workspace/compose-graph.ts +228 -0
- package/src/workspace/compose-reports.test.ts +109 -0
- package/src/workspace/compose-reports.ts +186 -0
- package/src/workspace/declaration.schema.json +434 -0
- package/src/workspace/declaration.test.ts +347 -0
- package/src/workspace/declaration.ts +623 -0
- package/src/workspace/generated-files.test.ts +114 -0
- package/src/workspace/generated-files.ts +94 -0
- package/src/workspace/glob.ts +49 -0
- package/src/workspace/graph-cli.ts +220 -0
- package/src/workspace/graph-contract.test.ts +204 -0
- package/src/workspace/graph.schema.json +662 -0
- package/src/workspace/init.test.ts +223 -0
- package/src/workspace/init.ts +585 -0
- package/src/workspace/jsonc.test.ts +65 -0
- package/src/workspace/jsonc.ts +231 -0
- package/src/workspace/kinds.test.ts +288 -0
- package/src/workspace/kinds.ts +506 -0
- package/src/workspace/lineage-check.ts +264 -0
- package/src/workspace/lineage-cli.ts +114 -0
- package/src/workspace/lineage-init.test.ts +231 -0
- package/src/workspace/lineage-init.ts +365 -0
- package/src/workspace/lineage-lock.test.ts +224 -0
- package/src/workspace/lineage-lock.ts +357 -0
- package/src/workspace/lineage-merge.ts +44 -0
- package/src/workspace/lineage-migrations.test.ts +178 -0
- package/src/workspace/lineage-migrations.ts +343 -0
- package/src/workspace/lineage-update.ts +160 -0
- package/src/workspace/lineage-upgrade-cli.ts +151 -0
- package/src/workspace/lineage-upgrade.test.ts +474 -0
- package/src/workspace/lineage-upgrade.ts +624 -0
- package/src/workspace/lineage-vendor.test.ts +126 -0
- package/src/workspace/lineage-vendor.ts +215 -0
- package/src/workspace/lineage-version.ts +127 -0
- package/src/workspace/links.test.ts +344 -0
- package/src/workspace/links.ts +331 -0
- package/src/workspace/ls-contract.test.ts +200 -0
- package/src/workspace/ls.schema.json +190 -0
- package/src/workspace/ls.ts +254 -0
- package/src/workspace/member-commands.e2e.test.ts +244 -0
- package/src/workspace/member-commands.test.ts +217 -0
- package/src/workspace/member-commands.ts +540 -0
- package/src/workspace/member-handles.test.ts +76 -0
- package/src/workspace/member-handles.ts +228 -0
- package/src/workspace/member-pipeline.test.ts +135 -0
- package/src/workspace/member-pipeline.ts +317 -0
- package/src/workspace/member-run.ts +146 -0
- package/src/workspace/read-contract.test.ts +129 -0
- package/src/workspace/reason-codes.test.ts +113 -0
- package/src/workspace/reason-codes.ts +81 -0
- package/src/workspace/records-cli.ts +86 -14
- package/src/workspace/records-contract.test.ts +5 -0
- package/src/workspace/records.schema.json +32 -2
- package/src/workspace/records.ts +3 -2
- package/src/workspace/root-refusal.ts +97 -0
- package/src/workspace/status-contract.test.ts +308 -0
- package/src/workspace/status.schema.json +210 -0
- package/src/workspace/status.ts +412 -0
- package/src/workspace/template-manifest.test.ts +79 -0
- package/src/workspace/template-manifest.ts +221 -0
- package/src/workspace/tree.ts +132 -0
- package/src/workspace/trust/attestor.ts +93 -0
- package/src/workspace/trust/policy.ts +306 -0
- package/src/workspace/trust/provenance.ts +181 -0
- package/src/workspace/trust/ssh-commit.ts +138 -0
- package/src/workspace/trust/test-repo.ts +138 -0
- package/src/workspace/trust/trust.test.ts +397 -0
- package/src/workspace/trust/verify-cli.ts +56 -0
- package/src/workspace/trust/verify.ts +194 -0
- package/src/workspace/which-chant.test.ts +117 -0
- package/src/workspace/which-chant.ts +151 -0
- package/src/workspace/workspace-kinds.schema.json +133 -0
- package/dist/components/unbound-gate-approval.d.ts +0 -24
- package/dist/components/unbound-gate-approval.d.ts.map +0 -1
- package/dist/discovery/convergence.d.ts +0 -97
- package/dist/discovery/convergence.d.ts.map +0 -1
- package/src/cli/commands/audit-walk-warnings.test.ts +0 -156
- package/src/components/unbound-gate-approval.test.ts +0 -82
- package/src/components/unbound-gate-approval.ts +0 -51
- package/src/discovery/convergence.test.ts +0 -231
- package/src/discovery/convergence.ts +0 -399
- package/src/op/discover-convergence.test.ts +0 -70
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import { describe, test, expect } from "vitest";
|
|
2
|
-
import { fork } from "node:child_process";
|
|
3
|
-
import {
|
|
1
|
+
import { describe, test, expect, vi } from "vitest";
|
|
2
|
+
import { fork, type ChildProcess } from "node:child_process";
|
|
3
|
+
import { EventEmitter } from "node:events";
|
|
4
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, realpathSync } from "node:fs";
|
|
4
5
|
import { tmpdir } from "node:os";
|
|
5
6
|
import { join } from "node:path";
|
|
6
7
|
import { runFallbackFilesSandboxed } from "./run";
|
|
8
|
+
import { forkSandboxed, settleOnExitAfterChannelDrains } from "./fork";
|
|
9
|
+
import { generateConfigDriverSource, generateDriverSource, generatePolicyDriverSource, sendFunctionSource } from "./driver";
|
|
7
10
|
|
|
8
11
|
/**
|
|
9
12
|
* chant#2461 — a child that ends without a usable result says which way it did.
|
|
@@ -22,13 +25,13 @@ import { runFallbackFilesSandboxed } from "./run";
|
|
|
22
25
|
* - a payload the parent's `isResponse` refuses, which used to be dropped in
|
|
23
26
|
* silence and then reported as though nothing had been sent
|
|
24
27
|
*
|
|
25
|
-
* A third mechanism
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
28
|
+
* A third mechanism, the one the issue first proposed, was ruled out early on
|
|
29
|
+
* a test that blocked the parent (`message` won 60 of 60) and turned out to be
|
|
30
|
+
* the real one: under concurrent load the parent can handle a child's `exit`
|
|
31
|
+
* before the `message` it had already written. A stress run of the real
|
|
32
|
+
* `evaluateConfigSandboxed` lost 3 of 800 that way. The parent now waits for
|
|
33
|
+
* the IPC channel to drain before it reads an exit, and the drivers await
|
|
34
|
+
* their `process.send` callback; both are pinned below.
|
|
32
35
|
*/
|
|
33
36
|
function runChild(source: string): Promise<{ code: number | null; message: unknown; stderr: string }> {
|
|
34
37
|
const dir = mkdtempSync(join(tmpdir(), "chant-fork-diag-"));
|
|
@@ -62,9 +65,9 @@ describe("why a sandboxed child ends without a result (chant#2461)", () => {
|
|
|
62
65
|
expect(r.stderr.trim()).toBe("");
|
|
63
66
|
});
|
|
64
67
|
|
|
65
|
-
test("a child that sends and falls off the end
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
+
test("a child that sends and falls off the end gets its message through", async () => {
|
|
69
|
+
// Usually true even on a bare `exit` handler; the drain wait is what makes
|
|
70
|
+
// it true every time. See the settleOnExitAfterChannelDrains tests below.
|
|
68
71
|
const results = await Promise.all(
|
|
69
72
|
Array.from({ length: 12 }, () => runChild("process.send({ ok: true });\n")),
|
|
70
73
|
);
|
|
@@ -123,3 +126,123 @@ describe("why a sandboxed child ends without a result (chant#2461)", () => {
|
|
|
123
126
|
}
|
|
124
127
|
}, 120_000);
|
|
125
128
|
});
|
|
129
|
+
|
|
130
|
+
/** A stand-in for a ChildProcess: just the events and the `connected` flag the helper reads. */
|
|
131
|
+
function fakeChild(connected: boolean): ChildProcess & EventEmitter {
|
|
132
|
+
const child = new EventEmitter() as ChildProcess & EventEmitter;
|
|
133
|
+
(child as { connected: boolean }).connected = connected;
|
|
134
|
+
return child;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
describe("settleOnExitAfterChannelDrains (chant#2461)", () => {
|
|
138
|
+
test("an exit handled before the last message waits for the channel, so the message wins", () => {
|
|
139
|
+
// The observed ordering: exit first, the already-written message after,
|
|
140
|
+
// then the channel's EOF. Settling on exit alone dropped the message.
|
|
141
|
+
const child = fakeChild(true);
|
|
142
|
+
const order: string[] = [];
|
|
143
|
+
child.on("message", () => order.push("message"));
|
|
144
|
+
settleOnExitAfterChannelDrains(child, (code) => order.push(`exit:${code}`));
|
|
145
|
+
|
|
146
|
+
child.emit("exit", 0, null);
|
|
147
|
+
child.emit("message", { ok: true });
|
|
148
|
+
expect(order).toEqual(["message"]);
|
|
149
|
+
|
|
150
|
+
child.emit("disconnect");
|
|
151
|
+
expect(order).toEqual(["message", "exit:0"]);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("a channel that has already closed settles on exit straight away", () => {
|
|
155
|
+
const child = fakeChild(false);
|
|
156
|
+
const onExit = vi.fn();
|
|
157
|
+
settleOnExitAfterChannelDrains(child, onExit);
|
|
158
|
+
child.emit("exit", 3, null);
|
|
159
|
+
expect(onExit).toHaveBeenCalledWith(3, null);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("a channel that never reports closed is bounded by the grace period", () => {
|
|
163
|
+
vi.useFakeTimers();
|
|
164
|
+
try {
|
|
165
|
+
const child = fakeChild(true);
|
|
166
|
+
const onExit = vi.fn();
|
|
167
|
+
settleOnExitAfterChannelDrains(child, onExit, 50);
|
|
168
|
+
child.emit("exit", 0, null);
|
|
169
|
+
vi.advanceTimersByTime(49);
|
|
170
|
+
expect(onExit).not.toHaveBeenCalled();
|
|
171
|
+
vi.advanceTimersByTime(1);
|
|
172
|
+
expect(onExit).toHaveBeenCalledTimes(1);
|
|
173
|
+
child.emit("disconnect");
|
|
174
|
+
expect(onExit).toHaveBeenCalledTimes(1);
|
|
175
|
+
} finally {
|
|
176
|
+
vi.useRealTimers();
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("real sandboxed children that send and return all resolve, many at once", async () => {
|
|
181
|
+
// The shape of the stress harness, small enough for CI: a --permission
|
|
182
|
+
// child that sends and returns, through forkSandboxed's settle logic.
|
|
183
|
+
const dir = realpathSync(mkdtempSync(join(tmpdir(), "chant-2461-many-")));
|
|
184
|
+
try {
|
|
185
|
+
const file = join(dir, "child.mjs");
|
|
186
|
+
writeFileSync(file, 'process.send({ kind: "chant-config", ok: true, config: { pad: "x".repeat(65536) } });\n');
|
|
187
|
+
const isResponse = (v: unknown): v is { kind: string } =>
|
|
188
|
+
typeof v === "object" && v !== null && (v as { kind?: unknown }).kind === "chant-config";
|
|
189
|
+
const results = await Promise.all(
|
|
190
|
+
Array.from({ length: 32 }, () =>
|
|
191
|
+
forkSandboxed(
|
|
192
|
+
{ bundlePath: file, bundleDir: dir, projectRealpath: dir, externalReadPaths: [], env: { PATH: process.env.PATH ?? "" }, timeoutMs: 60_000, label: "many", outputPrefix: "[t]" },
|
|
193
|
+
isResponse,
|
|
194
|
+
),
|
|
195
|
+
),
|
|
196
|
+
);
|
|
197
|
+
expect(results.every((r) => r.kind === "chant-config")).toBe(true);
|
|
198
|
+
} finally {
|
|
199
|
+
rmSync(dir, { recursive: true, force: true });
|
|
200
|
+
}
|
|
201
|
+
}, 120_000);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe("driver send awaits delivery and fails loudly (chant#2461)", () => {
|
|
205
|
+
const harness = (body: string): string =>
|
|
206
|
+
['import { writeSync } from "node:fs";', ...sendFunctionSource(), body].join("\n");
|
|
207
|
+
|
|
208
|
+
test("a send that succeeds resolves, and the code after it runs", async () => {
|
|
209
|
+
const r = await runChild(harness('await send({ ok: 1 }); writeSync(2, "after\\n");'));
|
|
210
|
+
expect(r.code).toBe(0);
|
|
211
|
+
expect(r.message).toEqual({ ok: 1 });
|
|
212
|
+
expect(r.stderr).toBe("after\n");
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("a send on a closed channel exits non-zero with the reason on stderr", async () => {
|
|
216
|
+
const r = await runChild(harness('process.disconnect(); await send({ ok: 1 }); writeSync(2, "unreachable\\n");'));
|
|
217
|
+
expect(r.code).toBe(1);
|
|
218
|
+
expect(r.message).toBeUndefined();
|
|
219
|
+
expect(r.stderr).toContain("could not hand its result to the parent");
|
|
220
|
+
expect(r.stderr).toContain("chant#2461");
|
|
221
|
+
expect(r.stderr).not.toContain("unreachable");
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("a payload that cannot be serialized exits non-zero with the reason on stderr", async () => {
|
|
225
|
+
const r = await runChild(harness("await send({ big: 1n });"));
|
|
226
|
+
expect(r.code).toBe(1);
|
|
227
|
+
expect(r.message).toBeUndefined();
|
|
228
|
+
expect(r.stderr).toContain("could not hand its result to the parent");
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("every driver awaits or returns every send", () => {
|
|
232
|
+
// A bare `send(...)` statement would let main() return before the write
|
|
233
|
+
// completes. The only unawaited call allowed is main().catch's, whose
|
|
234
|
+
// promise is the last thing the module does.
|
|
235
|
+
const sources = {
|
|
236
|
+
config: generateConfigDriverSource("/tmp/project/chant.config.ts"),
|
|
237
|
+
run: generateDriverSource({ files: ["/tmp/project/a.ts"], buildRoot: "/tmp/project" }),
|
|
238
|
+
policy: generatePolicyDriverSource(["/tmp/project/policy.ts"]),
|
|
239
|
+
};
|
|
240
|
+
for (const [name, source] of Object.entries(sources)) {
|
|
241
|
+
const bare = source
|
|
242
|
+
.split("\n")
|
|
243
|
+
.filter((line) => /^\s*(send|fail)\(/.test(line))
|
|
244
|
+
.filter((line) => !/^\s*send\(\{[^\n]*fatal: true|^\s*send\(\{ kind: "chant-config", ok: false, error: classifyChildError\([^)]*, err\)\.toJSON\(\) \}\),$/.test(line));
|
|
245
|
+
expect({ name, bare }).toEqual({ name, bare: [] });
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { fork } from "node:child_process";
|
|
1
|
+
import { fork, type ChildProcess } from "node:child_process";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* The one place chant starts a sandboxed child process.
|
|
@@ -131,38 +131,33 @@ function lineBuffered(emit: (line: string) => void) {
|
|
|
131
131
|
* chant#2461 — this used to be one sentence, `child exited before reporting
|
|
132
132
|
* results (code N, signal S)`, for three unrelated situations. The one that
|
|
133
133
|
* was observed in the wild is the hardest to read: exit code 0, empty stderr,
|
|
134
|
-
* no message. That reads like the child was cut off, and it was not
|
|
135
|
-
* to completion
|
|
134
|
+
* no message. That reads like the child was cut off, and it was not: it ran
|
|
135
|
+
* to completion.
|
|
136
136
|
*
|
|
137
|
-
*
|
|
137
|
+
* The observed case turned out to be the parent's fault. A config child that
|
|
138
|
+
* sent its result and exited could have its `exit` dispatched here BEFORE its
|
|
139
|
+
* already-written `message`, and the old `exit` handler settled on the spot.
|
|
140
|
+
* The payload arrived a moment later and was ignored. Stress runs of the real
|
|
141
|
+
* `evaluateConfigSandboxed`, 24 at a time under 16 CPU hogs, lost 3 of 800 and
|
|
142
|
+
* 13 of 1500 that way. An earlier test that blocked the parent had seen
|
|
143
|
+
* `message` win 60 of 60, which is why this was ruled out for a while.
|
|
144
|
+
* {@link settleOnExitAfterChannelDrains} closes it by not reading the exit
|
|
145
|
+
* until the IPC channel has drained. Every driver also awaits its
|
|
146
|
+
* `process.send` callback and exits 1 with a stderr line if the send fails
|
|
147
|
+
* (see `./driver.ts`'s `sendFunctionSource`), so a result that never left the
|
|
148
|
+
* child cannot look like exit 0 either.
|
|
138
149
|
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
* The second is the payload being lost between `process.send` and exit.
|
|
152
|
-
*
|
|
153
|
-
* For the CONFIG driver the first is impossible, which is worth stating because
|
|
154
|
-
* it was the working hypothesis until the bundle was read. Its `await
|
|
155
|
-
* import(configPath)` bundles to `await Promise.resolve().then(() =>
|
|
156
|
-
* (init_chant_config(), chant_config_exports))` — one microtask over
|
|
157
|
-
* synchronous code — and the bundle has no runtime imports, no dynamic
|
|
158
|
-
* `import(`, and exactly one `process.send`. There is nothing there to hang on.
|
|
159
|
-
* So a config child that exits 0 with nothing sent DID send, and the payload
|
|
160
|
-
* did not arrive.
|
|
161
|
-
*
|
|
162
|
-
* The exit/message race originally proposed in chant#2461 is a third thing and
|
|
163
|
-
* is not it: with the parent blocked so a queued payload and the reap were both
|
|
164
|
-
* pending, `message` was dispatched first 60 times out of 60, because the live
|
|
165
|
-
* IPC channel keeps the child alive until the payload flushes.
|
|
150
|
+
* With both of those in place, exit 0 with nothing on stderr and nothing
|
|
151
|
+
* received has one known cause left: an `await` that never settles.
|
|
152
|
+
* `main().catch(...)` catches a REJECTION, and a promise that never settles is
|
|
153
|
+
* not one, so if nothing keeps the loop alive Node drains it and exits 0
|
|
154
|
+
* having sent nothing. On the RUN path that is reproducible: the driver does
|
|
155
|
+
* `await import(<project file>)` per run-fallback file, module scope is
|
|
156
|
+
* arbitrary project source, and a file whose top level never settles makes
|
|
157
|
+
* that import never complete (the run driver names the file on stderr in that
|
|
158
|
+
* case). The CONFIG driver bundles to one microtask over synchronous code with
|
|
159
|
+
* no runtime imports, so on that path the cause is not known, and the message
|
|
160
|
+
* says so rather than guessing.
|
|
166
161
|
*/
|
|
167
162
|
function describeSilentExit(
|
|
168
163
|
label: string,
|
|
@@ -190,21 +185,64 @@ function describeSilentExit(
|
|
|
190
185
|
if (stderr) return `${head}: ${stderr}`;
|
|
191
186
|
if (code !== 0 || signal !== null) return head;
|
|
192
187
|
|
|
193
|
-
// Exit 0, nothing on stderr, nothing
|
|
194
|
-
//
|
|
195
|
-
// it is depends on the driver — see this function's doc.
|
|
188
|
+
// Exit 0, nothing on stderr, and nothing received even after the IPC
|
|
189
|
+
// channel drained. See this function's doc for what is and is not left.
|
|
196
190
|
return (
|
|
197
|
-
`${head}. It exited cleanly with nothing on stderr and
|
|
198
|
-
`
|
|
199
|
-
`cause is a project file with a top-level \`await\` that does not settle
|
|
200
|
-
`arbitrary project source,
|
|
201
|
-
`never settles is not a rejection, so the driver's own \`main().catch\` does
|
|
202
|
-
`either
|
|
203
|
-
`
|
|
204
|
-
`
|
|
191
|
+
`${head}. It exited cleanly with nothing on stderr, and no message arrived even after its ` +
|
|
192
|
+
`IPC channel closed, so it never sent one: something it awaited never settled. On the run ` +
|
|
193
|
+
`path the usual cause is a project file with a top-level \`await\` that does not settle. ` +
|
|
194
|
+
`Module scope is arbitrary project source, an import of such a file never completes, and a ` +
|
|
195
|
+
`promise that never settles is not a rejection, so the driver's own \`main().catch\` does ` +
|
|
196
|
+
`not see it either. The config driver has nothing to await that can hang, and a failed ` +
|
|
197
|
+
`send exits non-zero with a reason on stderr, so on the config path this has no known ` +
|
|
198
|
+
`cause: please report it with the command that produced it (chant#2461).`
|
|
205
199
|
);
|
|
206
200
|
}
|
|
207
201
|
|
|
202
|
+
/** How long {@link settleOnExitAfterChannelDrains} waits, after exit, for the IPC channel to report closed. Normally it takes well under a millisecond; this only bounds a channel that never does. */
|
|
203
|
+
const EXIT_DRAIN_GRACE_MS = 2_000;
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* chant#2461 — call `onExit` once `child` has exited AND its IPC channel has
|
|
207
|
+
* drained, so a message the child wrote before exiting is always dispatched
|
|
208
|
+
* first.
|
|
209
|
+
*
|
|
210
|
+
* Node does not order `exit` after `message`. The child's exit and the last
|
|
211
|
+
* bytes on the IPC socket reach the parent's event loop as separate events,
|
|
212
|
+
* and under load the exit can be handled first even though the child wrote
|
|
213
|
+
* the message before it exited. Settling on `exit` alone then drops a payload
|
|
214
|
+
* that is already sitting in the socket. The channel reports `disconnect`
|
|
215
|
+
* only after it has read EOF, which comes after every message the child
|
|
216
|
+
* wrote, so waiting for it is exact.
|
|
217
|
+
*
|
|
218
|
+
* This is deliberately not `close`. `close` also waits for stdout and stderr
|
|
219
|
+
* to end, and a grandchild holding those pipes would turn a fast failure into
|
|
220
|
+
* the full timeout. The wait here is on the IPC channel alone, and is bounded
|
|
221
|
+
* by `graceMs` in case that channel never reports closed.
|
|
222
|
+
*/
|
|
223
|
+
export function settleOnExitAfterChannelDrains(
|
|
224
|
+
child: ChildProcess,
|
|
225
|
+
onExit: (code: number | null, signal: NodeJS.Signals | null) => void,
|
|
226
|
+
graceMs = EXIT_DRAIN_GRACE_MS,
|
|
227
|
+
): void {
|
|
228
|
+
child.on("exit", (code, signal) => {
|
|
229
|
+
let done = false;
|
|
230
|
+
let grace: NodeJS.Timeout | undefined;
|
|
231
|
+
const finish = (): void => {
|
|
232
|
+
if (done) return;
|
|
233
|
+
done = true;
|
|
234
|
+
if (grace !== undefined) clearTimeout(grace);
|
|
235
|
+
onExit(code, signal);
|
|
236
|
+
};
|
|
237
|
+
if (!child.connected) {
|
|
238
|
+
finish();
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
child.once("disconnect", finish);
|
|
242
|
+
grace = setTimeout(finish, graceMs);
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
208
246
|
export function forkSandboxed<T>(
|
|
209
247
|
options: SandboxForkOptions,
|
|
210
248
|
isResponse: (value: unknown) => value is T,
|
|
@@ -298,7 +336,9 @@ export function forkSandboxed<T>(
|
|
|
298
336
|
reject(err);
|
|
299
337
|
});
|
|
300
338
|
|
|
301
|
-
child.on("exit"
|
|
339
|
+
// chant#2461 — not `child.on("exit")`: a payload can still be in the IPC
|
|
340
|
+
// socket when the exit is handled. See settleOnExitAfterChannelDrains.
|
|
341
|
+
settleOnExitAfterChannelDrains(child, (code, signal) => {
|
|
302
342
|
if (settled) return;
|
|
303
343
|
settled = true;
|
|
304
344
|
clearTimeout(timeout);
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* chant#2590 — every file of a multi-file lexicon declared by path gets the
|
|
3
|
+
* fold trust a subpath of the same lexicon installed as a package gets, with
|
|
4
|
+
* and without `--sandbox`.
|
|
5
|
+
*
|
|
6
|
+
* The lexicon is three files: `resources.ts`, `composites.ts` (which imports
|
|
7
|
+
* `./resources`) and `index.ts` (which re-exports both and holds the plugin).
|
|
8
|
+
* It is written once as the package `@intentius/chant-lexicon-demo` and once
|
|
9
|
+
* as `./lexicon/` named by path. One project file imports the lexicon's entry
|
|
10
|
+
* and one imports a constructor from its `resources` file directly, by
|
|
11
|
+
* package subpath or by path. Outputs and fold verdicts are compared.
|
|
12
|
+
*
|
|
13
|
+
* The rest of the file covers the bounds of the trusted directory.
|
|
14
|
+
*/
|
|
15
|
+
import { describe, test, expect, beforeAll, afterAll, afterEach } from "vitest";
|
|
16
|
+
import { mkdir, mkdtemp, realpath, rm, writeFile } from "node:fs/promises";
|
|
17
|
+
import { join, resolve } from "node:path";
|
|
18
|
+
import { tmpdir } from "node:os";
|
|
19
|
+
import { build } from "../../build";
|
|
20
|
+
import { loadChantConfig } from "../../config";
|
|
21
|
+
import { lexiconModuleRoot, pathLexiconRoot, resetLexiconModules } from "../../lexicon-module";
|
|
22
|
+
import { createFoldSession } from "../fold-import";
|
|
23
|
+
import type { LexiconPlugin } from "../../lexicon";
|
|
24
|
+
|
|
25
|
+
const runtimePath = resolve(import.meta.dirname, "../../runtime");
|
|
26
|
+
const compositePath = resolve(import.meta.dirname, "../../composite");
|
|
27
|
+
|
|
28
|
+
const RESOURCES = `
|
|
29
|
+
import { createResource } from ${JSON.stringify(runtimePath)};
|
|
30
|
+
|
|
31
|
+
export const Bucket = createResource("Demo::Bucket", "demo", { arn: "Arn" });
|
|
32
|
+
export const Role = createResource("Demo::Role", "demo", {});
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
const COMPOSITES = `
|
|
36
|
+
import { Composite } from ${JSON.stringify(compositePath)};
|
|
37
|
+
import { Bucket, Role } from "./resources";
|
|
38
|
+
|
|
39
|
+
export const WebApp = Composite((props) => {
|
|
40
|
+
const bucket = new Bucket({ bucketName: props.name });
|
|
41
|
+
const role = new Role({ resource: bucket.arn });
|
|
42
|
+
return { bucket, role };
|
|
43
|
+
}, "WebApp");
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
const INDEX = `
|
|
47
|
+
export * from "./resources";
|
|
48
|
+
export * from "./composites";
|
|
49
|
+
|
|
50
|
+
export const Regions = { primary: "us-east-1" };
|
|
51
|
+
|
|
52
|
+
function plain(value) {
|
|
53
|
+
if (value && typeof value === "object" && typeof value.getLogicalName === "function") {
|
|
54
|
+
return { ref: value.getLogicalName(), attr: value.attribute };
|
|
55
|
+
}
|
|
56
|
+
if (Array.isArray(value)) return value.map(plain);
|
|
57
|
+
if (value && typeof value === "object") {
|
|
58
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, plain(v)]));
|
|
59
|
+
}
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const demoPlugin = {
|
|
64
|
+
name: "demo",
|
|
65
|
+
serializer: {
|
|
66
|
+
name: "demo",
|
|
67
|
+
rulePrefix: "DEMO",
|
|
68
|
+
serialize: (entities) =>
|
|
69
|
+
JSON.stringify(
|
|
70
|
+
Object.fromEntries(
|
|
71
|
+
[...entities.keys()].sort().map((name) => {
|
|
72
|
+
const entity = entities.get(name);
|
|
73
|
+
return [name, { type: entity.entityType, props: plain(entity.props) }];
|
|
74
|
+
}),
|
|
75
|
+
),
|
|
76
|
+
),
|
|
77
|
+
},
|
|
78
|
+
generate: async () => {},
|
|
79
|
+
validate: async () => {},
|
|
80
|
+
coverage: async () => {},
|
|
81
|
+
package: async () => {},
|
|
82
|
+
};
|
|
83
|
+
`;
|
|
84
|
+
|
|
85
|
+
function mainSource(entry: string): string {
|
|
86
|
+
return `
|
|
87
|
+
import { Bucket, Regions, WebApp } from ${JSON.stringify(entry)};
|
|
88
|
+
|
|
89
|
+
export const logs = new Bucket({ bucketName: Regions.primary });
|
|
90
|
+
export const web = WebApp({ name: "data" });
|
|
91
|
+
`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function rolesSource(resources: string): string {
|
|
95
|
+
return `
|
|
96
|
+
import { Role } from ${JSON.stringify(resources)};
|
|
97
|
+
|
|
98
|
+
export const deployer = new Role({ resource: "deploy" });
|
|
99
|
+
`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function writeLexicon(dir: string): Promise<void> {
|
|
103
|
+
await mkdir(dir, { recursive: true });
|
|
104
|
+
await writeFile(join(dir, "resources.ts"), RESOURCES);
|
|
105
|
+
await writeFile(join(dir, "composites.ts"), COMPOSITES);
|
|
106
|
+
await writeFile(join(dir, "index.ts"), INDEX);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let root: string;
|
|
110
|
+
|
|
111
|
+
beforeAll(async () => {
|
|
112
|
+
root = await realpath(await mkdtemp(join(tmpdir(), "chant-2590-")));
|
|
113
|
+
|
|
114
|
+
const pkg = join(root, "pkg");
|
|
115
|
+
const pkgDir = join(pkg, "node_modules", "@intentius", "chant-lexicon-demo");
|
|
116
|
+
await writeLexicon(pkgDir);
|
|
117
|
+
await writeFile(
|
|
118
|
+
join(pkgDir, "package.json"),
|
|
119
|
+
JSON.stringify({
|
|
120
|
+
name: "@intentius/chant-lexicon-demo",
|
|
121
|
+
type: "module",
|
|
122
|
+
exports: { ".": "./index.ts", "./resources": "./resources.ts" },
|
|
123
|
+
}),
|
|
124
|
+
);
|
|
125
|
+
await mkdir(join(pkg, "src"));
|
|
126
|
+
await writeFile(join(pkg, "src", "main.ts"), mainSource("@intentius/chant-lexicon-demo"));
|
|
127
|
+
await writeFile(join(pkg, "src", "roles.ts"), rolesSource("@intentius/chant-lexicon-demo/resources"));
|
|
128
|
+
await writeFile(join(pkg, "chant.config.ts"), `export default { lexicons: ["demo"] };\n`);
|
|
129
|
+
|
|
130
|
+
const path = join(root, "path");
|
|
131
|
+
await writeLexicon(join(path, "lexicon"));
|
|
132
|
+
await mkdir(join(path, "src"));
|
|
133
|
+
await writeFile(join(path, "src", "main.ts"), mainSource("../lexicon"));
|
|
134
|
+
await writeFile(join(path, "src", "roles.ts"), rolesSource("../lexicon/resources"));
|
|
135
|
+
await writeFile(
|
|
136
|
+
join(path, "chant.config.ts"),
|
|
137
|
+
`export default { lexicons: [{ name: "demo", module: "./lexicon/index.ts" }] };\n`,
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
// The same lexicon with its module at the project root: only the module
|
|
141
|
+
// file is trusted, so its sibling stays a project file.
|
|
142
|
+
const flat = join(root, "flat");
|
|
143
|
+
await mkdir(join(flat, "src"), { recursive: true });
|
|
144
|
+
await writeFile(join(flat, "resources.ts"), RESOURCES);
|
|
145
|
+
await writeFile(join(flat, "composites.ts"), COMPOSITES);
|
|
146
|
+
await writeFile(join(flat, "demo.ts"), INDEX);
|
|
147
|
+
await writeFile(join(flat, "src", "roles.ts"), rolesSource("../resources"));
|
|
148
|
+
await writeFile(join(flat, "chant.config.ts"), `export default { lexicons: [{ name: "demo", module: "./demo.ts" }] };\n`);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
afterAll(async () => {
|
|
152
|
+
await rm(root, { recursive: true, force: true });
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
afterEach(() => {
|
|
156
|
+
resetLexiconModules();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
async function buildProject(project: "pkg" | "path" | "flat", modes: { fold: boolean; sandbox: boolean }) {
|
|
160
|
+
const dir = join(root, project);
|
|
161
|
+
const lexiconFile = {
|
|
162
|
+
pkg: join(dir, "node_modules", "@intentius", "chant-lexicon-demo", "index.ts"),
|
|
163
|
+
path: join(dir, "lexicon", "index.ts"),
|
|
164
|
+
flat: join(dir, "demo.ts"),
|
|
165
|
+
}[project];
|
|
166
|
+
await loadChantConfig(dir);
|
|
167
|
+
const plugin = (await import(lexiconFile)).demoPlugin as LexiconPlugin;
|
|
168
|
+
const result = await build(join(dir, "src"), [plugin.serializer], undefined, {
|
|
169
|
+
...modes,
|
|
170
|
+
lexicons: [plugin.name],
|
|
171
|
+
});
|
|
172
|
+
return {
|
|
173
|
+
errors: result.errors,
|
|
174
|
+
output: result.outputs.get("demo") as string,
|
|
175
|
+
decisions: result.foldDecisions
|
|
176
|
+
.map((d) => ({ file: d.file.slice(dir.length), mode: d.mode, reason: d.reason }))
|
|
177
|
+
.sort((a, b) => a.file.localeCompare(b.file)),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
describe("a multi-file lexicon declared by path folds like the same lexicon installed as a package (chant#2590)", () => {
|
|
182
|
+
for (const modes of [
|
|
183
|
+
{ fold: true, sandbox: true },
|
|
184
|
+
{ fold: true, sandbox: false },
|
|
185
|
+
]) {
|
|
186
|
+
test(
|
|
187
|
+
`--fold${modes.sandbox ? " --sandbox" : ""}: same output, same fold verdicts`,
|
|
188
|
+
async () => {
|
|
189
|
+
const asPackage = await buildProject("pkg", modes);
|
|
190
|
+
const byPath = await buildProject("path", modes);
|
|
191
|
+
|
|
192
|
+
expect(asPackage.errors).toEqual([]);
|
|
193
|
+
expect(byPath.errors).toEqual([]);
|
|
194
|
+
expect(byPath.output).toBe(asPackage.output);
|
|
195
|
+
expect(JSON.parse(byPath.output)).toEqual({
|
|
196
|
+
deployer: { type: "Demo::Role", props: { resource: "deploy" } },
|
|
197
|
+
logs: { type: "Demo::Bucket", props: { bucketName: "us-east-1" } },
|
|
198
|
+
webBucket: { type: "Demo::Bucket", props: { bucketName: "data" } },
|
|
199
|
+
webRole: { type: "Demo::Role", props: { resource: { ref: "webBucket", attr: "Arn" } } },
|
|
200
|
+
});
|
|
201
|
+
expect(byPath.decisions).toEqual(asPackage.decisions);
|
|
202
|
+
expect(byPath.decisions).toEqual([
|
|
203
|
+
{ file: "/src/main.ts", mode: "fold", reason: undefined },
|
|
204
|
+
{ file: "/src/roles.ts", mode: "fold", reason: undefined },
|
|
205
|
+
]);
|
|
206
|
+
},
|
|
207
|
+
60_000,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
test("a module at the project root trusts only itself: under --sandbox its sibling runs in the child", async () => {
|
|
212
|
+
const flat = await buildProject("flat", { fold: true, sandbox: true });
|
|
213
|
+
expect(flat.errors).toEqual([]);
|
|
214
|
+
expect(JSON.parse(flat.output)).toEqual({ deployer: { type: "Demo::Role", props: { resource: "deploy" } } });
|
|
215
|
+
expect(flat.decisions).toHaveLength(1);
|
|
216
|
+
expect(flat.decisions[0]).toMatchObject({ file: "/src/roles.ts", mode: "run" });
|
|
217
|
+
expect(flat.decisions[0].reason).toContain("neither chant's own nor an active lexicon");
|
|
218
|
+
}, 60_000);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe("the trusted directory's bounds (chant#2590)", () => {
|
|
222
|
+
const project = "/p";
|
|
223
|
+
|
|
224
|
+
test("defaults to the module's own directory", () => {
|
|
225
|
+
expect(pathLexiconRoot({ module: "./lexicon/index.ts" }, project)).toEqual({ root: "/p/lexicon" });
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("a module at the project root gets no directory", () => {
|
|
229
|
+
expect(pathLexiconRoot({ module: "./demo.ts" }, project)).toEqual({});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("a module outside the project gets no directory", () => {
|
|
233
|
+
expect(pathLexiconRoot({ module: "/elsewhere/demo.ts" }, project)).toEqual({});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("the module's directory is dropped when it contains the source directory", () => {
|
|
237
|
+
expect(pathLexiconRoot({ module: "./infra/lexicon.ts" }, project, "infra/src")).toEqual({});
|
|
238
|
+
expect(pathLexiconRoot({ module: "./infra/lexicon.ts" }, project, "infra")).toEqual({});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("a declared root is used when it holds the module", () => {
|
|
242
|
+
expect(pathLexiconRoot({ module: "./lexicon/src/index.ts", root: "./lexicon" }, project)).toEqual({
|
|
243
|
+
root: "/p/lexicon",
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("a declared root outside the project, over the source, or not holding the module is a problem", () => {
|
|
248
|
+
expect(pathLexiconRoot({ module: "./lexicon/index.ts", root: ".." }, project).problem).toContain("outside the project");
|
|
249
|
+
expect(pathLexiconRoot({ module: "./lexicon/index.ts", root: "." }, project).problem).toContain(
|
|
250
|
+
"contains the project's source directory",
|
|
251
|
+
);
|
|
252
|
+
expect(pathLexiconRoot({ module: "./lexicon/index.ts", root: "./other" }, project).problem).toContain(
|
|
253
|
+
"is not inside",
|
|
254
|
+
);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("the config loader refuses a declared root that breaks a rule", async () => {
|
|
258
|
+
const dir = join(root, "bad-root");
|
|
259
|
+
await mkdir(join(dir, "lexicon"), { recursive: true });
|
|
260
|
+
await writeFile(
|
|
261
|
+
join(dir, "chant.config.json"),
|
|
262
|
+
JSON.stringify({ lexicons: [{ name: "demo", module: "./lexicon/index.ts", root: "." }] }),
|
|
263
|
+
);
|
|
264
|
+
await expect(loadChantConfig(dir)).rejects.toThrow(/lexicons\.0\.root: .*contains the project's source directory/);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test("the config loader records a declared root", async () => {
|
|
268
|
+
const dir = join(root, "good-root");
|
|
269
|
+
await mkdir(join(dir, "lexicon", "src"), { recursive: true });
|
|
270
|
+
await writeFile(
|
|
271
|
+
join(dir, "chant.config.json"),
|
|
272
|
+
JSON.stringify({ lexicons: [{ name: "demo", module: "./lexicon/src/index.ts", root: "./lexicon" }] }),
|
|
273
|
+
);
|
|
274
|
+
await loadChantConfig(dir);
|
|
275
|
+
expect(lexiconModuleRoot("demo")).toBe(join(dir, "lexicon"));
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test("a fold session drops a directory holding any of the build's own source files", async () => {
|
|
279
|
+
const dir = join(root, "path");
|
|
280
|
+
await loadChantConfig(dir);
|
|
281
|
+
expect(createFoldSession([], undefined, ["demo"]).lexiconRoots).toEqual([join(dir, "lexicon")]);
|
|
282
|
+
const session = createFoldSession([], undefined, ["demo"], true, [], false, [join(dir, "lexicon", "extra.ts")]);
|
|
283
|
+
expect(session.lexiconRoots).toEqual([]);
|
|
284
|
+
});
|
|
285
|
+
});
|