@intentius/chant 0.124.46 → 0.124.48
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 +21 -2
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.js +6 -1
- 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 +143 -39
- 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 +65 -14
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +36 -2
- package/dist/cli/registry.d.ts +14 -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/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 +9 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +6 -2
- package/dist/gate-decision.d.ts +11 -2
- package/dist/gate-decision.d.ts.map +1 -1
- package/dist/gate-decision.js +15 -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 +71 -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 +5 -2
- 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.d.ts +11 -5
- package/dist/op/gate-approval.d.ts.map +1 -1
- package/dist/op/gate-approval.js +11 -6
- 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 +44 -3
- 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 +38 -3
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-executor.js +48 -6
- 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-post.d.ts +42 -0
- package/dist/pr-post.d.ts.map +1 -1
- package/dist/pr-post.js +67 -1
- 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 +263 -254
- package/dist/workspace/check.schema.json +11 -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/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 +11 -1
- package/dist/workspace/checks.d.ts.map +1 -1
- package/dist/workspace/checks.js +59 -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 +43 -2
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.js +45 -11
- package/dist/workspace/declaration.schema.json +133 -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/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-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/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/kinds.d.ts +66 -2
- package/dist/workspace/kinds.d.ts.map +1 -1
- package/dist/workspace/kinds.js +98 -4
- 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/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/reason-codes.d.ts +8 -0
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +11 -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 +58 -3
- 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 +23 -2
- package/src/cli/handlers/build.ts +7 -1
- 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 +138 -36
- 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 +15 -0
- package/src/cli/main.ts +65 -14
- package/src/cli/parse-args.ts +27 -2
- package/src/cli/registry.ts +14 -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-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/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 +8 -1
- package/src/gate-decision-bundle.test.ts +1 -0
- package/src/gate-decision-identity.test.ts +9 -0
- package/src/gate-decision.ts +26 -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 +74 -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.test.ts +6 -1
- package/src/op/gate-approval.ts +12 -6
- 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 +67 -8
- package/src/op/index.ts +3 -2
- package/src/op/local-executor.ts +69 -9
- 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-post-open.e2e.test.ts +36 -0
- package/src/pr-post.test.ts +102 -3
- package/src/pr-post.ts +89 -1
- 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/behold-kinds.test.ts +1 -1
- package/src/workspace/check-contract.test.ts +7 -6
- package/src/workspace/check.schema.json +11 -2
- package/src/workspace/checks/ci-branch.test.ts +128 -0
- package/src/workspace/checks/ci-branch.ts +141 -0
- package/src/workspace/checks/links.ts +12 -1
- package/src/workspace/checks/org.ts +169 -0
- package/src/workspace/checks.ts +64 -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 +133 -24
- package/src/workspace/declaration.ts +84 -15
- package/src/workspace/environments.ts +40 -4
- package/src/workspace/flyway-dir.ts +113 -0
- package/src/workspace/flyway-layout.ts +337 -0
- 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 +14 -2
- package/src/workspace/intent.ts +6 -14
- package/src/workspace/kind-link-readers.ts +68 -3
- package/src/workspace/kinds.test.ts +12 -7
- package/src/workspace/kinds.ts +132 -7
- 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/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/org-scope.test.ts +238 -0
- package/src/workspace/reason-codes.test.ts +6 -1
- package/src/workspace/reason-codes.ts +11 -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 +58 -3
- 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
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { loadHcl2json, parseTerraformDir } from "./parse";
|
|
4
|
+
import { carveIndex, reach } from "./reach";
|
|
5
|
+
import type { TfEdge, TfGraph, TfNode } from "./types";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* #2325: transitive reachability over the carve graph. The fixture assertions
|
|
9
|
+
* run against depth-estate (#2323) as the production parse builds it; the
|
|
10
|
+
* cycle and cross-stack cases need graphs Terraform would not produce, so they
|
|
11
|
+
* are hand-built.
|
|
12
|
+
*/
|
|
13
|
+
let parserAvailable = false;
|
|
14
|
+
try {
|
|
15
|
+
await loadHcl2json();
|
|
16
|
+
parserAvailable = true;
|
|
17
|
+
} catch {
|
|
18
|
+
parserAvailable = false;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const ESTATE = join(__dirname, "__fixtures__", "depth-estate");
|
|
22
|
+
const STATE = join(ESTATE, "terraform.tfstate");
|
|
23
|
+
|
|
24
|
+
const node = (address: string, extra: Partial<TfNode> = {}): TfNode => {
|
|
25
|
+
const [type, name] = address.split(".");
|
|
26
|
+
return { address, kind: "resource", type, name, instances: 1, hasDynamic: false, ...extra };
|
|
27
|
+
};
|
|
28
|
+
const edge = (from: string, to: string, extra: Partial<TfEdge> = {}): TfEdge => ({ from, to, attrs: [], via: [], ...extra });
|
|
29
|
+
|
|
30
|
+
describe("reach against depth-estate", () => {
|
|
31
|
+
test("the subnet group's radius is the cluster, its instance and the Lambda, plus one output, two deep", async () => {
|
|
32
|
+
if (!parserAvailable) return;
|
|
33
|
+
const graph = await parseTerraformDir(ESTATE);
|
|
34
|
+
expect(reach(graph, "aws_db_subnet_group.main")).toEqual({
|
|
35
|
+
address: "aws_db_subnet_group.main",
|
|
36
|
+
resources: ["aws_lambda_function.api", "aws_rds_cluster.main", "aws_rds_cluster_instance.one"],
|
|
37
|
+
outputs: ["output.db_endpoint"],
|
|
38
|
+
depth: 2,
|
|
39
|
+
crossStack: [],
|
|
40
|
+
hidden: [],
|
|
41
|
+
});
|
|
42
|
+
const vpc = reach(graph, "aws_vpc.main");
|
|
43
|
+
expect(vpc.resources).toHaveLength(13);
|
|
44
|
+
expect(vpc.outputs).toEqual(["output.db_endpoint", "output.lb_dns"]);
|
|
45
|
+
expect(vpc.depth).toBe(3);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("folds contract first: the bucket's radius is the Lambda alone, not its own versioning block", async () => {
|
|
49
|
+
if (!parserAvailable) return;
|
|
50
|
+
const graph = await parseTerraformDir(ESTATE);
|
|
51
|
+
// The uncontracted graph has the versioning block reading the bucket.
|
|
52
|
+
expect(graph.edges.some((e) => e.from === "aws_s3_bucket_versioning.assets" && e.to === "aws_s3_bucket.assets")).toBe(true);
|
|
53
|
+
expect(reach(graph, "aws_s3_bucket.assets").resources).toEqual(["aws_lambda_function.api"]);
|
|
54
|
+
// Asking from the folded child walks from its parent.
|
|
55
|
+
expect(reach(graph, "aws_s3_bucket_versioning.assets").address).toBe("aws_s3_bucket.assets");
|
|
56
|
+
expect(carveIndex(graph).membersOf("aws_s3_bucket.assets")).toEqual(["aws_s3_bucket.assets", "aws_s3_bucket_versioning.assets"]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("outputs are counted apart and end the walk", async () => {
|
|
60
|
+
if (!parserAvailable) return;
|
|
61
|
+
const graph = await parseTerraformDir(ESTATE);
|
|
62
|
+
const lb = reach(graph, "aws_lb.public");
|
|
63
|
+
expect(lb.resources).toEqual(["aws_lb_listener.https"]);
|
|
64
|
+
expect(lb.outputs).toEqual(["output.lb_dns"]);
|
|
65
|
+
expect(reach(graph, "aws_lb_listener.https")).toMatchObject({ resources: [], outputs: [], depth: 0 });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("a module and an unresolved count are named as hiding their contents; --state resolves the count", async () => {
|
|
69
|
+
if (!parserAvailable) return;
|
|
70
|
+
const bare = await parseTerraformDir(ESTATE);
|
|
71
|
+
expect(reach(bare, "aws_vpc.main").hidden).toEqual(["aws_subnet.private", "module.platform"]);
|
|
72
|
+
const withState = await parseTerraformDir(ESTATE, { statePath: STATE });
|
|
73
|
+
expect(reach(withState, "aws_vpc.main").hidden).toEqual(["module.platform"]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("direction out walks what a node needs", async () => {
|
|
77
|
+
if (!parserAvailable) return;
|
|
78
|
+
const graph = await parseTerraformDir(ESTATE);
|
|
79
|
+
expect(reach(graph, "aws_rds_cluster_instance.one", { direction: "out" }).resources).toEqual([
|
|
80
|
+
"aws_db_subnet_group.main",
|
|
81
|
+
"aws_kms_key.data",
|
|
82
|
+
"aws_rds_cluster.main",
|
|
83
|
+
"aws_security_group.db",
|
|
84
|
+
"aws_subnet.a",
|
|
85
|
+
"aws_subnet.b",
|
|
86
|
+
"aws_vpc.main",
|
|
87
|
+
]);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("reach on hand-built graphs", () => {
|
|
92
|
+
test("a depends_on cycle terminates and does not count the start", () => {
|
|
93
|
+
const graph: TfGraph = {
|
|
94
|
+
nodes: [node("aws_sqs_queue.a"), node("aws_sqs_queue.b"), node("aws_sqs_queue.c")],
|
|
95
|
+
edges: [
|
|
96
|
+
edge("aws_sqs_queue.a", "aws_sqs_queue.b", { via: ["depends_on"] }),
|
|
97
|
+
edge("aws_sqs_queue.b", "aws_sqs_queue.c", { via: ["depends_on"] }),
|
|
98
|
+
edge("aws_sqs_queue.c", "aws_sqs_queue.a", { via: ["depends_on"] }),
|
|
99
|
+
],
|
|
100
|
+
};
|
|
101
|
+
expect(reach(graph, "aws_sqs_queue.a")).toMatchObject({ resources: ["aws_sqs_queue.b", "aws_sqs_queue.c"], depth: 2 });
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("what is reachable only through a crossStack edge is listed apart", () => {
|
|
105
|
+
const graph: TfGraph = {
|
|
106
|
+
nodes: [node("A/Bucket"), node("A/Fn"), node("B/Fn"), node("B/Alarm")],
|
|
107
|
+
edges: [
|
|
108
|
+
edge("A/Fn", "A/Bucket"),
|
|
109
|
+
edge("B/Fn", "A/Bucket", { crossStack: true }),
|
|
110
|
+
edge("B/Alarm", "B/Fn"),
|
|
111
|
+
],
|
|
112
|
+
};
|
|
113
|
+
expect(reach(graph, "A/Bucket")).toMatchObject({ resources: ["A/Fn", "B/Alarm", "B/Fn"], crossStack: ["B/Alarm", "B/Fn"] });
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("an address the graph does not hold is an error, not an empty radius", () => {
|
|
117
|
+
expect(() => reach({ nodes: [], edges: [] }, "aws_vpc.nope")).toThrow(/not a resource or module/);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("under 100 ms at 6 000 nodes, index included", () => {
|
|
121
|
+
// Layered like the spike's synthetic estates: 60 layers of 100, each node
|
|
122
|
+
// reading two nodes in the layer below, about 11 800 edges. Node i reads
|
|
123
|
+
// 2i and 2i+1 below it, so the dependents of one bottom node double per
|
|
124
|
+
// layer and a walk from it covers almost everything.
|
|
125
|
+
const layers = 60;
|
|
126
|
+
const width = 100;
|
|
127
|
+
const nodes: TfNode[] = [];
|
|
128
|
+
const edges: TfEdge[] = [];
|
|
129
|
+
for (let l = 0; l < layers; l++) {
|
|
130
|
+
for (let i = 0; i < width; i++) {
|
|
131
|
+
nodes.push(node(`aws_sqs_queue.l${l}_${i}`));
|
|
132
|
+
if (l > 0) {
|
|
133
|
+
edges.push(edge(`aws_sqs_queue.l${l}_${i}`, `aws_sqs_queue.l${l - 1}_${(2 * i) % width}`));
|
|
134
|
+
edges.push(edge(`aws_sqs_queue.l${l}_${i}`, `aws_sqs_queue.l${l - 1}_${(2 * i + 1) % width}`));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
reach({ nodes, edges }, "aws_sqs_queue.l0_0"); // warm the JIT on a different graph object
|
|
139
|
+
const graph: TfGraph = { nodes, edges };
|
|
140
|
+
const started = performance.now();
|
|
141
|
+
const r = reach(graph, "aws_sqs_queue.l0_0");
|
|
142
|
+
const took = performance.now() - started;
|
|
143
|
+
expect(r.resources.length).toBeGreaterThan(5_000);
|
|
144
|
+
expect(took).toBeLessThan(100);
|
|
145
|
+
});
|
|
146
|
+
});
|