@intentius/chant 0.124.46 → 0.124.49
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/__golden__/wave-loop/op-by-apply-after-fails.json +10 -0
- package/dist/__golden__/wave-loop/op-by-apply-hold.json +10 -0
- package/dist/__golden__/wave-loop/op-by-apply-ignored.json +10 -0
- package/dist/__golden__/wave-loop/op-by-apply-pass.json +10 -0
- package/dist/__golden__/wave-loop/op-by-apply-skip.json +10 -0
- package/dist/__golden__/wave-loop/op-cost-member-clash.json +6 -0
- package/dist/__golden__/wave-loop/op-cost-member.json +13 -0
- package/dist/__golden__/wave-loop/op-denied-source.json +16 -0
- package/dist/__golden__/wave-loop/op-denied.json +16 -0
- package/dist/__golden__/wave-loop/op-discover-rest-short.json +6 -0
- package/dist/__golden__/wave-loop/op-discover-rest-stops.json +13 -0
- package/dist/__golden__/wave-loop/op-discover-rest.json +26 -0
- package/dist/__golden__/wave-loop/op-discover-wide.json +26 -0
- package/dist/__golden__/wave-loop/op-held-on-again.json +16 -0
- package/dist/__golden__/wave-loop/op-held-on-before-apply.json +16 -0
- package/dist/__golden__/wave-loop/op-held-on.json +16 -0
- package/dist/__golden__/wave-loop/op-hooks-apply-fails.json +13 -0
- package/dist/__golden__/wave-loop/op-hooks-fail-after-apply.json +13 -0
- package/dist/__golden__/wave-loop/op-hooks-fail-before-apply.json +13 -0
- package/dist/__golden__/wave-loop/op-hooks-fail-before-plan.json +13 -0
- package/dist/__golden__/wave-loop/op-hooks-held.json +13 -0
- package/dist/__golden__/wave-loop/op-hooks-hold-before-apply.json +13 -0
- package/dist/__golden__/wave-loop/op-hooks-pass.json +13 -0
- package/dist/__golden__/wave-loop/op-hooks-refused-at-base.json +13 -0
- package/dist/__golden__/wave-loop/op-hooks-share.json +33 -0
- package/dist/__golden__/wave-loop/op-outcome-held.json +10 -0
- package/dist/__golden__/wave-loop/op-outcome-replan-approved.json +10 -0
- package/dist/__golden__/wave-loop/op-outcome-replan-forever.json +10 -0
- package/dist/__golden__/wave-loop/op-outcome-replan.json +10 -0
- package/dist/__golden__/wave-loop/op-outcome-stood-down.json +10 -0
- package/dist/__golden__/wave-loop/op-outcome-unreadable.json +10 -0
- package/dist/__golden__/wave-loop/op-overridden.json +16 -0
- package/dist/__golden__/wave-loop/op-per-run.json +20 -0
- package/dist/__golden__/wave-loop/op-point.json +13 -0
- package/dist/__golden__/wave-loop/op-pr-review.json +10 -0
- package/dist/__golden__/wave-loop/op-sealed.json +10 -0
- package/dist/__golden__/wave-loop/op-spend-change-only.json +10 -0
- package/dist/__golden__/wave-loop/op-spend.json +10 -0
- package/dist/__golden__/wave-loop/op-wave-commands.json +55 -0
- package/dist/__golden__/wave-loop/op-wide-refused.json +26 -0
- package/dist/__golden__/wave-loop/op-wide-waiting.json +29 -0
- package/dist/__golden__/wave-loop/op-wide.json +39 -0
- package/dist/action-pins.d.ts +61 -0
- package/dist/action-pins.d.ts.map +1 -0
- package/dist/action-pins.js +77 -0
- package/dist/cli/commands/carve.d.ts +3 -0
- package/dist/cli/commands/carve.d.ts.map +1 -1
- package/dist/cli/commands/carve.js +36 -7
- package/dist/cli/commands/import-report.d.ts +26 -0
- package/dist/cli/commands/import-report.d.ts.map +1 -0
- package/dist/cli/commands/import-report.js +65 -0
- package/dist/cli/commands/import.d.ts +11 -1
- package/dist/cli/commands/import.d.ts.map +1 -1
- package/dist/cli/commands/import.js +22 -3
- package/dist/cli/commands/migrate.d.ts +13 -1
- package/dist/cli/commands/migrate.d.ts.map +1 -1
- package/dist/cli/commands/migrate.js +134 -10
- package/dist/cli/handlers/approve.d.ts.map +1 -1
- package/dist/cli/handlers/approve.js +41 -14
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.js +6 -1
- package/dist/cli/handlers/graph-reader.d.ts +26 -0
- package/dist/cli/handlers/graph-reader.d.ts.map +1 -0
- package/dist/cli/handlers/graph-reader.js +41 -0
- package/dist/cli/handlers/graph.d.ts.map +1 -1
- package/dist/cli/handlers/graph.js +14 -0
- package/dist/cli/handlers/ledger.d.ts +3 -0
- package/dist/cli/handlers/ledger.d.ts.map +1 -0
- package/dist/cli/handlers/ledger.js +55 -0
- package/dist/cli/handlers/migrate.d.ts.map +1 -1
- package/dist/cli/handlers/migrate.js +18 -11
- package/dist/cli/handlers/misc.d.ts.map +1 -1
- package/dist/cli/handlers/misc.js +12 -1
- package/dist/cli/handlers/pr-env.js +2 -2
- package/dist/cli/handlers/pr.d.ts +15 -1
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/pr.js +183 -40
- package/dist/cli/handlers/query.d.ts +7 -2
- package/dist/cli/handlers/query.d.ts.map +1 -1
- package/dist/cli/handlers/query.js +16 -4
- package/dist/cli/handlers/release.d.ts.map +1 -1
- package/dist/cli/handlers/release.js +6 -3
- package/dist/cli/handlers/search.d.ts +2 -4
- package/dist/cli/handlers/search.d.ts.map +1 -1
- package/dist/cli/handlers/search.js +13 -39
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +87 -14
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +47 -2
- package/dist/cli/registry.d.ts +22 -4
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/builders.d.ts +5 -1
- package/dist/components/builders.d.ts.map +1 -1
- package/dist/components/builders.js +5 -0
- package/dist/components/capability-plugin-loader.d.ts.map +1 -1
- package/dist/components/capability-plugin-loader.js +16 -1
- package/dist/components/capability-plugin.d.ts +14 -0
- package/dist/components/capability-plugin.d.ts.map +1 -1
- package/dist/components/pr-hooks.d.ts +21 -1
- package/dist/components/pr-hooks.d.ts.map +1 -1
- package/dist/components/pr-hooks.js +72 -4
- package/dist/components/pr-pipeline.d.ts +45 -10
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/components/pr-pipeline.js +47 -14
- package/dist/components/pr-policy.d.ts +36 -0
- package/dist/components/pr-policy.d.ts.map +1 -1
- package/dist/components/pr-policy.js +36 -0
- package/dist/components/pr-run.d.ts +3 -0
- package/dist/components/pr-run.d.ts.map +1 -1
- package/dist/components/pr-run.js +1 -0
- package/dist/components/starter-plugin.d.ts +1 -0
- package/dist/components/starter-plugin.d.ts.map +1 -1
- package/dist/components/starter-plugin.js +5 -1
- package/dist/components/verbs/index.d.ts +2 -0
- package/dist/components/verbs/index.d.ts.map +1 -1
- package/dist/components/verbs/index.js +2 -0
- package/dist/components/verbs/publish-artifact.d.ts +65 -0
- package/dist/components/verbs/publish-artifact.d.ts.map +1 -0
- package/dist/components/verbs/publish-artifact.js +74 -0
- package/dist/components/verbs/publish-image.d.ts +129 -0
- package/dist/components/verbs/publish-image.d.ts.map +1 -0
- package/dist/components/verbs/publish-image.js +145 -0
- package/dist/config-environments.d.ts +20 -0
- package/dist/config-environments.d.ts.map +1 -1
- package/dist/config-environments.js +12 -0
- package/dist/config.d.ts +25 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +10 -2
- package/dist/gate-decision.d.ts +20 -2
- package/dist/gate-decision.d.ts.map +1 -1
- package/dist/gate-decision.js +30 -3
- package/dist/graph-ir.d.ts +25 -0
- package/dist/graph-ir.d.ts.map +1 -1
- package/dist/graph-walk.d.ts +66 -0
- package/dist/graph-walk.d.ts.map +1 -0
- package/dist/graph-walk.js +70 -0
- package/dist/import/parser.d.ts +19 -0
- package/dist/import/parser.d.ts.map +1 -1
- package/dist/lexicon.d.ts +94 -6
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.d.ts +11 -0
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/lifecycle/git.d.ts +2 -5
- package/dist/lifecycle/git.d.ts.map +1 -1
- package/dist/lifecycle/ledger-read.d.ts +65 -0
- package/dist/lifecycle/ledger-read.d.ts.map +1 -0
- package/dist/lifecycle/ledger-read.js +160 -0
- package/dist/lifecycle/release-workstation.d.ts +7 -0
- package/dist/lifecycle/release-workstation.d.ts.map +1 -1
- package/dist/lifecycle/release-workstation.js +25 -0
- package/dist/lifecycle/run-ledger.d.ts +4 -3
- package/dist/lifecycle/run-ledger.d.ts.map +1 -1
- package/dist/lifecycle/run-ledger.js +1 -0
- package/dist/lint/discover.js +3 -1
- package/dist/lint/post-synth.d.ts +127 -1
- package/dist/lint/post-synth.d.ts.map +1 -1
- package/dist/lint/post-synth.js +84 -2
- package/dist/lint/rules/op/index.d.ts +1 -0
- package/dist/lint/rules/op/index.d.ts.map +1 -1
- package/dist/lint/rules/op/index.js +2 -0
- package/dist/lint/rules/op/ops019-workspace-op-target.d.ts +32 -0
- package/dist/lint/rules/op/ops019-workspace-op-target.d.ts.map +1 -0
- package/dist/lint/rules/op/ops019-workspace-op-target.js +63 -0
- package/dist/op/activities/activity-contracts.d.ts +9 -6
- package/dist/op/activities/activity-contracts.d.ts.map +1 -1
- package/dist/op/activities/activity-contracts.js +5 -2
- package/dist/op/activities/index.d.ts +2 -2
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/index.js +1 -1
- package/dist/op/activities/review-agent.d.ts +90 -4
- package/dist/op/activities/review-agent.d.ts.map +1 -1
- package/dist/op/activities/review-agent.js +217 -19
- package/dist/op/activities/work-evidence.d.ts +8 -1
- package/dist/op/activities/work-evidence.d.ts.map +1 -1
- package/dist/op/activities/work-evidence.js +1 -0
- package/dist/op/builders.d.ts +9 -5
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/builders.js +9 -5
- package/dist/op/composites/apply-op.d.ts +29 -2
- package/dist/op/composites/apply-op.d.ts.map +1 -1
- package/dist/op/composites/apply-op.js +20 -1
- package/dist/op/gate-approval-base.d.ts +25 -0
- package/dist/op/gate-approval-base.d.ts.map +1 -0
- package/dist/op/gate-approval-base.js +71 -0
- package/dist/op/gate-approval.d.ts +18 -9
- package/dist/op/gate-approval.d.ts.map +1 -1
- package/dist/op/gate-approval.js +13 -31
- package/dist/op/gate-name.d.ts +13 -0
- package/dist/op/gate-name.d.ts.map +1 -1
- package/dist/op/gate-name.js +14 -1
- package/dist/op/gate-quorum.d.ts +100 -3
- package/dist/op/gate-quorum.d.ts.map +1 -1
- package/dist/op/gate-quorum.js +108 -6
- package/dist/op/gate-review.d.ts +9 -0
- package/dist/op/gate-review.d.ts.map +1 -1
- package/dist/op/gate-review.js +13 -0
- package/dist/op/gate.d.ts +47 -5
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/gate.js +25 -4
- package/dist/op/index.d.ts +3 -3
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/index.js +1 -1
- package/dist/op/local-executor.d.ts +45 -3
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-executor.js +68 -7
- package/dist/op/op-waves-run.d.ts +26 -1
- package/dist/op/op-waves-run.d.ts.map +1 -1
- package/dist/op/op-waves-run.js +259 -13
- package/dist/op/op-waves.d.ts +72 -5
- package/dist/op/op-waves.d.ts.map +1 -1
- package/dist/op/op-waves.js +110 -15
- package/dist/op/runtime.d.ts +11 -0
- package/dist/op/runtime.d.ts.map +1 -1
- package/dist/op/schedule-overdue.d.ts +16 -0
- package/dist/op/schedule-overdue.d.ts.map +1 -1
- package/dist/op/schedule-overdue.js +36 -0
- package/dist/op/stage-hooks.d.ts +9 -2
- package/dist/op/stage-hooks.d.ts.map +1 -1
- package/dist/op/stage-hooks.js +7 -5
- package/dist/op/workspace-run-env.d.ts +22 -0
- package/dist/op/workspace-run-env.d.ts.map +1 -0
- package/dist/op/workspace-run-env.js +21 -0
- package/dist/pr-forge.d.ts +24 -0
- package/dist/pr-forge.d.ts.map +1 -1
- package/dist/pr-forge.js +47 -0
- package/dist/pr-loop.d.ts +10 -0
- package/dist/pr-loop.d.ts.map +1 -1
- package/dist/pr-loop.js +4 -0
- package/dist/pr-migrations.d.ts +62 -0
- package/dist/pr-migrations.d.ts.map +1 -0
- package/dist/pr-migrations.js +50 -0
- package/dist/pr-post.d.ts +42 -0
- package/dist/pr-post.d.ts.map +1 -1
- package/dist/pr-post.js +68 -2
- package/dist/read-filter.d.ts +65 -0
- package/dist/read-filter.d.ts.map +1 -0
- package/dist/read-filter.js +179 -0
- package/dist/run-event.d.ts +1 -1
- package/dist/run-event.d.ts.map +1 -1
- package/dist/terraform/carve.d.ts +24 -3
- package/dist/terraform/carve.d.ts.map +1 -1
- package/dist/terraform/carve.js +32 -14
- package/dist/terraform/cuts.d.ts +97 -0
- package/dist/terraform/cuts.d.ts.map +1 -0
- package/dist/terraform/cuts.js +138 -0
- package/dist/terraform/folds.d.ts +26 -0
- package/dist/terraform/folds.d.ts.map +1 -0
- package/dist/terraform/folds.js +38 -0
- package/dist/terraform/graph.d.ts.map +1 -1
- package/dist/terraform/graph.js +10 -0
- package/dist/terraform/reach.d.ts +140 -0
- package/dist/terraform/reach.d.ts.map +1 -0
- package/dist/terraform/reach.js +170 -0
- package/dist/terraform/score.d.ts +14 -0
- package/dist/terraform/score.d.ts.map +1 -1
- package/dist/terraform/score.js +14 -31
- package/dist/terraform/state.d.ts.map +1 -1
- package/dist/terraform/state.js +4 -2
- package/dist/terraform/types.d.ts +8 -0
- package/dist/terraform/types.d.ts.map +1 -1
- package/dist/wave-blocked.d.ts +10 -1
- package/dist/wave-blocked.d.ts.map +1 -1
- package/dist/wave-blocked.js +31 -3
- package/dist/wave-runtime.mjs +264 -254
- package/dist/workspace/check.schema.json +13 -2
- package/dist/workspace/checks/ci-branch.d.ts +44 -0
- package/dist/workspace/checks/ci-branch.d.ts.map +1 -0
- package/dist/workspace/checks/ci-branch.js +133 -0
- package/dist/workspace/checks/gate-approval.d.ts +58 -0
- package/dist/workspace/checks/gate-approval.d.ts.map +1 -0
- package/dist/workspace/checks/gate-approval.js +103 -0
- package/dist/workspace/checks/links.d.ts +3 -0
- package/dist/workspace/checks/links.d.ts.map +1 -1
- package/dist/workspace/checks/links.js +12 -1
- package/dist/workspace/checks/org.d.ts +48 -0
- package/dist/workspace/checks/org.d.ts.map +1 -0
- package/dist/workspace/checks/org.js +156 -0
- package/dist/workspace/checks.d.ts +12 -1
- package/dist/workspace/checks.d.ts.map +1 -1
- package/dist/workspace/checks.js +63 -7
- package/dist/workspace/ci-branches.d.ts +16 -0
- package/dist/workspace/ci-branches.d.ts.map +1 -1
- package/dist/workspace/ci-branches.js +27 -0
- package/dist/workspace/ci-green-cli.js +7 -7
- package/dist/workspace/ci-green-workflow.d.ts +1 -1
- package/dist/workspace/ci-green-workflow.d.ts.map +1 -1
- package/dist/workspace/ci-green-workflow.js +13 -8
- package/dist/workspace/ci-green.d.ts +14 -0
- package/dist/workspace/ci-green.d.ts.map +1 -1
- package/dist/workspace/ci-green.js +44 -11
- package/dist/workspace/ci-run.d.ts +9 -1
- package/dist/workspace/ci-run.d.ts.map +1 -1
- package/dist/workspace/ci-run.js +67 -6
- package/dist/workspace/ci-serve.d.ts +9 -0
- package/dist/workspace/ci-serve.d.ts.map +1 -1
- package/dist/workspace/ci-serve.js +114 -76
- package/dist/workspace/compose-graph.d.ts +14 -0
- package/dist/workspace/compose-graph.d.ts.map +1 -1
- package/dist/workspace/composites.d.ts +4 -1
- package/dist/workspace/composites.d.ts.map +1 -1
- package/dist/workspace/composites.js +5 -2
- package/dist/workspace/composites.schema.json +22 -0
- package/dist/workspace/conformance/__writer_fixture__/work/work.schema.json +105 -1
- package/dist/workspace/cross-root-lint.d.ts +56 -0
- package/dist/workspace/cross-root-lint.d.ts.map +1 -0
- package/dist/workspace/cross-root-lint.js +111 -0
- package/dist/workspace/dbt-models-reader.d.ts +44 -0
- package/dist/workspace/dbt-models-reader.d.ts.map +1 -0
- package/dist/workspace/dbt-models-reader.js +356 -0
- package/dist/workspace/dbt-project-common.d.ts +40 -0
- package/dist/workspace/dbt-project-common.d.ts.map +1 -0
- package/dist/workspace/dbt-project-common.js +106 -0
- package/dist/workspace/dbt-project-reader.d.ts +4 -3
- package/dist/workspace/dbt-project-reader.d.ts.map +1 -1
- package/dist/workspace/dbt-project-reader.js +14 -108
- package/dist/workspace/decision-rank.d.ts +26 -0
- package/dist/workspace/decision-rank.d.ts.map +1 -0
- package/dist/workspace/decision-rank.js +16 -0
- package/dist/workspace/declaration.d.ts +50 -2
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.js +57 -11
- package/dist/workspace/declaration.schema.json +166 -24
- package/dist/workspace/environments.d.ts +26 -2
- package/dist/workspace/environments.d.ts.map +1 -1
- package/dist/workspace/environments.js +26 -2
- package/dist/workspace/exit-cli.d.ts +44 -0
- package/dist/workspace/exit-cli.d.ts.map +1 -0
- package/dist/workspace/exit-cli.js +198 -0
- package/dist/workspace/exit.d.ts +159 -0
- package/dist/workspace/exit.d.ts.map +1 -0
- package/dist/workspace/exit.js +480 -0
- package/dist/workspace/exit.schema.json +257 -0
- package/dist/workspace/flyway-dir.d.ts +39 -0
- package/dist/workspace/flyway-dir.d.ts.map +1 -0
- package/dist/workspace/flyway-dir.js +104 -0
- package/dist/workspace/flyway-layout.d.ts +96 -0
- package/dist/workspace/flyway-layout.d.ts.map +1 -0
- package/dist/workspace/flyway-layout.js +308 -0
- package/dist/workspace/graph-cache.d.ts +3 -1
- package/dist/workspace/graph-cache.d.ts.map +1 -1
- package/dist/workspace/graph-cache.js +12 -1
- package/dist/workspace/graph-cli.d.ts.map +1 -1
- package/dist/workspace/graph-cli.js +26 -8
- package/dist/workspace/graph.schema.json +34 -0
- package/dist/workspace/import-fetch.d.ts +38 -0
- package/dist/workspace/import-fetch.d.ts.map +1 -0
- package/dist/workspace/import-fetch.js +99 -0
- package/dist/workspace/import-init.d.ts +26 -0
- package/dist/workspace/import-init.d.ts.map +1 -0
- package/dist/workspace/import-init.js +130 -0
- package/dist/workspace/import-source.d.ts +147 -0
- package/dist/workspace/import-source.d.ts.map +1 -0
- package/dist/workspace/import-source.js +370 -0
- package/dist/workspace/init.d.ts +4 -0
- package/dist/workspace/init.d.ts.map +1 -1
- package/dist/workspace/init.js +70 -3
- package/dist/workspace/intent.d.ts +2 -3
- package/dist/workspace/intent.d.ts.map +1 -1
- package/dist/workspace/intent.js +5 -9
- package/dist/workspace/kind-link-readers.d.ts +59 -2
- package/dist/workspace/kind-link-readers.d.ts.map +1 -1
- package/dist/workspace/kind-link-readers.js +17 -1
- package/dist/workspace/kind-readers.d.ts +6 -0
- package/dist/workspace/kind-readers.d.ts.map +1 -1
- package/dist/workspace/kind-readers.js +18 -0
- package/dist/workspace/kinds.d.ts +90 -3
- package/dist/workspace/kinds.d.ts.map +1 -1
- package/dist/workspace/kinds.js +161 -7
- package/dist/workspace/ledger.schema.json +158 -0
- package/dist/workspace/lexicon-outputs.d.ts +20 -1
- package/dist/workspace/lexicon-outputs.d.ts.map +1 -1
- package/dist/workspace/lexicon-outputs.js +102 -10
- package/dist/workspace/lineage-check.d.ts +3 -0
- package/dist/workspace/lineage-check.d.ts.map +1 -1
- package/dist/workspace/lineage-check.js +23 -4
- package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
- package/dist/workspace/lineage-upgrade.js +13 -4
- package/dist/workspace/links.d.ts +8 -1
- package/dist/workspace/links.d.ts.map +1 -1
- package/dist/workspace/links.js +25 -1
- package/dist/workspace/ls.d.ts +8 -0
- package/dist/workspace/ls.d.ts.map +1 -1
- package/dist/workspace/ls.js +7 -1
- package/dist/workspace/ls.schema.json +7 -0
- package/dist/workspace/member-commands.d.ts.map +1 -1
- package/dist/workspace/member-commands.js +54 -5
- package/dist/workspace/migrate-links.d.ts +45 -0
- package/dist/workspace/migrate-links.d.ts.map +1 -0
- package/dist/workspace/migrate-links.js +100 -0
- package/dist/workspace/migration-dirs.d.ts +74 -0
- package/dist/workspace/migration-dirs.d.ts.map +1 -0
- package/dist/workspace/migration-dirs.js +254 -0
- package/dist/workspace/new-migrations.d.ts +47 -0
- package/dist/workspace/new-migrations.d.ts.map +1 -0
- package/dist/workspace/new-migrations.js +91 -0
- package/dist/workspace/pr-report.schema.json +46 -0
- package/dist/workspace/promote.d.ts +96 -0
- package/dist/workspace/promote.d.ts.map +1 -0
- package/dist/workspace/promote.js +263 -0
- package/dist/workspace/reason-codes.d.ts +21 -0
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +25 -0
- package/dist/workspace/records-cli.d.ts +11 -0
- package/dist/workspace/records-cli.d.ts.map +1 -1
- package/dist/workspace/records-cli.js +57 -5
- package/dist/workspace/records.schema.json +5 -0
- package/dist/workspace/release-candidates.d.ts +2 -0
- package/dist/workspace/release-candidates.d.ts.map +1 -1
- package/dist/workspace/release-candidates.js +5 -2
- package/dist/workspace/release-train.d.ts +53 -0
- package/dist/workspace/release-train.d.ts.map +1 -0
- package/dist/workspace/release-train.js +134 -0
- package/dist/workspace/release-workflow.d.ts.map +1 -1
- package/dist/workspace/release-workflow.js +4 -3
- package/dist/workspace/repo-ref.d.ts +45 -0
- package/dist/workspace/repo-ref.d.ts.map +1 -0
- package/dist/workspace/repo-ref.js +114 -0
- package/dist/workspace/run-event.schema.json +2 -2
- package/dist/workspace/runs-cli.d.ts +9 -0
- package/dist/workspace/runs-cli.d.ts.map +1 -1
- package/dist/workspace/runs-cli.js +48 -3
- package/dist/workspace/runs.schema.json +7 -0
- package/dist/workspace/search-contract.d.ts +1 -1
- package/dist/workspace/search-contract.d.ts.map +1 -1
- package/dist/workspace/status-decisions.d.ts +37 -0
- package/dist/workspace/status-decisions.d.ts.map +1 -0
- package/dist/workspace/status-decisions.js +147 -0
- package/dist/workspace/status-gates.d.ts +9 -0
- package/dist/workspace/status-gates.d.ts.map +1 -1
- package/dist/workspace/status.d.ts +4 -0
- package/dist/workspace/status.d.ts.map +1 -1
- package/dist/workspace/status.js +14 -2
- package/dist/workspace/status.schema.json +6 -0
- package/dist/workspace/work-cli.d.ts.map +1 -1
- package/dist/workspace/work-cli.js +5 -3
- package/dist/workspace/work-evidence-ops.d.ts +83 -0
- package/dist/workspace/work-evidence-ops.d.ts.map +1 -0
- package/dist/workspace/work-evidence-ops.js +169 -0
- package/dist/workspace/work-evidence.d.ts +12 -2
- package/dist/workspace/work-evidence.d.ts.map +1 -1
- package/dist/workspace/work-evidence.js +21 -3
- package/dist/workspace/work-evidence.schema.json +49 -2
- package/dist/workspace/workspace-gates.d.ts +110 -0
- package/dist/workspace/workspace-gates.d.ts.map +1 -0
- package/dist/workspace/workspace-gates.js +140 -0
- package/dist/workspace/workspace-kinds.schema.json +108 -10
- package/dist/workspace/workspace-ledger.d.ts +76 -0
- package/dist/workspace/workspace-ledger.d.ts.map +1 -0
- package/dist/workspace/workspace-ledger.js +91 -0
- package/dist/workspace/workspace-run.d.ts +112 -0
- package/dist/workspace/workspace-run.d.ts.map +1 -0
- package/dist/workspace/workspace-run.js +432 -0
- package/package.json +1 -1
- package/src/__golden__/wave-loop/op-by-apply-after-fails.json +10 -0
- package/src/__golden__/wave-loop/op-by-apply-hold.json +10 -0
- package/src/__golden__/wave-loop/op-by-apply-ignored.json +10 -0
- package/src/__golden__/wave-loop/op-by-apply-pass.json +10 -0
- package/src/__golden__/wave-loop/op-by-apply-skip.json +10 -0
- package/src/__golden__/wave-loop/op-cost-member-clash.json +6 -0
- package/src/__golden__/wave-loop/op-cost-member.json +13 -0
- package/src/__golden__/wave-loop/op-denied-source.json +16 -0
- package/src/__golden__/wave-loop/op-denied.json +16 -0
- package/src/__golden__/wave-loop/op-discover-rest-short.json +6 -0
- package/src/__golden__/wave-loop/op-discover-rest-stops.json +13 -0
- package/src/__golden__/wave-loop/op-discover-rest.json +26 -0
- package/src/__golden__/wave-loop/op-discover-wide.json +26 -0
- package/src/__golden__/wave-loop/op-held-on-again.json +16 -0
- package/src/__golden__/wave-loop/op-held-on-before-apply.json +16 -0
- package/src/__golden__/wave-loop/op-held-on.json +16 -0
- package/src/__golden__/wave-loop/op-hooks-apply-fails.json +13 -0
- package/src/__golden__/wave-loop/op-hooks-fail-after-apply.json +13 -0
- package/src/__golden__/wave-loop/op-hooks-fail-before-apply.json +13 -0
- package/src/__golden__/wave-loop/op-hooks-fail-before-plan.json +13 -0
- package/src/__golden__/wave-loop/op-hooks-held.json +13 -0
- package/src/__golden__/wave-loop/op-hooks-hold-before-apply.json +13 -0
- package/src/__golden__/wave-loop/op-hooks-pass.json +13 -0
- package/src/__golden__/wave-loop/op-hooks-refused-at-base.json +13 -0
- package/src/__golden__/wave-loop/op-hooks-share.json +33 -0
- package/src/__golden__/wave-loop/op-outcome-held.json +10 -0
- package/src/__golden__/wave-loop/op-outcome-replan-approved.json +10 -0
- package/src/__golden__/wave-loop/op-outcome-replan-forever.json +10 -0
- package/src/__golden__/wave-loop/op-outcome-replan.json +10 -0
- package/src/__golden__/wave-loop/op-outcome-stood-down.json +10 -0
- package/src/__golden__/wave-loop/op-outcome-unreadable.json +10 -0
- package/src/__golden__/wave-loop/op-overridden.json +16 -0
- package/src/__golden__/wave-loop/op-per-run.json +20 -0
- package/src/__golden__/wave-loop/op-point.json +13 -0
- package/src/__golden__/wave-loop/op-pr-review.json +10 -0
- package/src/__golden__/wave-loop/op-sealed.json +10 -0
- package/src/__golden__/wave-loop/op-spend-change-only.json +10 -0
- package/src/__golden__/wave-loop/op-spend.json +10 -0
- package/src/__golden__/wave-loop/op-wave-commands.json +55 -0
- package/src/__golden__/wave-loop/op-wide-refused.json +26 -0
- package/src/__golden__/wave-loop/op-wide-waiting.json +29 -0
- package/src/__golden__/wave-loop/op-wide.json +39 -0
- package/src/action-pins.test.ts +78 -0
- package/src/action-pins.ts +92 -0
- package/src/cli/commands/carve-score-once.test.ts +99 -0
- package/src/cli/commands/carve.ts +37 -6
- package/src/cli/commands/check-lexicon.test.ts +2 -2
- package/src/cli/commands/import-report.ts +71 -0
- package/src/cli/commands/import.ts +33 -3
- package/src/cli/commands/migrate-dialect.test.ts +145 -0
- package/src/cli/commands/migrate.ts +132 -11
- package/src/cli/handlers/__snapshots__/build-member.test.ts.snap +4 -4
- package/src/cli/handlers/approve.ts +33 -4
- package/src/cli/handlers/build.ts +7 -1
- package/src/cli/handlers/graph-reader.test.ts +31 -0
- package/src/cli/handlers/graph-reader.ts +51 -0
- package/src/cli/handlers/graph.ts +14 -0
- package/src/cli/handlers/ledger.ts +54 -0
- package/src/cli/handlers/migrate.ts +21 -12
- package/src/cli/handlers/misc.ts +10 -1
- package/src/cli/handlers/pr-env.ts +2 -2
- package/src/cli/handlers/pr.ts +176 -37
- package/src/cli/handlers/query.ts +16 -4
- package/src/cli/handlers/release.ts +6 -3
- package/src/cli/handlers/search.ts +13 -34
- package/src/cli/main.test.ts +17 -0
- package/src/cli/main.ts +87 -14
- package/src/cli/parse-args.ts +35 -2
- package/src/cli/registry.ts +22 -4
- package/src/components/builders.ts +8 -0
- package/src/components/capability-plugin-loader.ts +13 -1
- package/src/components/capability-plugin.ts +16 -0
- package/src/components/pr-env-run-bundle.test.ts +1 -0
- package/src/components/pr-hooks.test.ts +73 -5
- package/src/components/pr-hooks.ts +95 -5
- package/src/components/pr-pipeline.ts +64 -16
- package/src/components/pr-policy.test.ts +64 -1
- package/src/components/pr-policy.ts +53 -0
- package/src/components/pr-run.ts +4 -0
- package/src/components/registry.test.ts +1 -1
- package/src/components/starter-plugin.ts +6 -0
- package/src/components/verbs/index.ts +2 -0
- package/src/components/verbs/publish-artifact.test.ts +60 -0
- package/src/components/verbs/publish-artifact.ts +122 -0
- package/src/components/verbs/publish-image.test.ts +101 -0
- package/src/components/verbs/publish-image.ts +265 -0
- package/src/config-environments.ts +28 -1
- package/src/config.ts +24 -1
- package/src/gate-decision-bundle.test.ts +5 -2
- package/src/gate-decision-identity.test.ts +20 -0
- package/src/gate-decision.ts +46 -3
- package/src/graph-ir.ts +27 -0
- package/src/graph-walk.test.ts +44 -0
- package/src/graph-walk.ts +102 -0
- package/src/import/parser.ts +20 -0
- package/src/lexicon.ts +98 -6
- package/src/lifecycle/gate-ledger.ts +8 -0
- package/src/lifecycle/git.ts +2 -2
- package/src/lifecycle/ledger-read.test.ts +107 -0
- package/src/lifecycle/ledger-read.ts +195 -0
- package/src/lifecycle/release-workstation.ts +24 -0
- package/src/lifecycle/run-ledger.ts +5 -2
- package/src/lint/cross-root.test.ts +122 -0
- package/src/lint/discover.ts +3 -1
- package/src/lint/post-synth.ts +212 -2
- package/src/lint/rules/op/index.ts +2 -0
- package/src/lint/rules/op/ops019-workspace-op-target.ts +89 -0
- package/src/op/activities/activity-contracts.ts +5 -2
- package/src/op/activities/index.ts +2 -2
- package/src/op/activities/review-agent.test.ts +221 -4
- package/src/op/activities/review-agent.ts +265 -22
- package/src/op/activities/work-evidence.ts +9 -1
- package/src/op/builders.ts +9 -5
- package/src/op/composites/apply-op.test.ts +108 -0
- package/src/op/composites/apply-op.ts +37 -2
- package/src/op/gate-approval-at-base.test.ts +97 -0
- package/src/op/gate-approval-base.ts +72 -0
- package/src/op/gate-approval.test.ts +6 -1
- package/src/op/gate-approval.ts +22 -27
- package/src/op/gate-name.ts +16 -1
- package/src/op/gate-quorum-at-base.test.ts +211 -0
- package/src/op/gate-quorum.ts +156 -8
- package/src/op/gate-review.ts +14 -0
- package/src/op/gate.ts +69 -9
- package/src/op/index.ts +3 -2
- package/src/op/local-executor.ts +91 -11
- package/src/op/op-waves-depends-on.test.ts +81 -0
- package/src/op/op-waves-hook-targets.test.ts +4 -2
- package/src/op/op-waves-quorum-product.test.ts +158 -0
- package/src/op/op-waves-run.ts +288 -14
- package/src/op/op-waves.ts +143 -14
- package/src/op/runtime.ts +8 -0
- package/src/op/schedule-overdue.ts +33 -0
- package/src/op/stage-hooks.ts +13 -5
- package/src/op/workspace-run-env.ts +29 -0
- package/src/pr-forge-bundle.test.ts +2 -2
- package/src/pr-forge.test.ts +47 -0
- package/src/pr-forge.ts +68 -0
- package/src/pr-loop-bundle.test.ts +3 -2
- package/src/pr-loop.test.ts +52 -0
- package/src/pr-loop.ts +12 -0
- package/src/pr-migrations.ts +97 -0
- package/src/pr-post-open.e2e.test.ts +36 -0
- package/src/pr-post.test.ts +102 -3
- package/src/pr-post.ts +90 -2
- package/src/read-filter.test.ts +54 -0
- package/src/read-filter.ts +193 -0
- package/src/run-event.ts +1 -1
- package/src/stage-hooks-bundle.test.ts +1 -0
- package/src/terraform/carve.ts +52 -15
- package/src/terraform/cuts.test.ts +173 -0
- package/src/terraform/cuts.ts +188 -0
- package/src/terraform/folds.ts +44 -0
- package/src/terraform/graph.ts +9 -0
- package/src/terraform/radius.test.ts +90 -0
- package/src/terraform/reach.test.ts +146 -0
- package/src/terraform/reach.ts +247 -0
- package/src/terraform/score.ts +27 -29
- package/src/terraform/state.ts +3 -1
- package/src/terraform/types.ts +8 -0
- package/src/wave-blocked.ts +28 -3
- package/src/wave-loop-golden.test.ts +4 -3
- package/src/workspace/__fixtures__/import/spacelift.json +9 -0
- package/src/workspace/__fixtures__/import/tfc.json +13 -0
- package/src/workspace/behold-kinds.test.ts +2 -1
- package/src/workspace/check-contract.test.ts +8 -6
- package/src/workspace/check.schema.json +13 -2
- package/src/workspace/checks/ci-branch.test.ts +128 -0
- package/src/workspace/checks/ci-branch.ts +141 -0
- package/src/workspace/checks/gate-approval.test.ts +77 -0
- package/src/workspace/checks/gate-approval.ts +121 -0
- package/src/workspace/checks/links.ts +12 -1
- package/src/workspace/checks/org.ts +169 -0
- package/src/workspace/checks.ts +68 -7
- package/src/workspace/ci-branches.ts +31 -0
- package/src/workspace/ci-green-branches.test.ts +153 -0
- package/src/workspace/ci-green-cli.ts +7 -7
- package/src/workspace/ci-green-workflow.test.ts +4 -3
- package/src/workspace/ci-green-workflow.ts +13 -9
- package/src/workspace/ci-green.ts +49 -12
- package/src/workspace/ci-run.test.ts +72 -1
- package/src/workspace/ci-run.ts +68 -8
- package/src/workspace/ci-serve.test.ts +95 -0
- package/src/workspace/ci-serve.ts +106 -65
- package/src/workspace/component-environments.test.ts +25 -2
- package/src/workspace/compose-graph.ts +9 -2
- package/src/workspace/composites.schema.json +22 -0
- package/src/workspace/composites.test.ts +39 -8
- package/src/workspace/composites.ts +6 -2
- package/src/workspace/conformance/__writer_fixture__/work/work.schema.json +105 -1
- package/src/workspace/cross-root-lint.test.ts +90 -0
- package/src/workspace/cross-root-lint.ts +126 -0
- package/src/workspace/dbt-kind.test.ts +3 -2
- package/src/workspace/dbt-models-reader.ts +362 -0
- package/src/workspace/dbt-models.test.ts +248 -0
- package/src/workspace/dbt-project-common.ts +113 -0
- package/src/workspace/dbt-project-reader.ts +29 -112
- package/src/workspace/decision-rank.ts +40 -0
- package/src/workspace/declaration.schema.json +166 -24
- package/src/workspace/declaration.ts +101 -15
- package/src/workspace/environments.ts +40 -4
- package/src/workspace/exit-cli.ts +219 -0
- package/src/workspace/exit.schema.json +257 -0
- package/src/workspace/exit.test.ts +185 -0
- package/src/workspace/exit.ts +560 -0
- package/src/workspace/flyway-dir.ts +113 -0
- package/src/workspace/flyway-layout.ts +337 -0
- package/src/workspace/graph-cache.ts +12 -2
- package/src/workspace/graph-cli.ts +25 -10
- package/src/workspace/graph-contract.test.ts +4 -2
- package/src/workspace/graph.schema.json +34 -0
- package/src/workspace/identity.test.ts +29 -2
- package/src/workspace/import-fetch.ts +151 -0
- package/src/workspace/import-init.ts +147 -0
- package/src/workspace/import-source.test.ts +183 -0
- package/src/workspace/import-source.ts +462 -0
- package/src/workspace/init.test.ts +55 -1
- package/src/workspace/init.ts +66 -4
- package/src/workspace/intent.ts +6 -14
- package/src/workspace/kind-link-readers.ts +68 -3
- package/src/workspace/kind-readers.test.ts +36 -0
- package/src/workspace/kind-readers.ts +18 -1
- package/src/workspace/kinds.test.ts +35 -7
- package/src/workspace/kinds.ts +212 -11
- package/src/workspace/ledger.schema.json +158 -0
- package/src/workspace/lexicon-outputs.ts +132 -13
- package/src/workspace/lineage-check.ts +30 -7
- package/src/workspace/lineage-upgrade.test.ts +20 -0
- package/src/workspace/lineage-upgrade.ts +16 -7
- package/src/workspace/links.ts +24 -1
- package/src/workspace/ls-contract.test.ts +1 -1
- package/src/workspace/ls.schema.json +7 -0
- package/src/workspace/ls.ts +10 -1
- package/src/workspace/member-commands.ts +60 -2
- package/src/workspace/member-file-schema.test.ts +124 -0
- package/src/workspace/migrate-links.test.ts +99 -0
- package/src/workspace/migrate-links.ts +108 -0
- package/src/workspace/migration-dirs.test.ts +272 -0
- package/src/workspace/migration-dirs.ts +267 -0
- package/src/workspace/new-migrations.test.ts +98 -0
- package/src/workspace/new-migrations.ts +109 -0
- package/src/workspace/org-scope.test.ts +238 -0
- package/src/workspace/pr-report.schema.json +46 -0
- package/src/workspace/promote.test.ts +114 -0
- package/src/workspace/promote.ts +302 -0
- package/src/workspace/reason-codes.test.ts +10 -1
- package/src/workspace/reason-codes.ts +25 -0
- package/src/workspace/records-cli.ts +58 -5
- package/src/workspace/records.schema.json +5 -0
- package/src/workspace/release-candidates.ts +7 -2
- package/src/workspace/release-train.ts +157 -0
- package/src/workspace/release-workflow.ts +4 -3
- package/src/workspace/repo-ref.ts +113 -0
- package/src/workspace/run-event.schema.json +2 -2
- package/src/workspace/runs-cli.ts +48 -4
- package/src/workspace/runs.schema.json +7 -0
- package/src/workspace/status-contract.test.ts +46 -1
- package/src/workspace/status-decisions.test.ts +100 -0
- package/src/workspace/status-decisions.ts +151 -0
- package/src/workspace/status-gates.ts +9 -0
- package/src/workspace/status.schema.json +6 -0
- package/src/workspace/status.ts +17 -3
- package/src/workspace/where-filters.test.ts +39 -0
- package/src/workspace/work-cli.ts +5 -3
- package/src/workspace/work-evidence-ops.test.ts +176 -0
- package/src/workspace/work-evidence-ops.ts +189 -0
- package/src/workspace/work-evidence.schema.json +49 -2
- package/src/workspace/work-evidence.ts +22 -3
- package/src/workspace/workspace-gates.ts +200 -0
- package/src/workspace/workspace-kinds.schema.json +108 -10
- package/src/workspace/workspace-ledger.ts +130 -0
- package/src/workspace/workspace-run.test.ts +322 -0
- package/src/workspace/workspace-run.ts +461 -0
|
@@ -31,6 +31,7 @@ describe("the stage-hooks bundle", () => {
|
|
|
31
31
|
"packages/core/src/op/gate-quorum.ts",
|
|
32
32
|
"packages/core/src/op/op-waves.ts",
|
|
33
33
|
"packages/core/src/op/stage-hooks.ts",
|
|
34
|
+
"packages/core/src/wave-blocked.ts",
|
|
34
35
|
"packages/core/src/wave-layout.ts",
|
|
35
36
|
]);
|
|
36
37
|
expect(result.outputFiles.reduce((n, f) => n + f.contents.byteLength, 0)).toBeLessThan(48 * 1024);
|
package/src/terraform/carve.ts
CHANGED
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
|
|
14
14
|
import { inboundEdges, outboundEdges } from "./graph";
|
|
15
15
|
import { foldParentOf, resolveTier } from "./tier-map";
|
|
16
|
-
import { scoreEstate, type ScoreOptions } from "./score";
|
|
17
|
-
import type
|
|
16
|
+
import { scoreEstate, type Peelability, type ScoreOptions } from "./score";
|
|
17
|
+
import { adjacencyOf, type Adjacency } from "../graph-walk";
|
|
18
|
+
import type { TfEdge, TfGraph, TfNode } from "./types";
|
|
18
19
|
|
|
19
20
|
/** One resource in the carve set (the selected node plus its folded sub-resources). */
|
|
20
21
|
export interface CarveMember {
|
|
@@ -85,24 +86,53 @@ export interface CarveReport {
|
|
|
85
86
|
* that fold into it (same name, a `foldParentOf` child type whose parent is the
|
|
86
87
|
* selection). Those are carried along and inlined, not left behind.
|
|
87
88
|
*/
|
|
88
|
-
export function resolveCarveSet(graph: TfGraph, target: string): CarveMember[] {
|
|
89
|
-
const byAddress =
|
|
89
|
+
export function resolveCarveSet(graph: TfGraph, target: string, index?: BoundaryIndex): CarveMember[] {
|
|
90
|
+
const { byAddress, foldChildren } = index ?? boundaryIndex(graph);
|
|
90
91
|
const selected = byAddress.get(target);
|
|
91
92
|
if (!selected) return [];
|
|
92
93
|
|
|
93
94
|
const members: CarveMember[] = [{ address: selected.address, type: selected.type }];
|
|
94
95
|
if (selected.kind === "resource" && selected.type) {
|
|
95
|
-
for (const node of
|
|
96
|
-
|
|
97
|
-
const parentType = foldParentOf(node.type);
|
|
98
|
-
if (parentType === selected.type && node.name === selected.name) {
|
|
99
|
-
members.push({ address: node.address, type: node.type, foldedInto: selected.address });
|
|
100
|
-
}
|
|
96
|
+
for (const node of foldChildren.get(foldKey(selected.type, selected.name)) ?? []) {
|
|
97
|
+
members.push({ address: node.address, type: node.type, foldedInto: selected.address });
|
|
101
98
|
}
|
|
102
99
|
}
|
|
103
100
|
return members;
|
|
104
101
|
}
|
|
105
102
|
|
|
103
|
+
const foldKey = (parentType: string, name: string): string => `${parentType}\u001f${name}`;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* What `boundaryReport` looks up per target, built once per graph: nodes by
|
|
107
|
+
* address, the edges by both ends, and each fold parent's children in graph
|
|
108
|
+
* order. Without it each report scans the node and edge lists itself, which
|
|
109
|
+
* over a whole estate is O(V·E) (#4391).
|
|
110
|
+
*/
|
|
111
|
+
export interface BoundaryIndex {
|
|
112
|
+
byAddress: Map<string, TfNode>;
|
|
113
|
+
adjacency: Adjacency<TfEdge>;
|
|
114
|
+
/** `foldKey(parent type, name)` → resource nodes that fold into that parent. */
|
|
115
|
+
foldChildren: Map<string, TfNode[]>;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function boundaryIndex(graph: TfGraph): BoundaryIndex {
|
|
119
|
+
const foldChildren = new Map<string, TfNode[]>();
|
|
120
|
+
for (const node of graph.nodes) {
|
|
121
|
+
if (node.kind !== "resource" || !node.type) continue;
|
|
122
|
+
const parentType = foldParentOf(node.type);
|
|
123
|
+
if (!parentType) continue;
|
|
124
|
+
const key = foldKey(parentType, node.name);
|
|
125
|
+
const list = foldChildren.get(key);
|
|
126
|
+
if (list) list.push(node);
|
|
127
|
+
else foldChildren.set(key, [node]);
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
byAddress: new Map(graph.nodes.map((n) => [n.address, n])),
|
|
131
|
+
adjacency: adjacencyOf(graph.edges),
|
|
132
|
+
foldChildren,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
106
136
|
/**
|
|
107
137
|
* Which source the graph came from, and so which vocabulary its bridge patches
|
|
108
138
|
* are named in. Terraform is the default; `cdk` is the read-only cloud-assembly
|
|
@@ -115,6 +145,13 @@ export interface BoundaryOptions {
|
|
|
115
145
|
dialect?: CarveDialect;
|
|
116
146
|
/** Scoring hooks, so `peelability` is the same number the ranking reported. */
|
|
117
147
|
score?: ScoreOptions;
|
|
148
|
+
/**
|
|
149
|
+
* The target's score, when the caller already holds it (`carve advise`).
|
|
150
|
+
* Without it the estate is scored to find it (#4391).
|
|
151
|
+
*/
|
|
152
|
+
peelability?: Pick<Peelability, "score">;
|
|
153
|
+
/** `boundaryIndex(graph)`, shared across calls on one graph. */
|
|
154
|
+
index?: BoundaryIndex;
|
|
118
155
|
}
|
|
119
156
|
|
|
120
157
|
/** The bridge patch an inbound cut needs, per source. */
|
|
@@ -131,7 +168,7 @@ function inboundBridge(dialect: CarveDialect, fromOutput: boolean): BoundaryEdge
|
|
|
131
168
|
*/
|
|
132
169
|
export function boundaryReport(graph: TfGraph, target: string, opts: BoundaryOptions = {}): CarveReport | null {
|
|
133
170
|
const dialect = opts.dialect ?? "terraform";
|
|
134
|
-
const carveSet = resolveCarveSet(graph, target);
|
|
171
|
+
const carveSet = resolveCarveSet(graph, target, opts.index);
|
|
135
172
|
if (carveSet.length === 0) return null;
|
|
136
173
|
const inSet = new Set(carveSet.map((m) => m.address));
|
|
137
174
|
|
|
@@ -139,7 +176,7 @@ export function boundaryReport(graph: TfGraph, target: string, opts: BoundaryOpt
|
|
|
139
176
|
const outbound: BoundaryEdge[] = [];
|
|
140
177
|
|
|
141
178
|
for (const member of carveSet) {
|
|
142
|
-
for (const e of inboundEdges(graph, member.address)) {
|
|
179
|
+
for (const e of (opts.index ? (opts.index.adjacency.in.get(member.address) ?? []) : inboundEdges(graph, member.address))) {
|
|
143
180
|
if (inSet.has(e.from)) continue; // internal edge
|
|
144
181
|
inbound.push({
|
|
145
182
|
direction: "inbound",
|
|
@@ -152,7 +189,7 @@ export function boundaryReport(graph: TfGraph, target: string, opts: BoundaryOpt
|
|
|
152
189
|
...(e.crossStack ? { crossStack: true } : {}),
|
|
153
190
|
});
|
|
154
191
|
}
|
|
155
|
-
for (const e of outboundEdges(graph, member.address)) {
|
|
192
|
+
for (const e of (opts.index ? (opts.index.adjacency.out.get(member.address) ?? []) : outboundEdges(graph, member.address))) {
|
|
156
193
|
if (inSet.has(e.to)) continue; // internal edge
|
|
157
194
|
outbound.push({
|
|
158
195
|
direction: "outbound",
|
|
@@ -167,13 +204,13 @@ export function boundaryReport(graph: TfGraph, target: string, opts: BoundaryOpt
|
|
|
167
204
|
}
|
|
168
205
|
}
|
|
169
206
|
|
|
170
|
-
const score = scoreEstate(graph, opts.score).find((r) => r.address === target);
|
|
207
|
+
const score = opts.peelability ?? scoreEstate(graph, { ...opts.score, radius: false }).find((r) => r.address === target);
|
|
171
208
|
const diagnostics: string[] = [];
|
|
172
209
|
const tierOf = opts.score?.tierOf ?? resolveTier;
|
|
173
210
|
if (
|
|
174
211
|
carveSet[0].type &&
|
|
175
212
|
tierOf(carveSet[0].type) === null &&
|
|
176
|
-
graph.nodes.
|
|
213
|
+
(opts.index?.byAddress ?? new Map(graph.nodes.map((n) => [n.address, n]))).get(target)?.kind === "resource"
|
|
177
214
|
) {
|
|
178
215
|
diagnostics.push(`No known native mapping for ${carveSet[0].type} — emit will have no target type.`);
|
|
179
216
|
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { loadHcl2json, parseTerraformDir } from "./parse";
|
|
4
|
+
import { closure, components, cuts } from "./cuts";
|
|
5
|
+
import type { TfEdge, TfGraph, TfNode } from "./types";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* #3909: cuts and reference-closed sets. The proof runs against depth-estate
|
|
9
|
+
* (#2323) as `parseTerraformDir` builds it; several components, a cycle, a
|
|
10
|
+
* straddling output and cross-stack edges are on hand-built graphs.
|
|
11
|
+
*/
|
|
12
|
+
let parserAvailable = false;
|
|
13
|
+
try {
|
|
14
|
+
await loadHcl2json();
|
|
15
|
+
parserAvailable = true;
|
|
16
|
+
} catch {
|
|
17
|
+
parserAvailable = false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const ESTATE = join(__dirname, "__fixtures__", "depth-estate");
|
|
21
|
+
const ends = (edges: TfEdge[]): string[] => edges.map((e) => `${e.from} -> ${e.to}`);
|
|
22
|
+
|
|
23
|
+
const node = (address: string, extra: Partial<TfNode> = {}): TfNode => {
|
|
24
|
+
const [type, name] = address.split(".");
|
|
25
|
+
return { address, kind: "resource", type, name, instances: 1, hasDynamic: false, ...extra };
|
|
26
|
+
};
|
|
27
|
+
const edge = (from: string, to: string, extra: Partial<TfEdge> = {}): TfEdge => ({ from, to, attrs: [], via: [], ...extra });
|
|
28
|
+
|
|
29
|
+
const DEPTH_ESTATE_BLOCKS = [
|
|
30
|
+
"aws_cloudwatch_log_group.api",
|
|
31
|
+
"aws_db_subnet_group.main",
|
|
32
|
+
"aws_kms_key.data",
|
|
33
|
+
"aws_lambda_function.api",
|
|
34
|
+
"aws_lb.public",
|
|
35
|
+
"aws_lb_listener.https",
|
|
36
|
+
"aws_lb_target_group.app",
|
|
37
|
+
"aws_rds_cluster.main",
|
|
38
|
+
"aws_rds_cluster_instance.one",
|
|
39
|
+
"aws_s3_bucket.assets",
|
|
40
|
+
"aws_s3_bucket_versioning.assets",
|
|
41
|
+
"aws_security_group.app",
|
|
42
|
+
"aws_security_group.db",
|
|
43
|
+
"aws_subnet.a",
|
|
44
|
+
"aws_subnet.b",
|
|
45
|
+
"aws_subnet.private",
|
|
46
|
+
"aws_vpc.main",
|
|
47
|
+
"module.platform",
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
describe("against depth-estate", () => {
|
|
51
|
+
test("components match the pinned list: one, with the module and the count hiding what it holds", async () => {
|
|
52
|
+
if (!parserAvailable) return;
|
|
53
|
+
const graph = await parseTerraformDir(ESTATE);
|
|
54
|
+
expect(components(graph)).toEqual([
|
|
55
|
+
{
|
|
56
|
+
resources: DEPTH_ESTATE_BLOCKS,
|
|
57
|
+
outputs: ["output.db_endpoint", "output.lb_dns"],
|
|
58
|
+
instances: "unknown",
|
|
59
|
+
hiddenBy: ["aws_subnet.private", "module.platform"],
|
|
60
|
+
},
|
|
61
|
+
]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("cuts of a hand-picked set name each crossing edge with its direction", async () => {
|
|
65
|
+
if (!parserAvailable) return;
|
|
66
|
+
const graph = await parseTerraformDir(ESTATE);
|
|
67
|
+
const c = cuts(graph, ["aws_rds_cluster.main", "aws_rds_cluster_instance.one"]);
|
|
68
|
+
expect(ends(c.inbound)).toEqual([
|
|
69
|
+
"aws_lambda_function.api -> aws_rds_cluster.main",
|
|
70
|
+
"output.db_endpoint -> aws_rds_cluster.main",
|
|
71
|
+
]);
|
|
72
|
+
expect(ends(c.outbound)).toEqual([
|
|
73
|
+
"aws_rds_cluster.main -> aws_db_subnet_group.main",
|
|
74
|
+
"aws_rds_cluster.main -> aws_kms_key.data",
|
|
75
|
+
"aws_rds_cluster.main -> aws_security_group.db",
|
|
76
|
+
]);
|
|
77
|
+
expect(c.counts).toEqual({
|
|
78
|
+
inbound: { resources: 1, outputs: 1, crossStack: 0 },
|
|
79
|
+
outbound: { resources: 3, outputs: 0, crossStack: 0 },
|
|
80
|
+
});
|
|
81
|
+
expect(c.bothWays).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("closure from a leaf with outbound allowed leaves only outbound cuts", async () => {
|
|
85
|
+
if (!parserAvailable) return;
|
|
86
|
+
const graph = await parseTerraformDir(ESTATE);
|
|
87
|
+
const leaf = closure(graph, ["aws_rds_cluster_instance.one"], "outbound");
|
|
88
|
+
expect(leaf.resources).toEqual(["aws_rds_cluster_instance.one"]);
|
|
89
|
+
expect(leaf.inbound).toEqual([]);
|
|
90
|
+
expect(ends(leaf.outbound)).toEqual(["aws_rds_cluster_instance.one -> aws_rds_cluster.main"]);
|
|
91
|
+
|
|
92
|
+
// A seed with dependents takes them all, outputs included.
|
|
93
|
+
const key = closure(graph, ["aws_kms_key.data"], "outbound");
|
|
94
|
+
expect(key.resources).toEqual(["aws_kms_key.data", "aws_lambda_function.api", "aws_rds_cluster.main", "aws_rds_cluster_instance.one"]);
|
|
95
|
+
expect(key.outputs).toEqual(["output.db_endpoint"]);
|
|
96
|
+
expect(key.inbound).toEqual([]);
|
|
97
|
+
expect(key.outbound.length).toBeGreaterThan(0);
|
|
98
|
+
expect(key.instances).toBe(4);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("closure with inbound allowed takes what the seed depends on, and leaves only inbound cuts", async () => {
|
|
102
|
+
if (!parserAvailable) return;
|
|
103
|
+
const graph = await parseTerraformDir(ESTATE);
|
|
104
|
+
const c = closure(graph, ["aws_rds_cluster_instance.one"], "inbound");
|
|
105
|
+
expect(c.outbound).toEqual([]);
|
|
106
|
+
expect(c.resources).toContain("aws_vpc.main");
|
|
107
|
+
expect(c.inbound.every((e) => !c.resources.includes(e.from))).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("closure with none allowed returns the leaf's whole component", async () => {
|
|
111
|
+
if (!parserAvailable) return;
|
|
112
|
+
const graph = await parseTerraformDir(ESTATE);
|
|
113
|
+
const c = closure(graph, ["aws_rds_cluster_instance.one"], "none");
|
|
114
|
+
expect(c.resources).toEqual(DEPTH_ESTATE_BLOCKS);
|
|
115
|
+
expect(c.outputs).toEqual(["output.db_endpoint", "output.lb_dns"]);
|
|
116
|
+
expect([...c.inbound, ...c.outbound]).toEqual([]);
|
|
117
|
+
expect(c.instances).toBe("unknown");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("a set that splits a fold is contracted before its cuts are counted", async () => {
|
|
121
|
+
if (!parserAvailable) return;
|
|
122
|
+
const graph = await parseTerraformDir(ESTATE);
|
|
123
|
+
const child = cuts(graph, ["aws_s3_bucket_versioning.assets"]);
|
|
124
|
+
const parent = cuts(graph, ["aws_s3_bucket.assets"]);
|
|
125
|
+
expect(child).toEqual(parent);
|
|
126
|
+
expect(parent.resources).toEqual(["aws_s3_bucket.assets", "aws_s3_bucket_versioning.assets"]);
|
|
127
|
+
expect(ends(parent.inbound)).toEqual(["aws_lambda_function.api -> aws_s3_bucket.assets"]);
|
|
128
|
+
expect(parent.outbound).toEqual([]);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("on hand-built graphs", () => {
|
|
133
|
+
test("separate components stay separate, and an output never joins them", () => {
|
|
134
|
+
const graph: TfGraph = {
|
|
135
|
+
nodes: [node("aws_sqs_queue.a"), node("aws_sqs_queue.b"), node("aws_sns_topic.c")],
|
|
136
|
+
edges: [
|
|
137
|
+
edge("aws_sqs_queue.b", "aws_sqs_queue.a"),
|
|
138
|
+
edge("output.both", "aws_sqs_queue.a", { fromKind: "output" }),
|
|
139
|
+
edge("output.both", "aws_sns_topic.c", { fromKind: "output" }),
|
|
140
|
+
],
|
|
141
|
+
};
|
|
142
|
+
expect(components(graph)).toEqual([
|
|
143
|
+
{ resources: ["aws_sns_topic.c"], outputs: ["output.both"], instances: 1 },
|
|
144
|
+
{ resources: ["aws_sqs_queue.a", "aws_sqs_queue.b"], outputs: ["output.both"], instances: 2 },
|
|
145
|
+
]);
|
|
146
|
+
// The straddling output stays outside a none-closure, its read left as a cut.
|
|
147
|
+
const c = closure(graph, ["aws_sqs_queue.a"], "none");
|
|
148
|
+
expect(c.outputs).toEqual([]);
|
|
149
|
+
expect(ends(c.inbound)).toEqual(["output.both -> aws_sqs_queue.a"]);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("a depends_on cycle terminates in every closure", () => {
|
|
153
|
+
const graph: TfGraph = {
|
|
154
|
+
nodes: [node("aws_sqs_queue.a"), node("aws_sqs_queue.b"), node("aws_sqs_queue.c")],
|
|
155
|
+
edges: [edge("aws_sqs_queue.a", "aws_sqs_queue.b"), edge("aws_sqs_queue.b", "aws_sqs_queue.c"), edge("aws_sqs_queue.c", "aws_sqs_queue.a")],
|
|
156
|
+
};
|
|
157
|
+
for (const allow of ["inbound", "outbound", "none"] as const) {
|
|
158
|
+
expect(closure(graph, ["aws_sqs_queue.a"], allow).resources).toEqual(["aws_sqs_queue.a", "aws_sqs_queue.b", "aws_sqs_queue.c"]);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("crossStack cuts are counted apart", () => {
|
|
163
|
+
const graph: TfGraph = {
|
|
164
|
+
nodes: [node("Data/Bucket"), node("App/Fn")],
|
|
165
|
+
edges: [edge("App/Fn", "Data/Bucket", { crossStack: true })],
|
|
166
|
+
};
|
|
167
|
+
expect(cuts(graph, ["Data/Bucket"]).counts.inbound).toEqual({ resources: 1, outputs: 0, crossStack: 1 });
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("an address the graph does not hold is an error", () => {
|
|
171
|
+
expect(() => cuts({ nodes: [], edges: [] }, ["aws_vpc.nope"])).toThrow(/not a resource, module or output/);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cuts and reference-closed sets over the carve graph (#3909).
|
|
3
|
+
*
|
|
4
|
+
* `carve advise` scores one block at a time. A consumer splitting one root into
|
|
5
|
+
* several (terragucci's estate carving) needs the same question for a set:
|
|
6
|
+
* which edges cross its boundary and which way, and the smallest set around a
|
|
7
|
+
* seed whose crossings all run a way the caller allows. These are pure
|
|
8
|
+
* functions over `TfGraph`, on the index and walk in `reach.ts`, so the rules
|
|
9
|
+
* there hold here too: folds contract first, outputs are terminal and counted
|
|
10
|
+
* apart, modules and unresolved `count` are named as hiding their contents, a
|
|
11
|
+
* `depends_on` cycle terminates, and `crossStack` edges are counted apart.
|
|
12
|
+
*
|
|
13
|
+
* Nothing here ranks sets, reads state or plan times, writes HCL, or knows
|
|
14
|
+
* about roots or backends; that stays with the consumer.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { carveIndex, cmp, hidesContents, isOutputAddress, walkFrom, weigh, type CarveIndex } from "./reach";
|
|
18
|
+
import type { TfEdge, TfGraph } from "./types";
|
|
19
|
+
|
|
20
|
+
/** Cut edges of one direction, counted. */
|
|
21
|
+
export interface CutCount {
|
|
22
|
+
/** Edges whose reading side is a resource or module block. */
|
|
23
|
+
resources: number;
|
|
24
|
+
/** Edges whose reading side is an `output` block. */
|
|
25
|
+
outputs: number;
|
|
26
|
+
/** Of all of them, the `crossStack` edges (a CDK `Fn::ImportValue`). */
|
|
27
|
+
crossStack: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A set of blocks, as the walks see it. */
|
|
31
|
+
export interface BlockSet {
|
|
32
|
+
/** Resource and module addresses, each fold parent with the blocks folded into it. Sorted. */
|
|
33
|
+
resources: string[];
|
|
34
|
+
/** `output.<name>` pseudo-addresses in the set. Sorted. */
|
|
35
|
+
outputs: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface Cuts extends BlockSet {
|
|
39
|
+
/** Edges where something outside reads something inside, as the graph holds them. */
|
|
40
|
+
inbound: TfEdge[];
|
|
41
|
+
/** Edges where something inside reads something outside. */
|
|
42
|
+
outbound: TfEdge[];
|
|
43
|
+
counts: { inbound: CutCount; outbound: CutCount };
|
|
44
|
+
/** Cuts run in both directions: the set can move only with wiring both ways. */
|
|
45
|
+
bothWays: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The cut directions a closure may leave: `inbound` (outside may read in), `outbound` (inside may read out), or `none`. */
|
|
49
|
+
export type CutAllowance = "inbound" | "outbound" | "none";
|
|
50
|
+
|
|
51
|
+
export interface Closure extends Cuts {
|
|
52
|
+
/** The seeds as the walk saw them, folds contracted. */
|
|
53
|
+
seeds: string[];
|
|
54
|
+
allow: CutAllowance;
|
|
55
|
+
/**
|
|
56
|
+
* How many resource instances the set stands for: each block once, or its
|
|
57
|
+
* instance count once `--state` resolved it. `"unknown"` where a module or an
|
|
58
|
+
* unresolved `count`/`for_each` hides how much it holds; `hiddenBy` names them.
|
|
59
|
+
*/
|
|
60
|
+
instances: number | "unknown";
|
|
61
|
+
hiddenBy?: string[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface Component extends BlockSet {
|
|
65
|
+
/** As {@link Closure.instances}. */
|
|
66
|
+
instances: number | "unknown";
|
|
67
|
+
hiddenBy?: string[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The graph's weakly connected components (#3909): blocks joined by a
|
|
72
|
+
* reference either way, fold-contracted, each module one node. An output never
|
|
73
|
+
* joins two components, since outputs are terminal; it is listed in every
|
|
74
|
+
* component it reads into. Components are sorted by their first address.
|
|
75
|
+
*/
|
|
76
|
+
export function components(graph: TfGraph): Component[] {
|
|
77
|
+
const index = carveIndex(graph);
|
|
78
|
+
const assigned = new Set<string>();
|
|
79
|
+
const found: Component[] = [];
|
|
80
|
+
for (const address of [...index.nodes.keys()].sort(cmp)) {
|
|
81
|
+
const start = index.rep(address);
|
|
82
|
+
if (assigned.has(start)) continue;
|
|
83
|
+
const reached = walkFrom(index, [start], "both");
|
|
84
|
+
const reps = [start, ...[...reached.keys()].filter((a) => !isOutputAddress(a))];
|
|
85
|
+
for (const rep of reps) assigned.add(rep);
|
|
86
|
+
found.push({ ...blockSet(index, reps, [...reached.keys()].filter(isOutputAddress)), ...holdings(index, reps) });
|
|
87
|
+
}
|
|
88
|
+
return found;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Every edge crossing the boundary of `addresses` (#3909). A folded block in
|
|
93
|
+
* the set brings its parent and siblings, and a fold parent its folded blocks,
|
|
94
|
+
* before anything is counted, so an edge inside one fold is never a cut. An
|
|
95
|
+
* `output.<name>` in the set is inside it like any block.
|
|
96
|
+
*
|
|
97
|
+
* Throws when an address is not a block of the graph.
|
|
98
|
+
*/
|
|
99
|
+
export function cuts(graph: TfGraph, addresses: Iterable<string>): Cuts {
|
|
100
|
+
const index = carveIndex(graph);
|
|
101
|
+
return cutsOf(index, contract(index, addresses));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The smallest superset of `seeds` whose remaining cuts all run in the
|
|
106
|
+
* direction `allow` permits, plus those cuts (#3909).
|
|
107
|
+
*
|
|
108
|
+
* - `outbound`: nothing outside may read in, so the set takes every
|
|
109
|
+
* dependent of the seeds, outputs included.
|
|
110
|
+
* - `inbound`: nothing inside may read out, so the set takes everything the
|
|
111
|
+
* seeds depend on.
|
|
112
|
+
* - `none`: the seeds' whole components, with each output that reads only
|
|
113
|
+
* into them.
|
|
114
|
+
*
|
|
115
|
+
* Outputs are terminal, so none of these walks passes through one. An output
|
|
116
|
+
* that reads both the set and something outside it is therefore a cut no
|
|
117
|
+
* closure removes: under `none` it stays outside and its read into the set is
|
|
118
|
+
* left as an inbound cut, for the caller to wire.
|
|
119
|
+
*/
|
|
120
|
+
export function closure(graph: TfGraph, seeds: Iterable<string>, allow: CutAllowance): Closure {
|
|
121
|
+
const index = carveIndex(graph);
|
|
122
|
+
const start = contract(index, seeds);
|
|
123
|
+
const direction = allow === "outbound" ? "in" : allow === "inbound" ? "out" : "both";
|
|
124
|
+
const members = new Set([...start, ...walkFrom(index, start, direction).keys()]);
|
|
125
|
+
if (allow === "none") {
|
|
126
|
+
for (const address of [...members]) {
|
|
127
|
+
if (!isOutputAddress(address) || start.has(address)) continue;
|
|
128
|
+
const reads = (index.adj.out.get(address) ?? []).map((e) => e.to);
|
|
129
|
+
if (!reads.every((to) => members.has(to))) members.delete(address);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const found = cutsOf(index, members);
|
|
133
|
+
return {
|
|
134
|
+
...found,
|
|
135
|
+
seeds: [...start].sort(cmp),
|
|
136
|
+
allow,
|
|
137
|
+
...holdings(index, [...members].filter((a) => !isOutputAddress(a))),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** The addresses as the walks see them: each fold child replaced by its parent. Throws on an unknown address. */
|
|
142
|
+
function contract(index: CarveIndex, addresses: Iterable<string>): Set<string> {
|
|
143
|
+
const out = new Set<string>();
|
|
144
|
+
for (const address of addresses) {
|
|
145
|
+
const known = index.nodes.has(address) || (isOutputAddress(address) && index.adj.out.has(address));
|
|
146
|
+
if (!known) throw new Error(`carve: ${address} is not a resource, module or output in this graph`);
|
|
147
|
+
out.add(index.rep(address));
|
|
148
|
+
}
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function cutsOf(index: CarveIndex, members: ReadonlySet<string>): Cuts {
|
|
153
|
+
const inbound: TfEdge[] = [];
|
|
154
|
+
const outbound: TfEdge[] = [];
|
|
155
|
+
for (const address of members) {
|
|
156
|
+
for (const e of index.adj.in.get(address) ?? []) if (!members.has(e.from)) inbound.push(e.edge);
|
|
157
|
+
for (const e of index.adj.out.get(address) ?? []) if (!members.has(e.to)) outbound.push(e.edge);
|
|
158
|
+
}
|
|
159
|
+
const byEnds = (a: TfEdge, b: TfEdge): number => cmp(a.from, b.from) || cmp(a.to, b.to);
|
|
160
|
+
inbound.sort(byEnds);
|
|
161
|
+
outbound.sort(byEnds);
|
|
162
|
+
const reps = [...members];
|
|
163
|
+
return {
|
|
164
|
+
...blockSet(index, reps.filter((a) => !isOutputAddress(a)), reps.filter(isOutputAddress)),
|
|
165
|
+
inbound,
|
|
166
|
+
outbound,
|
|
167
|
+
counts: { inbound: countOf(inbound), outbound: countOf(outbound) },
|
|
168
|
+
bothWays: inbound.length > 0 && outbound.length > 0,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function countOf(edges: readonly TfEdge[]): CutCount {
|
|
173
|
+
const outputs = edges.filter((e) => e.fromKind === "output").length;
|
|
174
|
+
return { resources: edges.length - outputs, outputs, crossStack: edges.filter((e) => e.crossStack).length };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function blockSet(index: CarveIndex, reps: readonly string[], outputs: readonly string[]): BlockSet {
|
|
178
|
+
return {
|
|
179
|
+
resources: reps.flatMap((rep) => index.membersOf(rep)).sort(cmp),
|
|
180
|
+
outputs: [...outputs].sort(cmp),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function holdings(index: CarveIndex, reps: readonly string[]): { instances: number | "unknown"; hiddenBy?: string[] } {
|
|
185
|
+
const instances = weigh(index, reps);
|
|
186
|
+
if (instances !== "unknown") return { instances };
|
|
187
|
+
return { instances, hiddenBy: reps.filter((a) => hidesContents(index.nodes.get(a)!)).sort(cmp) };
|
|
188
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fold detection for the carve graph, shared by the scorer and the walks.
|
|
3
|
+
*
|
|
4
|
+
* A sub-resource whose type inlines into a parent (`foldParentOf`) carves with
|
|
5
|
+
* that parent when the parent is in the estate under the same name:
|
|
6
|
+
* `aws_s3_bucket_versioning.assets` goes with `aws_s3_bucket.assets`. The
|
|
7
|
+
* scorer drops it from the ranking and discounts its edge to the parent; the
|
|
8
|
+
* walks in `reach.ts` contract it into the parent before stepping, since an
|
|
9
|
+
* uncontracted walk counts the child as a separate dependent (#2325 measured a
|
|
10
|
+
* 100% overstatement on the sample estate). It lived privately in `score.ts`
|
|
11
|
+
* until the walks needed it.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { foldParentOf } from "./tier-map";
|
|
15
|
+
import type { TfGraph } from "./types";
|
|
16
|
+
|
|
17
|
+
export interface Folds {
|
|
18
|
+
/** Every address that folds into a parent present in the estate. */
|
|
19
|
+
folded: Set<string>;
|
|
20
|
+
/** Parent address → the addresses folded into it. */
|
|
21
|
+
childrenOf: Map<string, Set<string>>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Build the set of addresses that fold into a parent present in the estate, plus
|
|
26
|
+
* the reverse lookup (parent → its folded children), so a parent can discount
|
|
27
|
+
* inbound edges coming from its own folded sub-resources.
|
|
28
|
+
*/
|
|
29
|
+
export function computeFolds(graph: TfGraph): Folds {
|
|
30
|
+
const byAddress = new Map(graph.nodes.map((n) => [n.address, n]));
|
|
31
|
+
const folded = new Set<string>();
|
|
32
|
+
const childrenOf = new Map<string, Set<string>>();
|
|
33
|
+
for (const node of graph.nodes) {
|
|
34
|
+
if (node.kind !== "resource" || !node.type) continue;
|
|
35
|
+
const parentType = foldParentOf(node.type);
|
|
36
|
+
if (!parentType) continue;
|
|
37
|
+
const parentAddress = `${parentType}.${node.name}`;
|
|
38
|
+
if (!byAddress.has(parentAddress)) continue; // sub-resource without its parent → score it on its own
|
|
39
|
+
folded.add(node.address);
|
|
40
|
+
if (!childrenOf.has(parentAddress)) childrenOf.set(parentAddress, new Set());
|
|
41
|
+
childrenOf.get(parentAddress)!.add(node.address);
|
|
42
|
+
}
|
|
43
|
+
return { folded, childrenOf };
|
|
44
|
+
}
|
package/src/terraform/graph.ts
CHANGED
|
@@ -270,6 +270,13 @@ function blockHasMeta(block: unknown, key: string): boolean {
|
|
|
270
270
|
return !!block && typeof block === "object" && key in (block as Record<string, unknown>);
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
+
/** Which expansion meta-argument a block carries, as a spreadable `TfNode` field (#2325). */
|
|
274
|
+
function expansionOf(block: unknown): { expansion?: "count" | "for_each" } {
|
|
275
|
+
if (blockHasMeta(block, "count")) return { expansion: "count" };
|
|
276
|
+
if (blockHasMeta(block, "for_each")) return { expansion: "for_each" };
|
|
277
|
+
return {};
|
|
278
|
+
}
|
|
279
|
+
|
|
273
280
|
/**
|
|
274
281
|
* The literal string at a dotted path into a block, or undefined when the path
|
|
275
282
|
* misses or lands on an interpolation. A dotted path walks nested blocks —
|
|
@@ -366,6 +373,7 @@ export function buildGraph(tree: Hcl2JsonTree, exprRefs: ExpressionRefs): TfGrap
|
|
|
366
373
|
name,
|
|
367
374
|
instances: 1,
|
|
368
375
|
hasDynamic: dynamic || touchesData,
|
|
376
|
+
...expansionOf(block),
|
|
369
377
|
identity: literalIdentity(block, type),
|
|
370
378
|
dataSourceValues: dataSourceValues(block, type),
|
|
371
379
|
});
|
|
@@ -386,6 +394,7 @@ export function buildGraph(tree: Hcl2JsonTree, exprRefs: ExpressionRefs): TfGrap
|
|
|
386
394
|
name,
|
|
387
395
|
instances: 1,
|
|
388
396
|
hasDynamic: dynamic || touchesData,
|
|
397
|
+
...expansionOf(block),
|
|
389
398
|
});
|
|
390
399
|
}
|
|
391
400
|
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { carveAdvise, carveJson, formatCarveReport } from "../cli/commands/carve";
|
|
4
|
+
import { loadHcl2json } from "./parse";
|
|
5
|
+
import { blastRadius } from "./reach";
|
|
6
|
+
import { scoreEstate } from "./score";
|
|
7
|
+
import type { TfEdge, TfGraph, TfNode } from "./types";
|
|
8
|
+
|
|
9
|
+
/** #2326: blast radius on advise, its own axis beside the score. */
|
|
10
|
+
let parserAvailable = false;
|
|
11
|
+
try {
|
|
12
|
+
await loadHcl2json();
|
|
13
|
+
parserAvailable = true;
|
|
14
|
+
} catch {
|
|
15
|
+
parserAvailable = false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const ESTATE = join(__dirname, "__fixtures__", "depth-estate");
|
|
19
|
+
const STATE = join(ESTATE, "terraform.tfstate");
|
|
20
|
+
|
|
21
|
+
const node = (address: string, extra: Partial<TfNode> = {}): TfNode => {
|
|
22
|
+
const [type, name] = address.split(".");
|
|
23
|
+
return { address, kind: "resource", type, name, instances: 1, hasDynamic: false, ...extra };
|
|
24
|
+
};
|
|
25
|
+
const edge = (from: string, to: string, extra: Partial<TfEdge> = {}): TfEdge => ({ from, to, attrs: [], via: [], ...extra });
|
|
26
|
+
|
|
27
|
+
describe("radius on carve advise against depth-estate", () => {
|
|
28
|
+
test("each resource carries its radius in the JSON, and the score still sums from its penalties", async () => {
|
|
29
|
+
if (!parserAvailable) return;
|
|
30
|
+
const result = await carveAdvise({ from: ESTATE, statePath: STATE });
|
|
31
|
+
const byAddress = new Map(carveJson(result).resources.map((r) => [r.address, r]));
|
|
32
|
+
expect(byAddress.get("aws_db_subnet_group.main")?.radius).toEqual({ resources: 3, outputs: 1, depth: 2 });
|
|
33
|
+
expect(byAddress.get("aws_lambda_function.api")?.radius).toEqual({ resources: 0, outputs: 0, depth: 0 });
|
|
34
|
+
for (const r of byAddress.values()) {
|
|
35
|
+
if (r.breakdown.tier === null) continue;
|
|
36
|
+
const sum = 100 + Object.values(r.breakdown.penalties).reduce((a, b) => a + b, 0);
|
|
37
|
+
expect(r.score).toBe(Math.max(0, Math.min(100, sum)));
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("the subnet group outranks the cluster on peelability and out-reaches it on radius", async () => {
|
|
42
|
+
if (!parserAvailable) return;
|
|
43
|
+
const result = await carveAdvise({ from: ESTATE });
|
|
44
|
+
const group = result.results!.find((r) => r.address === "aws_db_subnet_group.main")!;
|
|
45
|
+
const cluster = result.results!.find((r) => r.address === "aws_rds_cluster.main")!;
|
|
46
|
+
expect(group.score).toBeGreaterThan(cluster.score);
|
|
47
|
+
expect(group.radius!.resources).toBeGreaterThan(cluster.radius!.resources as number);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("a module or an unresolved count makes the resource count unknown, and says which", async () => {
|
|
51
|
+
if (!parserAvailable) return;
|
|
52
|
+
const bare = await carveAdvise({ from: ESTATE });
|
|
53
|
+
const vpc = bare.results!.find((r) => r.address === "aws_vpc.main")!;
|
|
54
|
+
expect(vpc.radius).toEqual({ resources: "unknown", outputs: 2, depth: 3, hiddenBy: ["aws_subnet.private", "module.platform"] });
|
|
55
|
+
const withState = await carveAdvise({ from: ESTATE, statePath: STATE });
|
|
56
|
+
expect(withState.results!.find((r) => r.address === "aws_vpc.main")!.radius!.hiddenBy).toEqual(["module.platform"]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("the human report prints the radius as a column", async () => {
|
|
60
|
+
if (!parserAvailable) return;
|
|
61
|
+
const text = formatCarveReport(await carveAdvise({ from: ESTATE }));
|
|
62
|
+
expect(text).toMatch(/ 80 {2}3\+1 d2 {2}aws_db_subnet_group\.main /);
|
|
63
|
+
expect(text).toMatch(/\?\+2 d3 {2}aws_vpc\.main /);
|
|
64
|
+
expect(text).toMatch(/ - {7}aws_lambda_function\.api /);
|
|
65
|
+
expect(text).toContain("Radius: resources+outputs");
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("radius on hand-built graphs", () => {
|
|
70
|
+
test("instances weight the count once state resolved them, and only then", () => {
|
|
71
|
+
const graph = (resolved: boolean): TfGraph => ({
|
|
72
|
+
nodes: [
|
|
73
|
+
node("aws_vpc.main"),
|
|
74
|
+
node("aws_subnet.private", { expansion: "count", hasDynamic: true, instances: resolved ? 3 : 1, ...(resolved ? { instancesFrom: "state" as const } : {}) }),
|
|
75
|
+
],
|
|
76
|
+
edges: [edge("aws_subnet.private", "aws_vpc.main")],
|
|
77
|
+
});
|
|
78
|
+
expect(blastRadius(graph(true), "aws_vpc.main")).toEqual({ resources: 3, outputs: 0, depth: 1 });
|
|
79
|
+
expect(blastRadius(graph(false), "aws_vpc.main")).toEqual({ resources: "unknown", outputs: 0, depth: 1, hiddenBy: ["aws_subnet.private"] });
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("on a graph with crossStack edges the radius splits off what sits across a stack boundary", () => {
|
|
83
|
+
const graph: TfGraph = {
|
|
84
|
+
nodes: [node("Data/Bucket"), node("Data/Reader"), node("App/Fn"), node("App/Alarm")],
|
|
85
|
+
edges: [edge("Data/Reader", "Data/Bucket"), edge("App/Fn", "Data/Bucket", { crossStack: true }), edge("App/Alarm", "App/Fn")],
|
|
86
|
+
};
|
|
87
|
+
const bucket = scoreEstate(graph, { tierOf: () => ({ tier: 1, mapsTo: "AWS::S3::Bucket" }) }).find((r) => r.address === "Data/Bucket")!;
|
|
88
|
+
expect(bucket.radius).toEqual({ resources: 3, outputs: 0, depth: 2, crossStack: 2 });
|
|
89
|
+
});
|
|
90
|
+
});
|