@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
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Member links (#2539): declared links resolved in source, inferred joins
|
|
3
|
+
* labelled by the core joinKey(), ambiguity rows, and the WSP091 to WSP097
|
|
4
|
+
* checks.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { execFileSync } from "node:child_process";
|
|
8
|
+
import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { dirname, join, resolve } from "node:path";
|
|
11
|
+
import { afterAll, describe, expect, test } from "vitest";
|
|
12
|
+
import { runDeclarationChecks } from "./checks";
|
|
13
|
+
import { parseDeclaration, WorkspaceReadError } from "./declaration";
|
|
14
|
+
import { declaredMemberLinks, graphLinks, resolveLinks, type LinkTableRow, type MemberHandles } from "./links";
|
|
15
|
+
import { builtinKindRegistry } from "./kinds";
|
|
16
|
+
|
|
17
|
+
const scratch: string[] = [];
|
|
18
|
+
afterAll(() => {
|
|
19
|
+
for (const d of scratch) rmSync(d, { recursive: true, force: true });
|
|
20
|
+
});
|
|
21
|
+
function repo(files: Record<string, string>): string {
|
|
22
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), "chant-links-")));
|
|
23
|
+
scratch.push(root);
|
|
24
|
+
execFileSync("git", ["init", "-q"], { cwd: root });
|
|
25
|
+
for (const [path, text] of Object.entries(files)) {
|
|
26
|
+
mkdirSync(dirname(join(root, path)), { recursive: true });
|
|
27
|
+
writeFileSync(join(root, path), text);
|
|
28
|
+
}
|
|
29
|
+
return root;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const declaration = (members: unknown[], extra: Record<string, unknown> = {}) =>
|
|
33
|
+
JSON.stringify({ name: "acme", schema: 1, members, ...extra }, null, 2);
|
|
34
|
+
|
|
35
|
+
const producerSource = (outputs: Record<string, string>) =>
|
|
36
|
+
[
|
|
37
|
+
`import { output } from "@intentius/chant-lexicon-aws";`,
|
|
38
|
+
`import { cluster, listener } from "./alb";`,
|
|
39
|
+
...Object.entries(outputs).map(([binding, name]) => `export const ${binding} = output(cluster.Arn, ${JSON.stringify(name)});`),
|
|
40
|
+
`void listener;`,
|
|
41
|
+
].join("\n");
|
|
42
|
+
|
|
43
|
+
const consumerSource = (params: string[]) =>
|
|
44
|
+
[`import { Parameter } from "@intentius/chant-lexicon-aws";`, ...params.map((p) => `export const ${p} = new Parameter("String");`)].join("\n");
|
|
45
|
+
|
|
46
|
+
/** A producer and two consumers, the shape the issue asks for. */
|
|
47
|
+
function threeMembers(opts: { outputs?: Record<string, string>; webLinks?: unknown[]; jobsLinks?: unknown[] } = {}) {
|
|
48
|
+
return repo({
|
|
49
|
+
"chant.workspace.json": declaration([
|
|
50
|
+
{ name: "shared", dir: "infra/shared", kind: "chant" },
|
|
51
|
+
{ name: "web", dir: "services/web", kind: "chant", ...(opts.webLinks ? { links: opts.webLinks } : {}) },
|
|
52
|
+
{ name: "jobs", dir: "services/jobs", kind: "chant", ...(opts.jobsLinks ? { links: opts.jobsLinks } : {}) },
|
|
53
|
+
]),
|
|
54
|
+
"infra/shared/chant.config.ts": "",
|
|
55
|
+
"infra/shared/src/outputs.ts": producerSource(opts.outputs ?? { clusterArn: "ClusterArn", listenerArn: "ListenerArn" }),
|
|
56
|
+
"services/web/chant.config.ts": "",
|
|
57
|
+
"services/web/src/params.ts": consumerSource(["clusterArn", "listenerArn"]),
|
|
58
|
+
"services/jobs/chant.config.ts": "",
|
|
59
|
+
"services/jobs/src/params.ts": consumerSource(["clusterArn"]),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const ids = async (root: string) => (await runDeclarationChecks(root)).diagnostics.map((d) => `${d.ruleId}:${d.entity ?? ""}`);
|
|
64
|
+
const summary = (rows: LinkTableRow[]) =>
|
|
65
|
+
rows.map((r) =>
|
|
66
|
+
r.status === "ambiguous"
|
|
67
|
+
? `${r.consumer}.${r.input} ambiguous ${r.candidates.map((c) => `${c.producer}.${c.output}`).join("|")}`
|
|
68
|
+
: `${r.consumer}${r.input ? `.${r.input}` : ""} -> ${r.producer}.${r.output} ${r.origin} ${r.label} ${r.status}`,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
describe("declared links, resolved in source", () => {
|
|
72
|
+
test("a link to an output the producer's source declares resolves, and suppresses the inferred edge", async () => {
|
|
73
|
+
const root = threeMembers({ webLinks: [{ member: "shared", output: "ClusterArn" }] });
|
|
74
|
+
const report = await runDeclarationChecks(root);
|
|
75
|
+
expect(report.ok).toBe(true);
|
|
76
|
+
expect(report.diagnostics).toEqual([]);
|
|
77
|
+
expect(summary(report.links)).toEqual([
|
|
78
|
+
"web -> shared.ClusterArn declared exact resolved",
|
|
79
|
+
"jobs.clusterArn -> shared.ClusterArn inferred:joinKey folded resolved",
|
|
80
|
+
"web.listenerArn -> shared.ListenerArn inferred:joinKey folded resolved",
|
|
81
|
+
]);
|
|
82
|
+
expect(report.links[0]).toMatchObject({ resolves: "source", kind: "output", pointer: "/members/1/links/0" });
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("a renamed producer output fails check for every declared consumer", async () => {
|
|
86
|
+
const link = [{ member: "shared", output: "ClusterArn" }];
|
|
87
|
+
const root = threeMembers({ outputs: { clusterArn: "EcsClusterArn", listenerArn: "ListenerArn" }, webLinks: link, jobsLinks: link });
|
|
88
|
+
const report = await runDeclarationChecks(root);
|
|
89
|
+
expect(report.ok).toBe(false);
|
|
90
|
+
expect(report.diagnostics.map((d) => `${d.ruleId}:${d.entity}`)).toEqual(["WSP093:web", "WSP093:jobs"]);
|
|
91
|
+
expect(report.diagnostics[0].message).toBe(
|
|
92
|
+
"web's link to shared output ClusterArn does not resolve: shared has no output ClusterArn; its outputs: EcsClusterArn, ListenerArn",
|
|
93
|
+
);
|
|
94
|
+
// The finding points at the link's output in the file.
|
|
95
|
+
expect(report.diagnostics[0]).toMatchObject({ file: "chant.workspace.json", line: 17, column: 21 });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("a near miss in case is named, since declared links match exactly", async () => {
|
|
99
|
+
const root = threeMembers({ webLinks: [{ member: "shared", output: "clusterArn" }] });
|
|
100
|
+
const [d] = (await runDeclarationChecks(root)).diagnostics;
|
|
101
|
+
expect(d.ruleId).toBe("WSP093");
|
|
102
|
+
expect(d.message).toMatch(/did you mean ClusterArn\? Links match exactly/);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("an output name that can't be read without running code keeps the link unresolved, as info", async () => {
|
|
106
|
+
const root = threeMembers({ webLinks: [{ member: "shared", output: "Missing" }] });
|
|
107
|
+
writeFileSync(join(root, "infra/shared/src/more.ts"), `import { output } from "@intentius/chant-lexicon-aws";\nexport const x = output(1, process.env.NAME!);\n`);
|
|
108
|
+
const report = await runDeclarationChecks(root);
|
|
109
|
+
expect(report.ok).toBe(true);
|
|
110
|
+
expect(report.diagnostics.map((d) => `${d.ruleId}:${d.severity}`)).toEqual(["WSP094:info"]);
|
|
111
|
+
expect(report.diagnostics[0].message).toMatch(/kept unresolved: Missing was not found in shared.*infra\/shared\/src\/more\.ts:2: an output whose name is not a string literal/);
|
|
112
|
+
expect(report.links[0]).toMatchObject({ status: "unresolved" });
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("other members expose what the entry lists, and a nested workspace exposes nothing", async () => {
|
|
116
|
+
const root = repo({
|
|
117
|
+
"chant.workspace.json": declaration([
|
|
118
|
+
{ name: "legacy", dir: "legacy", kind: "other", because: "a hand-run stack", outputs: ["QueueUrl"], suppress: [{ check: "WSP009", because: "known" }] },
|
|
119
|
+
{ name: "nested", dir: "nested", kind: "workspace" },
|
|
120
|
+
{
|
|
121
|
+
name: "app",
|
|
122
|
+
dir: "app",
|
|
123
|
+
kind: "chant",
|
|
124
|
+
links: [
|
|
125
|
+
{ member: "legacy", output: "QueueUrl" },
|
|
126
|
+
{ member: "legacy", output: "TopicArn" },
|
|
127
|
+
{ member: "nested", output: "Anything" },
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
]),
|
|
131
|
+
"legacy/README": "",
|
|
132
|
+
"nested/chant.workspace.json": declaration([]),
|
|
133
|
+
"app/chant.config.ts": "",
|
|
134
|
+
});
|
|
135
|
+
const report = await runDeclarationChecks(root);
|
|
136
|
+
expect(summary(report.links)).toEqual([
|
|
137
|
+
"app -> legacy.QueueUrl declared exact resolved",
|
|
138
|
+
"app -> legacy.TopicArn declared exact missing",
|
|
139
|
+
"app -> nested.Anything declared exact missing",
|
|
140
|
+
]);
|
|
141
|
+
expect(report.diagnostics.map((d) => d.message)).toEqual([
|
|
142
|
+
"app's link to legacy output TopicArn does not resolve: legacy has no output TopicArn; its outputs: QueueUrl",
|
|
143
|
+
"app's link to nested output Anything does not resolve: nested is kind workspace, which exposes no outputs as link targets",
|
|
144
|
+
]);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("an unknown member, a group, the consumer itself, an unknown link kind and a repeat each fail", async () => {
|
|
148
|
+
const root = repo({
|
|
149
|
+
"chant.workspace.json": declaration([
|
|
150
|
+
{ name: "api", dir: "api", kind: "chant", outputs: ["Url"] },
|
|
151
|
+
{
|
|
152
|
+
name: "web",
|
|
153
|
+
dir: "web",
|
|
154
|
+
kind: "chant",
|
|
155
|
+
links: [
|
|
156
|
+
{ member: "ghost", output: "Url" },
|
|
157
|
+
{ member: "examples", output: "Url" },
|
|
158
|
+
{ member: "web", output: "Url" },
|
|
159
|
+
{ member: "api", output: "Url", kind: "depends-on" },
|
|
160
|
+
{ member: "api", output: "Url" },
|
|
161
|
+
{ member: "api", output: "Url" },
|
|
162
|
+
],
|
|
163
|
+
},
|
|
164
|
+
{ name: "examples", kind: "examples", glob: "examples/*" },
|
|
165
|
+
]),
|
|
166
|
+
"api/chant.config.ts": "",
|
|
167
|
+
"api/src/out.ts": `import * as aws from "@intentius/chant-lexicon-aws";\nconst NAME = "Url";\nexport const url = aws.output(1, NAME);\n`,
|
|
168
|
+
"web/chant.config.ts": "",
|
|
169
|
+
"examples/one/chant.config.ts": "",
|
|
170
|
+
});
|
|
171
|
+
expect(await ids(root)).toEqual(["WSP097:api", "WSP091:web", "WSP091:web", "WSP091:web", "WSP092:web", "WSP096:web"]);
|
|
172
|
+
const messages = (await runDeclarationChecks(root)).diagnostics.map((d) => d.message);
|
|
173
|
+
expect(messages).toContain("member web links to ghost, which is not a member of this workspace");
|
|
174
|
+
expect(messages).toContain("member web links to examples, which is an example group; a group has no links");
|
|
175
|
+
expect(messages).toContain("member web links to itself; a link joins two members");
|
|
176
|
+
expect(messages).toContain("member api lists outputs, and kind chant reads a member's outputs from its source; remove the list");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("WSP091 and WSP092 can't be suppressed, and WSP093 can", async () => {
|
|
180
|
+
const root = repo({
|
|
181
|
+
"chant.workspace.json": declaration([
|
|
182
|
+
{ name: "api", dir: "api", kind: "chant" },
|
|
183
|
+
{
|
|
184
|
+
name: "web",
|
|
185
|
+
dir: "web",
|
|
186
|
+
kind: "chant",
|
|
187
|
+
links: [{ member: "api", output: "Gone" }],
|
|
188
|
+
suppress: [{ check: "WSP093", because: "api is being rebuilt" }],
|
|
189
|
+
},
|
|
190
|
+
]),
|
|
191
|
+
"api/chant.config.ts": "",
|
|
192
|
+
"web/chant.config.ts": "",
|
|
193
|
+
});
|
|
194
|
+
const report = await runDeclarationChecks(root);
|
|
195
|
+
expect(report.ok).toBe(true);
|
|
196
|
+
expect(report.suppressed.map((s) => `${s.ruleId}:${s.reason}`)).toEqual(["WSP093:api is being rebuilt"]);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe("inferred joins", () => {
|
|
201
|
+
function twoProducers(webLinks?: unknown[]) {
|
|
202
|
+
return repo({
|
|
203
|
+
"chant.workspace.json": declaration([
|
|
204
|
+
{ name: "net-a", dir: "net-a", kind: "chant" },
|
|
205
|
+
{ name: "net-b", dir: "net-b", kind: "chant" },
|
|
206
|
+
{ name: "web", dir: "web", kind: "chant", ...(webLinks ? { links: webLinks } : {}) },
|
|
207
|
+
]),
|
|
208
|
+
"net-a/chant.config.ts": "",
|
|
209
|
+
"net-a/src/o.ts": producerSource({ vpc: "VpcId" }),
|
|
210
|
+
"net-b/chant.config.ts": "",
|
|
211
|
+
"net-b/src/o.ts": producerSource({ vpc: "vpc_id" }),
|
|
212
|
+
"web/chant.config.ts": "",
|
|
213
|
+
"web/src/p.ts": consumerSource(["vpcId"]),
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
test("two producers of one handle is an ambiguity row and a warning", async () => {
|
|
218
|
+
const report = await runDeclarationChecks(twoProducers());
|
|
219
|
+
expect(summary(report.links)).toEqual(["web.vpcId ambiguous net-a.VpcId|net-b.vpc_id"]);
|
|
220
|
+
expect(report.ok).toBe(true);
|
|
221
|
+
expect(report.diagnostics.map((d) => `${d.ruleId}:${d.severity}:${d.entity}`)).toEqual(["WSP095:warning:web"]);
|
|
222
|
+
expect(report.diagnostics[0].message).toBe(
|
|
223
|
+
'parameter vpcId of web matches outputs of net-a and net-b: net-a output VpcId (folded), net-b output vpc_id (folded). Declare the one it reads, such as { "member": "net-a", "output": "VpcId" } in web\'s links',
|
|
224
|
+
);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("declaring one of the matches settles the ambiguity", async () => {
|
|
228
|
+
const report = await runDeclarationChecks(twoProducers([{ member: "net-b", output: "vpc_id" }]));
|
|
229
|
+
expect(report.diagnostics).toEqual([]);
|
|
230
|
+
expect(summary(report.links)).toEqual(["web -> net-b.vpc_id declared exact resolved"]);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("equal names are labelled exact, and a member never joins itself", async () => {
|
|
234
|
+
const decl = parseDeclaration(declaration([{ name: "a", dir: "a", kind: "chant" }, { name: "b", dir: "b", kind: "chant" }]), "chant.workspace.json");
|
|
235
|
+
const h = (member: string, outputs: string[], inputs: string[]): MemberHandles => ({
|
|
236
|
+
member,
|
|
237
|
+
outputs: outputs.map((name) => ({ name })),
|
|
238
|
+
inputs: inputs.map((name) => ({ name })),
|
|
239
|
+
complete: true,
|
|
240
|
+
why: null,
|
|
241
|
+
exposesNone: false,
|
|
242
|
+
});
|
|
243
|
+
const rows = resolveLinks(decl, new Map([["a", h("a", ["Url"], ["Url"])], ["b", h("b", [], ["Url"])]]));
|
|
244
|
+
expect(summary(rows)).toEqual(["b.Url -> a.Url inferred:joinKey exact resolved"]);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe("the graph's links section", () => {
|
|
249
|
+
const decl = parseDeclaration(
|
|
250
|
+
declaration([
|
|
251
|
+
{ name: "shared", dir: "shared", kind: "chant" },
|
|
252
|
+
{ name: "web", dir: "web", kind: "chant", links: [{ member: "shared", output: "ClusterArn" }] },
|
|
253
|
+
{ name: "legacy", dir: "legacy", kind: "other", because: "x", outputs: ["QueueUrl"] },
|
|
254
|
+
]),
|
|
255
|
+
"chant.workspace.json",
|
|
256
|
+
);
|
|
257
|
+
const graph = {
|
|
258
|
+
composed: ["shared", "web"],
|
|
259
|
+
exports: [
|
|
260
|
+
{ member: "shared", name: "ClusterArn", node: "shared/Cluster" },
|
|
261
|
+
{ member: "shared", name: "ListenerArn", node: "shared/Listener" },
|
|
262
|
+
],
|
|
263
|
+
imports: [
|
|
264
|
+
{ member: "web", name: "clusterArn", node: "web/clusterArn" },
|
|
265
|
+
{ member: "web", name: "listenerArn", node: "web/listenerArn" },
|
|
266
|
+
{ member: "web", name: "queueUrl", node: "web/queueUrl" },
|
|
267
|
+
],
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
test("inferred joins carry their label and composed node ids, and a declared link suppresses its inferred edge", async () => {
|
|
271
|
+
const rows = graphLinks(decl, graph, builtinKindRegistry());
|
|
272
|
+
expect(rows).toEqual([
|
|
273
|
+
{ consumer: "web", producer: "shared", output: "ClusterArn", kind: "output", origin: "declared", label: "exact", input: null, resolves: "source", status: "resolved", reason: null, to: "shared/Cluster" },
|
|
274
|
+
{
|
|
275
|
+
consumer: "web",
|
|
276
|
+
producer: "shared",
|
|
277
|
+
output: "ListenerArn",
|
|
278
|
+
kind: "output",
|
|
279
|
+
origin: "inferred:joinKey",
|
|
280
|
+
label: "folded",
|
|
281
|
+
input: "listenerArn",
|
|
282
|
+
resolves: "source",
|
|
283
|
+
status: "resolved",
|
|
284
|
+
reason: null,
|
|
285
|
+
from: "web/listenerArn",
|
|
286
|
+
to: "shared/Listener",
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
consumer: "web",
|
|
290
|
+
producer: "legacy",
|
|
291
|
+
output: "QueueUrl",
|
|
292
|
+
kind: "output",
|
|
293
|
+
origin: "inferred:joinKey",
|
|
294
|
+
label: "folded",
|
|
295
|
+
input: "queueUrl",
|
|
296
|
+
resolves: "source",
|
|
297
|
+
status: "resolved",
|
|
298
|
+
reason: null,
|
|
299
|
+
from: "web/queueUrl",
|
|
300
|
+
},
|
|
301
|
+
]);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test("without kinds, only composed members are link targets", async () => {
|
|
305
|
+
expect(graphLinks(decl, graph).map((r) => r.status === "ambiguous" ? "" : r.producer)).toEqual(["shared", "shared"]);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("the linked member pairs, for the pipeline checks' environment rule", async () => {
|
|
309
|
+
expect(declaredMemberLinks(decl)).toEqual([{ consumer: "web", producer: "shared" }]);
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
describe("the declaration's link fields", () => {
|
|
314
|
+
const read = (members: unknown[]) => parseDeclaration(declaration(members), "chant.workspace.json");
|
|
315
|
+
|
|
316
|
+
test("links and outputs are read in file order, with pointers", async () => {
|
|
317
|
+
const d = read([
|
|
318
|
+
{ name: "a", dir: "a", kind: "other", because: "x", outputs: ["One", "Two"] },
|
|
319
|
+
{ name: "b", dir: "b", kind: "chant", links: [{ member: "a", output: "One" }, { member: "a", output: "Two", kind: "output" }] },
|
|
320
|
+
]);
|
|
321
|
+
expect(d.members[0].outputs).toEqual(["One", "Two"]);
|
|
322
|
+
expect(d.members[1].outputs).toBeNull();
|
|
323
|
+
expect(d.members[1].links).toEqual([
|
|
324
|
+
{ member: "a", output: "One", kind: null, pointer: "/members/1/links/0" },
|
|
325
|
+
{ member: "a", output: "Two", kind: "output", pointer: "/members/1/links/1" },
|
|
326
|
+
]);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test("a group has no links, and a link has only member, output, kind and x- fields", async () => {
|
|
330
|
+
expect(() => read([{ name: "ex", kind: "examples", glob: "examples/*", links: [] }])).toThrow(WorkspaceReadError);
|
|
331
|
+
expect(() => read([{ name: "b", dir: "b", kind: "chant", links: [{ member: "a", output: "One", hash: "x" }] }])).toThrow(/unknown field "hash"/);
|
|
332
|
+
expect(() => read([{ name: "b", dir: "b", kind: "chant", links: [{ member: "a" }] }])).toThrow(/missing required field "output"/);
|
|
333
|
+
expect(() => read([{ name: "b", dir: "b", kind: "chant", links: [{ member: "a", output: "" }] }])).toThrow(WorkspaceReadError);
|
|
334
|
+
expect(read([{ name: "b", dir: "b", kind: "chant", links: [{ member: "a", output: "One", "x-note": "hi" }] }]).members[0].links).toHaveLength(1);
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
describe("the chant repository's own declaration (#2557)", () => {
|
|
339
|
+
test("has no link finding", async () => {
|
|
340
|
+
const report = await runDeclarationChecks(resolve(import.meta.dirname, "../../../.."));
|
|
341
|
+
expect(report.diagnostics.filter((d) => /^WSP09\d$/.test(d.ruleId))).toEqual([]);
|
|
342
|
+
expect(report.links.filter((r) => r.status !== "resolved")).toEqual([]);
|
|
343
|
+
});
|
|
344
|
+
});
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Member links (#2524 D6, #2539; ws-008).
|
|
3
|
+
*
|
|
4
|
+
* A member link is stated once, on the consumer, as `{member, output}`, and it
|
|
5
|
+
* is matched exactly against the outputs the producer exposes. Which outputs
|
|
6
|
+
* a member exposes is up to its kind ({@link KindOutputs}): a `chant` member's
|
|
7
|
+
* are read from its source, a nested workspace exposes none, and `other` and
|
|
8
|
+
* kinds from a package expose what the kind and the entry list.
|
|
9
|
+
*
|
|
10
|
+
* A join nobody wrote down is inferred: a `chant` consumer's parameter is
|
|
11
|
+
* matched to other members' outputs with the core {@link joinKey}, and the
|
|
12
|
+
* join is labelled `exact` or `folded`. A parameter that matches two or more
|
|
13
|
+
* outputs is an ambiguity row, not an edge. A declared link from the same
|
|
14
|
+
* consumer to one of the matched outputs settles it: the inferred rows for
|
|
15
|
+
* that parameter give way to the declared one, which is how an inferred edge
|
|
16
|
+
* is promoted.
|
|
17
|
+
*
|
|
18
|
+
* Every row says how it was found (`declared` or `inferred:joinKey`), where it
|
|
19
|
+
* resolved (`source`; `check --live` comes later) and its status. A link that
|
|
20
|
+
* can't be resolved is kept, with the reason.
|
|
21
|
+
*
|
|
22
|
+
* {@link resolveLinks} takes the members' handles as plain data, so the same
|
|
23
|
+
* rules serve `chant workspace check`, which reads the handles from source
|
|
24
|
+
* (`sourceMemberHandles` in `./member-handles.ts`, which loads the TypeScript
|
|
25
|
+
* parser), and `chant workspace graph`, which reads
|
|
26
|
+
* them from each member's graph IR ({@link irMemberHandles}).
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { joinKey, joinLabel, type JoinLabel } from "../join-key";
|
|
30
|
+
import type { Declaration, LinkDeclaration, Member } from "./declaration";
|
|
31
|
+
import type { KindRegistry, MemberKind } from "./kinds";
|
|
32
|
+
|
|
33
|
+
/** The link kinds chant knows. Closed: a link of any other kind fails closed. */
|
|
34
|
+
export const LINK_KINDS = ["output"] as const;
|
|
35
|
+
export const DEFAULT_LINK_KIND = "output";
|
|
36
|
+
|
|
37
|
+
/** How a row was found: written down, or inferred by the named rule. */
|
|
38
|
+
export type LinkOrigin = "declared" | "inferred:joinKey";
|
|
39
|
+
|
|
40
|
+
/** One output or parameter of a member, as a join sees it. */
|
|
41
|
+
export interface JoinHandle {
|
|
42
|
+
name: string;
|
|
43
|
+
/** The node that holds it, when the reader knows one (a graph IR id). */
|
|
44
|
+
node?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** What a member offers links, and what it reads. */
|
|
48
|
+
export interface MemberHandles {
|
|
49
|
+
member: string;
|
|
50
|
+
outputs: JoinHandle[];
|
|
51
|
+
/** Parameters a join may feed. Only `chant` members have them. */
|
|
52
|
+
inputs: JoinHandle[];
|
|
53
|
+
/**
|
|
54
|
+
* Whether `outputs` is every output the member exposes. When it isn't, a
|
|
55
|
+
* link to a name not in the list is unresolved, not missing.
|
|
56
|
+
*/
|
|
57
|
+
complete: boolean;
|
|
58
|
+
/** Why the list is incomplete, or why the member exposes none. */
|
|
59
|
+
why: string | null;
|
|
60
|
+
/** True when the member's kind exposes no outputs at all. */
|
|
61
|
+
exposesNone: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface RowBase {
|
|
65
|
+
consumer: string;
|
|
66
|
+
kind: string;
|
|
67
|
+
origin: LinkOrigin;
|
|
68
|
+
resolves: "source";
|
|
69
|
+
/** Why the row is not `resolved`, or null. */
|
|
70
|
+
reason: string | null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** A declared link, or an inferred join with one producer. */
|
|
74
|
+
export interface LinkRow extends RowBase {
|
|
75
|
+
status: "resolved" | "missing" | "unresolved" | "invalid";
|
|
76
|
+
producer: string;
|
|
77
|
+
output: string;
|
|
78
|
+
/** `exact` for every declared link; `exact` or `folded` for an inferred one. */
|
|
79
|
+
label: JoinLabel;
|
|
80
|
+
/** The consumer's parameter an inferred join feeds; null for a declared link. */
|
|
81
|
+
input: string | null;
|
|
82
|
+
/** The consumer's parameter node and the producer's output node, when known. */
|
|
83
|
+
from?: string;
|
|
84
|
+
to?: string;
|
|
85
|
+
/** The declared link's JSON Pointer in the declaration. */
|
|
86
|
+
pointer?: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** A parameter that joins outputs of two or more producers (or two outputs of one). */
|
|
90
|
+
export interface AmbiguousRow extends RowBase {
|
|
91
|
+
status: "ambiguous";
|
|
92
|
+
input: string;
|
|
93
|
+
from?: string;
|
|
94
|
+
candidates: { producer: string; output: string; label: JoinLabel; to?: string }[];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export type LinkTableRow = LinkRow | AmbiguousRow;
|
|
98
|
+
|
|
99
|
+
/** A link between two members, in the shape the pipeline checks read (#2542). */
|
|
100
|
+
export interface MemberLinkPair {
|
|
101
|
+
consumer: string;
|
|
102
|
+
producer: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The member pairs the declaration links, consumer first, once each, in
|
|
107
|
+
* declaration order. Only links to a declared member other than the consumer
|
|
108
|
+
* count.
|
|
109
|
+
*/
|
|
110
|
+
export function declaredMemberLinks(declaration: Declaration): MemberLinkPair[] {
|
|
111
|
+
const names = new Set(declaration.members.map((m) => m.name));
|
|
112
|
+
const seen = new Set<string>();
|
|
113
|
+
const out: MemberLinkPair[] = [];
|
|
114
|
+
for (const m of declaration.members) {
|
|
115
|
+
for (const l of m.links) {
|
|
116
|
+
const key = `${m.name}\0${l.member}`;
|
|
117
|
+
if (l.member === m.name || !names.has(l.member) || seen.has(key)) continue;
|
|
118
|
+
seen.add(key);
|
|
119
|
+
out.push({ consumer: m.name, producer: l.member });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0);
|
|
126
|
+
|
|
127
|
+
/** Why a declared link's target is not a member it may name, or undefined. */
|
|
128
|
+
export function linkTargetProblem(declaration: Declaration, consumer: Member, link: LinkDeclaration): string | undefined {
|
|
129
|
+
if (link.member === consumer.name) return `member ${consumer.name} links to itself; a link joins two members`;
|
|
130
|
+
const entry = declaration.entries.find((e) => e.name === link.member);
|
|
131
|
+
if (!entry) return `member ${consumer.name} links to ${link.member}, which is not a member of this workspace`;
|
|
132
|
+
if (entry.type === "group") return `member ${consumer.name} links to ${link.member}, which is an example group; a group has no links`;
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Output names that fold to the same key as `name`, for a "did you mean" hint. */
|
|
137
|
+
function nearNames(name: string, outputs: JoinHandle[]): string[] {
|
|
138
|
+
const key = joinKey(name);
|
|
139
|
+
return [...new Set(outputs.filter((o) => o.name !== name && joinKey(o.name) === key).map((o) => o.name))].sort(cmp);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Resolve every declared link and infer the joins nobody wrote down.
|
|
144
|
+
* `handles` holds one entry per member whose handles were read; a member
|
|
145
|
+
* missing from it is unresolved. Declared rows come first, in declaration
|
|
146
|
+
* order; inferred and ambiguous rows follow, sorted by consumer and parameter.
|
|
147
|
+
*/
|
|
148
|
+
export function resolveLinks(declaration: Declaration, handles: ReadonlyMap<string, MemberHandles>): LinkTableRow[] {
|
|
149
|
+
const rows: LinkTableRow[] = [];
|
|
150
|
+
const declaredTargets = new Map<string, Set<string>>();
|
|
151
|
+
|
|
152
|
+
for (const consumer of declaration.members) {
|
|
153
|
+
for (const link of consumer.links) {
|
|
154
|
+
const kind = link.kind ?? DEFAULT_LINK_KIND;
|
|
155
|
+
const row: LinkRow = {
|
|
156
|
+
consumer: consumer.name,
|
|
157
|
+
producer: link.member,
|
|
158
|
+
output: link.output,
|
|
159
|
+
kind,
|
|
160
|
+
origin: "declared",
|
|
161
|
+
label: "exact",
|
|
162
|
+
input: null,
|
|
163
|
+
resolves: "source",
|
|
164
|
+
status: "resolved",
|
|
165
|
+
reason: null,
|
|
166
|
+
pointer: link.pointer,
|
|
167
|
+
};
|
|
168
|
+
rows.push(row);
|
|
169
|
+
const targetProblem = linkTargetProblem(declaration, consumer, link);
|
|
170
|
+
if (targetProblem) {
|
|
171
|
+
row.status = "invalid";
|
|
172
|
+
row.reason = targetProblem;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
(declaredTargets.get(consumer.name) ?? declaredTargets.set(consumer.name, new Set()).get(consumer.name)!).add(`${link.member}\0${link.output}`);
|
|
176
|
+
if (!(LINK_KINDS as readonly string[]).includes(kind)) {
|
|
177
|
+
row.status = "invalid";
|
|
178
|
+
row.reason = `the link kind ${kind} is not one chant knows; known link kinds: ${LINK_KINDS.join(", ")}`;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const producer = handles.get(link.member);
|
|
182
|
+
const found = producer?.outputs.find((o) => o.name === link.output);
|
|
183
|
+
if (found) {
|
|
184
|
+
if (found.node) row.to = found.node;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (!producer) {
|
|
188
|
+
row.status = "unresolved";
|
|
189
|
+
row.reason = `the outputs of ${link.member} were not read`;
|
|
190
|
+
} else if (producer.exposesNone) {
|
|
191
|
+
row.status = "missing";
|
|
192
|
+
row.reason = producer.why ?? `${link.member} exposes no outputs`;
|
|
193
|
+
} else if (!producer.complete) {
|
|
194
|
+
row.status = "unresolved";
|
|
195
|
+
row.reason = `${link.output} was not found in ${link.member}, whose outputs could not all be read in source${producer.why ? ` (${producer.why})` : ""}`;
|
|
196
|
+
} else {
|
|
197
|
+
const near = nearNames(link.output, producer.outputs);
|
|
198
|
+
const known = producer.outputs.map((o) => o.name);
|
|
199
|
+
row.status = "missing";
|
|
200
|
+
row.reason =
|
|
201
|
+
`${link.member} has no output ${link.output}` +
|
|
202
|
+
(near.length ? `; did you mean ${near.join(" or ")}? Links match exactly` : "") +
|
|
203
|
+
(known.length ? `; its outputs: ${[...new Set(known)].sort(cmp).join(", ")}` : "; it exposes no outputs");
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Inferred joins: a consumer's parameter against every other member's outputs.
|
|
209
|
+
const inferred: LinkTableRow[] = [];
|
|
210
|
+
for (const consumer of declaration.members) {
|
|
211
|
+
const mine = handles.get(consumer.name);
|
|
212
|
+
if (!mine) continue;
|
|
213
|
+
const declared = declaredTargets.get(consumer.name) ?? new Set<string>();
|
|
214
|
+
for (const input of mine.inputs) {
|
|
215
|
+
const candidates: { producer: string; output: string; label: JoinLabel; to?: string }[] = [];
|
|
216
|
+
for (const producer of declaration.members) {
|
|
217
|
+
if (producer.name === consumer.name) continue;
|
|
218
|
+
for (const o of handles.get(producer.name)?.outputs ?? []) {
|
|
219
|
+
const label = joinLabel(input.name, o.name);
|
|
220
|
+
if (!label) continue;
|
|
221
|
+
if (candidates.some((c) => c.producer === producer.name && c.output === o.name)) continue;
|
|
222
|
+
candidates.push({ producer: producer.name, output: o.name, label, ...(o.node ? { to: o.node } : {}) });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (candidates.length === 0) continue;
|
|
226
|
+
// A declared link to one of the matches settles the parameter.
|
|
227
|
+
if (candidates.some((c) => declared.has(`${c.producer}\0${c.output}`))) continue;
|
|
228
|
+
const base = { consumer: consumer.name, kind: DEFAULT_LINK_KIND, origin: "inferred:joinKey" as const, resolves: "source" as const };
|
|
229
|
+
if (candidates.length === 1) {
|
|
230
|
+
const c = candidates[0];
|
|
231
|
+
inferred.push({
|
|
232
|
+
...base,
|
|
233
|
+
status: "resolved",
|
|
234
|
+
reason: null,
|
|
235
|
+
producer: c.producer,
|
|
236
|
+
output: c.output,
|
|
237
|
+
label: c.label,
|
|
238
|
+
input: input.name,
|
|
239
|
+
...(input.node ? { from: input.node } : {}),
|
|
240
|
+
...(c.to ? { to: c.to } : {}),
|
|
241
|
+
});
|
|
242
|
+
} else {
|
|
243
|
+
candidates.sort((a, b) => cmp(a.producer, b.producer) || cmp(a.output, b.output));
|
|
244
|
+
const producers = new Set(candidates.map((c) => c.producer));
|
|
245
|
+
inferred.push({
|
|
246
|
+
...base,
|
|
247
|
+
status: "ambiguous",
|
|
248
|
+
reason:
|
|
249
|
+
producers.size > 1
|
|
250
|
+
? `parameter ${input.name} of ${consumer.name} matches outputs of ${[...producers].join(" and ")}`
|
|
251
|
+
: `parameter ${input.name} of ${consumer.name} matches ${candidates.map((c) => c.output).join(" and ")} of ${candidates[0].producer}`,
|
|
252
|
+
input: input.name,
|
|
253
|
+
...(input.node ? { from: input.node } : {}),
|
|
254
|
+
candidates,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
inferred.sort((a, b) => cmp(a.consumer, b.consumer) || cmp(a.input ?? "", b.input ?? ""));
|
|
260
|
+
return [...rows, ...inferred];
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ── Where the handles come from ──────────────────────────────────────────────
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* The handles of a member whose kind lists its outputs, or exposes none.
|
|
267
|
+
* Undefined for a kind whose outputs are read from the member (`chant`).
|
|
268
|
+
*/
|
|
269
|
+
export function kindHandles(m: Member, kind: MemberKind): MemberHandles | undefined {
|
|
270
|
+
const outputs = kind.outputs;
|
|
271
|
+
if (outputs.from === "none") {
|
|
272
|
+
return { member: m.name, outputs: [], inputs: [], complete: true, why: `${m.name} is kind ${m.kind}, which exposes no outputs as link targets`, exposesNone: true };
|
|
273
|
+
}
|
|
274
|
+
if (outputs.from === "declared") {
|
|
275
|
+
const names = [...new Set([...outputs.names, ...(m.outputs ?? [])])];
|
|
276
|
+
return { member: m.name, outputs: names.map((name) => ({ name })), inputs: [], complete: true, why: null, exposesNone: false };
|
|
277
|
+
}
|
|
278
|
+
return undefined;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** The part of a composed workspace graph (#2537) the links read. */
|
|
282
|
+
export interface ComposedHandles {
|
|
283
|
+
/** Members whose IR was composed. */
|
|
284
|
+
composed: readonly string[];
|
|
285
|
+
exports: readonly { member: string; name: string; node?: string }[];
|
|
286
|
+
imports: readonly { member: string; name: string; node: string }[];
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Every composed member's handles, from the `exports` and `imports` of the
|
|
291
|
+
* composed workspace graph, for its `links` section. Node ids are the
|
|
292
|
+
* composed ones (`<member>/<id>`). A member's IR lists all its outputs, so
|
|
293
|
+
* its list is complete.
|
|
294
|
+
*/
|
|
295
|
+
export function irMemberHandles(graph: ComposedHandles): Map<string, MemberHandles> {
|
|
296
|
+
const out = new Map<string, MemberHandles>();
|
|
297
|
+
for (const member of graph.composed) {
|
|
298
|
+
out.set(member, {
|
|
299
|
+
member,
|
|
300
|
+
outputs: graph.exports.filter((e) => e.member === member).map((e) => ({ name: e.name, ...(e.node ? { node: e.node } : {}) })),
|
|
301
|
+
inputs: graph.imports.filter((i) => i.member === member).map((i) => ({ name: i.name, node: i.node })),
|
|
302
|
+
complete: true,
|
|
303
|
+
why: null,
|
|
304
|
+
exposesNone: false,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
return out;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* The `links` section of the composed workspace graph (#2537, #2524 D8):
|
|
312
|
+
* the declared links and inferred joins, with composed node ids on the ends
|
|
313
|
+
* that have one. Declared links suppress the inferred edges they cover.
|
|
314
|
+
* With `kinds`, a member that isn't composed but whose kind lists its outputs
|
|
315
|
+
* (`other`, kinds from a package) is a link target too.
|
|
316
|
+
*/
|
|
317
|
+
export function graphLinks(declaration: Declaration, graph: ComposedHandles, kinds?: KindRegistry): LinkTableRow[] {
|
|
318
|
+
const handles = irMemberHandles(graph);
|
|
319
|
+
for (const m of declaration.members) {
|
|
320
|
+
const kind = kinds?.get(m.kind);
|
|
321
|
+
if (handles.has(m.name) || !kind) continue;
|
|
322
|
+
const listed = kindHandles(m, kind);
|
|
323
|
+
if (listed) handles.set(m.name, listed);
|
|
324
|
+
}
|
|
325
|
+
return resolveLinks(declaration, handles).map((row) => {
|
|
326
|
+
if (row.status === "ambiguous" || row.origin !== "declared") return row;
|
|
327
|
+
// The declaration's pointer means nothing to a graph reader.
|
|
328
|
+
const { pointer: _pointer, ...rest } = row;
|
|
329
|
+
return rest;
|
|
330
|
+
});
|
|
331
|
+
}
|