infraweaver 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +244 -0
- package/dist/agents/claude.d.ts +113 -0
- package/dist/agents/claudePretoolGate.d.ts +137 -0
- package/dist/agents/dispatchMetrics.d.ts +77 -0
- package/dist/agents/gateServer.d.ts +7 -0
- package/dist/agents/index.d.ts +6 -0
- package/dist/agents/nativeFsDenies.d.ts +46 -0
- package/dist/agents/opencode.d.ts +284 -0
- package/dist/agents/opencodePlugin.d.ts +96 -0
- package/dist/agents/opencodeShared.d.ts +40 -0
- package/dist/agents/postRun.d.ts +126 -0
- package/dist/agents/reviewer.d.ts +41 -0
- package/dist/agents/sessionLabeler.d.ts +97 -0
- package/dist/agents/shared.d.ts +246 -0
- package/dist/agents/subagentModels.d.ts +19 -0
- package/dist/agents/tokenQuota.d.ts +118 -0
- package/dist/agents/writeGateSource.d.ts +22 -0
- package/dist/agents/writePolicy.d.ts +79 -0
- package/dist/brand.d.ts +67 -0
- package/dist/cli.mjs +247846 -0
- package/dist/external.d.ts +227 -0
- package/dist/i18n/scaffolding.d.ts +59 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +247061 -0
- package/dist/internal/index.d.ts +18 -0
- package/dist/internal.js +2398 -0
- package/dist/lifecycle.d.ts +2 -0
- package/dist/main.d.ts +8 -0
- package/dist/mcp/arkConfig.d.ts +1 -0
- package/dist/mcp/assess.d.ts +157 -0
- package/dist/mcp/capabilityContext.d.ts +71 -0
- package/dist/mcp/changeSummary.d.ts +52 -0
- package/dist/mcp/checkSuite.d.ts +27 -0
- package/dist/mcp/checkout.d.ts +92 -0
- package/dist/mcp/comment.d.ts +127 -0
- package/dist/mcp/commitInfo.d.ts +11 -0
- package/dist/mcp/crosswalk.d.ts +178 -0
- package/dist/mcp/crosswalkDigest.d.ts +1 -0
- package/dist/mcp/cyberEssentials.d.ts +24 -0
- package/dist/mcp/dashboard.d.ts +125 -0
- package/dist/mcp/dependencies.d.ts +12 -0
- package/dist/mcp/frameworks.d.ts +74 -0
- package/dist/mcp/geminiSanitizer.d.ts +28 -0
- package/dist/mcp/git.d.ts +60 -0
- package/dist/mcp/guardrails.d.ts +222 -0
- package/dist/mcp/issue.d.ts +20 -0
- package/dist/mcp/issueComments.d.ts +11 -0
- package/dist/mcp/issueEvents.d.ts +11 -0
- package/dist/mcp/issueInfo.d.ts +11 -0
- package/dist/mcp/labels.d.ts +14 -0
- package/dist/mcp/localContext.d.ts +26 -0
- package/dist/mcp/moduleExtraction.d.ts +79 -0
- package/dist/mcp/moduleTests.d.ts +106 -0
- package/dist/mcp/modules.d.ts +198 -0
- package/dist/mcp/output.d.ts +16 -0
- package/dist/mcp/pathSafety.d.ts +14 -0
- package/dist/mcp/policy.d.ts +50 -0
- package/dist/mcp/pr.d.ts +64 -0
- package/dist/mcp/prInfo.d.ts +11 -0
- package/dist/mcp/providerSchema.d.ts +52 -0
- package/dist/mcp/review.d.ts +212 -0
- package/dist/mcp/reviewComments.d.ts +245 -0
- package/dist/mcp/roots.d.ts +60 -0
- package/dist/mcp/scope.d.ts +26 -0
- package/dist/mcp/selectMode.d.ts +20 -0
- package/dist/mcp/server.d.ts +61 -0
- package/dist/mcp/shared.d.ts +62 -0
- package/dist/mcp/shell.d.ts +58 -0
- package/dist/mcp/staleFix.d.ts +138 -0
- package/dist/mcp/terraform/azurePrices.d.ts +8 -0
- package/dist/mcp/terraform/baseline.d.ts +57 -0
- package/dist/mcp/terraform/concernResult.d.ts +38 -0
- package/dist/mcp/terraform/cost.d.ts +55 -0
- package/dist/mcp/terraform/cspm.d.ts +80 -0
- package/dist/mcp/terraform/currency.d.ts +110 -0
- package/dist/mcp/terraform/decisions.d.ts +178 -0
- package/dist/mcp/terraform/eidas.d.ts +100 -0
- package/dist/mcp/terraform/evidence.d.ts +343 -0
- package/dist/mcp/terraform/findings.d.ts +76 -0
- package/dist/mcp/terraform/fixMemory.d.ts +81 -0
- package/dist/mcp/terraform/governance.d.ts +56 -0
- package/dist/mcp/terraform/hcl.d.ts +115 -0
- package/dist/mcp/terraform/iacLanguages.d.ts +120 -0
- package/dist/mcp/terraform/idleFloor.d.ts +78 -0
- package/dist/mcp/terraform/keyless.d.ts +68 -0
- package/dist/mcp/terraform/moduleDocs.d.ts +56 -0
- package/dist/mcp/terraform/nativeRules.d.ts +38 -0
- package/dist/mcp/terraform/nativeScan.d.ts +11 -0
- package/dist/mcp/terraform/noiseProfile.d.ts +100 -0
- package/dist/mcp/terraform/normalization.d.ts +52 -0
- package/dist/mcp/terraform/oscal.d.ts +103 -0
- package/dist/mcp/terraform/packs/azurerm-3-4.d.ts +2 -0
- package/dist/mcp/terraform/packs/cloudflare-4-5.d.ts +2 -0
- package/dist/mcp/terraform/paths.d.ts +28 -0
- package/dist/mcp/terraform/plan.d.ts +157 -0
- package/dist/mcp/terraform/planDrift.d.ts +199 -0
- package/dist/mcp/terraform/policyAuthor.d.ts +191 -0
- package/dist/mcp/terraform/prowlerOcsf.d.ts +136 -0
- package/dist/mcp/terraform/refactor.d.ts +183 -0
- package/dist/mcp/terraform/registry.d.ts +108 -0
- package/dist/mcp/terraform/risk.d.ts +41 -0
- package/dist/mcp/terraform/scanSession.d.ts +116 -0
- package/dist/mcp/terraform/scannerCache.d.ts +15 -0
- package/dist/mcp/terraform/scannerEgress.d.ts +72 -0
- package/dist/mcp/terraform/scannerJson.d.ts +13 -0
- package/dist/mcp/terraform/scanners.d.ts +205 -0
- package/dist/mcp/terraform/signing.d.ts +61 -0
- package/dist/mcp/terraform/stateLocking.d.ts +18 -0
- package/dist/mcp/terraform/subprocess.d.ts +43 -0
- package/dist/mcp/terraform/suppressions.d.ts +90 -0
- package/dist/mcp/terraform/taxonomy.d.ts +39 -0
- package/dist/mcp/terraform/tfquery.d.ts +47 -0
- package/dist/mcp/terraform/toolchainIntegrity.d.ts +82 -0
- package/dist/mcp/terraform/tools/authorPolicy.d.ts +59 -0
- package/dist/mcp/terraform/tools/detectPlanDrift.d.ts +20 -0
- package/dist/mcp/terraform/tools/emitCost.d.ts +11 -0
- package/dist/mcp/terraform/tools/emitSarif.d.ts +14 -0
- package/dist/mcp/terraform/tools/fixMemory.d.ts +19 -0
- package/dist/mcp/terraform/tools/infracostDiff.d.ts +22 -0
- package/dist/mcp/terraform/tools/ingestExternalFindings.d.ts +21 -0
- package/dist/mcp/terraform/tools/moduleLookup.d.ts +14 -0
- package/dist/mcp/terraform/tools/moduleSearch.d.ts +23 -0
- package/dist/mcp/terraform/tools/plan.d.ts +59 -0
- package/dist/mcp/terraform/tools/providerUpgrade.d.ts +55 -0
- package/dist/mcp/terraform/tools/readFindings.d.ts +17 -0
- package/dist/mcp/terraform/tools/scan.d.ts +114 -0
- package/dist/mcp/terraform/tools/validate.d.ts +31 -0
- package/dist/mcp/terraform/tools/verifyRemediation.d.ts +31 -0
- package/dist/mcp/terraform/tools/versionCurrency.d.ts +5 -0
- package/dist/mcp/terraform/tools.d.ts +16 -0
- package/dist/mcp/terraform/transformPacks.d.ts +267 -0
- package/dist/mcp/terraform/types.d.ts +226 -0
- package/dist/mcp/terraform/verification.d.ts +81 -0
- package/dist/mcp/terraform/vex.d.ts +115 -0
- package/dist/mcp/terraform/writeOnly.d.ts +67 -0
- package/dist/mcp/terraform.d.ts +51 -0
- package/dist/mcp/terratest.d.ts +85 -0
- package/dist/mcp/untrustedContent.d.ts +76 -0
- package/dist/mcp/upload.d.ts +8 -0
- package/dist/models.d.ts +181 -0
- package/dist/modes/address-reviews.d.ts +2 -0
- package/dist/modes/assess.d.ts +2 -0
- package/dist/modes/build.d.ts +2 -0
- package/dist/modes/compliance-audit.d.ts +2 -0
- package/dist/modes/cost-optimization.d.ts +2 -0
- package/dist/modes/drift-detect.d.ts +2 -0
- package/dist/modes/fix.d.ts +2 -0
- package/dist/modes/incremental-review.d.ts +2 -0
- package/dist/modes/index.d.ts +24 -0
- package/dist/modes/modernize-deprecated.d.ts +2 -0
- package/dist/modes/plan.d.ts +2 -0
- package/dist/modes/policy-gate.d.ts +2 -0
- package/dist/modes/prFormats.d.ts +3 -0
- package/dist/modes/refactor.d.ts +2 -0
- package/dist/modes/refresh-remediation.d.ts +2 -0
- package/dist/modes/remediate-and-refactor.d.ts +2 -0
- package/dist/modes/remediate.d.ts +2 -0
- package/dist/modes/resolve-conflicts.d.ts +2 -0
- package/dist/modes/review.d.ts +2 -0
- package/dist/modes/summarize-pr.d.ts +2 -0
- package/dist/modes/task.d.ts +2 -0
- package/dist/modes/terraform-code-review.d.ts +2 -0
- package/dist/modes/types.d.ts +9 -0
- package/dist/modes/update-dependencies.d.ts +2 -0
- package/dist/prep/index.d.ts +7 -0
- package/dist/prep/installNodeDependencies.d.ts +2 -0
- package/dist/prep/installPythonDependencies.d.ts +2 -0
- package/dist/prep/types.d.ts +31 -0
- package/dist/reviewQuality.d.ts +107 -0
- package/dist/skills/terraform-best-practices/SKILL.md +379 -0
- package/dist/toolState.d.ts +123 -0
- package/dist/utils/activity.d.ts +40 -0
- package/dist/utils/agent.d.ts +47 -0
- package/dist/utils/agentHangReport.d.ts +38 -0
- package/dist/utils/annotations.d.ts +17 -0
- package/dist/utils/apiFetch.d.ts +11 -0
- package/dist/utils/apiKeys.d.ts +48 -0
- package/dist/utils/apiUrl.d.ts +28 -0
- package/dist/utils/assets.d.ts +8 -0
- package/dist/utils/baseRefConfig.d.ts +121 -0
- package/dist/utils/billingErrors.d.ts +85 -0
- package/dist/utils/body.d.ts +34 -0
- package/dist/utils/buildInfraweaverFooter.d.ts +29 -0
- package/dist/utils/byokFallback.d.ts +85 -0
- package/dist/utils/changeImpact.d.ts +101 -0
- package/dist/utils/claudeSubscription.d.ts +30 -0
- package/dist/utils/cli.d.ts +10 -0
- package/dist/utils/cloudClient.d.ts +33 -0
- package/dist/utils/cloudFetchCore.d.ts +42 -0
- package/dist/utils/cloudReport.d.ts +114 -0
- package/dist/utils/codexHome.d.ts +29 -0
- package/dist/utils/codexOAuth.d.ts +60 -0
- package/dist/utils/diffCoverage.d.ts +63 -0
- package/dist/utils/env.d.ts +46 -0
- package/dist/utils/errorReport.d.ts +17 -0
- package/dist/utils/exitHandler.d.ts +8 -0
- package/dist/utils/fixDoubleEscapedString.d.ts +1 -0
- package/dist/utils/gitAuth.d.ts +84 -0
- package/dist/utils/gitAuthServer.d.ts +24 -0
- package/dist/utils/github.d.ts +104 -0
- package/dist/utils/globals.d.ts +3 -0
- package/dist/utils/infraweaverConfig.d.ts +56 -0
- package/dist/utils/install.d.ts +37 -0
- package/dist/utils/instructions.d.ts +48 -0
- package/dist/utils/keylessOidc.d.ts +28 -0
- package/dist/utils/leapingComment.d.ts +11 -0
- package/dist/utils/learnings.d.ts +62 -0
- package/dist/utils/learningsTruncate.d.ts +25 -0
- package/dist/utils/lifecycle.d.ts +57 -0
- package/dist/utils/log.d.ts +111 -0
- package/dist/utils/modelResolver.d.ts +63 -0
- package/dist/utils/moduleFetch.d.ts +39 -0
- package/dist/utils/normalizeEnv.d.ts +30 -0
- package/dist/utils/openCodeModels.d.ts +11 -0
- package/dist/utils/openaiCompatible.d.ts +26 -0
- package/dist/utils/opentofuMcp.d.ts +50 -0
- package/dist/utils/overrides.d.ts +40 -0
- package/dist/utils/packageManager.d.ts +49 -0
- package/dist/utils/patchWorkflowRunFields.d.ts +29 -0
- package/dist/utils/payload.d.ts +249 -0
- package/dist/utils/prSummary.d.ts +61 -0
- package/dist/utils/presets.d.ts +36 -0
- package/dist/utils/progressComment.d.ts +146 -0
- package/dist/utils/providerErrors.d.ts +31 -0
- package/dist/utils/proxyModel.d.ts +76 -0
- package/dist/utils/rangeDiff.d.ts +51 -0
- package/dist/utils/redact.d.ts +33 -0
- package/dist/utils/registryAuth.d.ts +52 -0
- package/dist/utils/remediationCommand.d.ts +58 -0
- package/dist/utils/retry.d.ts +38 -0
- package/dist/utils/reviewCleanup.d.ts +14 -0
- package/dist/utils/run.d.ts +9 -0
- package/dist/utils/runContext.d.ts +112 -0
- package/dist/utils/runContextData.d.ts +42 -0
- package/dist/utils/runErrorRenderer.d.ts +64 -0
- package/dist/utils/runLifecycle.d.ts +89 -0
- package/dist/utils/runStartupLog.d.ts +15 -0
- package/dist/utils/secrets.d.ts +34 -0
- package/dist/utils/setup.d.ts +90 -0
- package/dist/utils/shell.d.ts +44 -0
- package/dist/utils/skills.d.ts +10 -0
- package/dist/utils/subprocess.d.ts +81 -0
- package/dist/utils/terraformMcp.d.ts +46 -0
- package/dist/utils/time.d.ts +15 -0
- package/dist/utils/timer.d.ts +23 -0
- package/dist/utils/todoTracking.d.ts +16 -0
- package/dist/utils/token.d.ts +52 -0
- package/dist/utils/toolLicensing.d.ts +56 -0
- package/dist/utils/toolSelection.d.ts +73 -0
- package/dist/utils/toon.d.ts +16 -0
- package/dist/utils/version.d.ts +2 -0
- package/dist/utils/versioning.d.ts +7 -0
- package/dist/utils/vertex.d.ts +16 -0
- package/dist/utils/workflow.d.ts +13 -0
- package/package.json +132 -0
- package/src/agents/claude.ts +1588 -0
- package/src/agents/claudePretoolGate.ts +286 -0
- package/src/agents/dispatchMetrics.ts +162 -0
- package/src/agents/gateServer.ts +178 -0
- package/src/agents/index.ts +10 -0
- package/src/agents/nativeFsDenies.ts +164 -0
- package/src/agents/opencode.ts +1600 -0
- package/src/agents/opencodePlugin.ts +304 -0
- package/src/agents/opencodeShared.ts +134 -0
- package/src/agents/postRun.ts +615 -0
- package/src/agents/reviewer.ts +134 -0
- package/src/agents/sessionLabeler.ts +185 -0
- package/src/agents/shared.ts +377 -0
- package/src/agents/subagentModels.ts +40 -0
- package/src/agents/tokenQuota.ts +203 -0
- package/src/agents/writeGateSource.ts +148 -0
- package/src/agents/writePolicy.ts +132 -0
- package/src/brand.ts +84 -0
- package/src/cli.ts +117 -0
- package/src/commands/gha.ts +188 -0
- package/src/commands/mcp.ts +126 -0
- package/src/commands/verifyEvidence.ts +100 -0
- package/src/entry.ts +7 -0
- package/src/entryPost.ts +109 -0
- package/src/external.ts +308 -0
- package/src/i18n/scaffolding.ts +191 -0
- package/src/index.ts +7 -0
- package/src/internal/index.ts +74 -0
- package/src/lifecycle.ts +2 -0
- package/src/main.ts +815 -0
- package/src/mcp/__fixtures__/infraweaver-scratch-pr-49-review-3485940013.json +110 -0
- package/src/mcp/__fixtures__/infraweaver-scratch-pr-64-review-3531000326.json +14 -0
- package/src/mcp/__fixtures__/infraweaver-test-repo-pr-1.diff.json +67 -0
- package/src/mcp/__snapshots__/checkout.test.ts.snap +109 -0
- package/src/mcp/__snapshots__/reviewComments.test.ts.snap +71 -0
- package/src/mcp/arkConfig.ts +7 -0
- package/src/mcp/assess.ts +723 -0
- package/src/mcp/capabilityContext.ts +80 -0
- package/src/mcp/changeSummary.ts +145 -0
- package/src/mcp/checkSuite.ts +261 -0
- package/src/mcp/checkout.ts +1015 -0
- package/src/mcp/comment.ts +683 -0
- package/src/mcp/commitInfo.ts +57 -0
- package/src/mcp/crosswalk.ts +1253 -0
- package/src/mcp/crosswalkDigest.ts +5 -0
- package/src/mcp/cyberEssentials.ts +60 -0
- package/src/mcp/dashboard.ts +232 -0
- package/src/mcp/dependencies.ts +190 -0
- package/src/mcp/frameworks.ts +236 -0
- package/src/mcp/geminiSanitizer.ts +212 -0
- package/src/mcp/git.ts +1116 -0
- package/src/mcp/guardrails.ts +771 -0
- package/src/mcp/issue.ts +74 -0
- package/src/mcp/issueComments.ts +48 -0
- package/src/mcp/issueEvents.ts +100 -0
- package/src/mcp/issueInfo.ts +72 -0
- package/src/mcp/labels.ts +35 -0
- package/src/mcp/localContext.ts +65 -0
- package/src/mcp/localServer.ts +217 -0
- package/src/mcp/moduleExtraction.ts +368 -0
- package/src/mcp/moduleTests.ts +421 -0
- package/src/mcp/modules.ts +752 -0
- package/src/mcp/output.ts +71 -0
- package/src/mcp/pathSafety.ts +28 -0
- package/src/mcp/policy.ts +226 -0
- package/src/mcp/pr.ts +238 -0
- package/src/mcp/prInfo.ts +91 -0
- package/src/mcp/providerSchema.ts +175 -0
- package/src/mcp/review.ts +1081 -0
- package/src/mcp/reviewComments.ts +1137 -0
- package/src/mcp/roots.ts +217 -0
- package/src/mcp/scope.ts +82 -0
- package/src/mcp/selectMode.ts +207 -0
- package/src/mcp/server.ts +498 -0
- package/src/mcp/shared.ts +120 -0
- package/src/mcp/shell.ts +631 -0
- package/src/mcp/staleFix.ts +557 -0
- package/src/mcp/terraform/__snapshots__/moduleDocs.test.ts.snap +40 -0
- package/src/mcp/terraform/azurePrices.ts +38 -0
- package/src/mcp/terraform/baseline.ts +172 -0
- package/src/mcp/terraform/concernResult.ts +111 -0
- package/src/mcp/terraform/cost.ts +175 -0
- package/src/mcp/terraform/cspm.ts +326 -0
- package/src/mcp/terraform/currency.ts +354 -0
- package/src/mcp/terraform/decisions.ts +590 -0
- package/src/mcp/terraform/eidas.ts +134 -0
- package/src/mcp/terraform/evidence.ts +771 -0
- package/src/mcp/terraform/findings.ts +383 -0
- package/src/mcp/terraform/fixMemory.ts +253 -0
- package/src/mcp/terraform/governance.ts +231 -0
- package/src/mcp/terraform/hcl.ts +273 -0
- package/src/mcp/terraform/iacLanguages.ts +431 -0
- package/src/mcp/terraform/idleFloor.ts +304 -0
- package/src/mcp/terraform/keyless.ts +123 -0
- package/src/mcp/terraform/moduleDocs.ts +303 -0
- package/src/mcp/terraform/nativeRules.ts +152 -0
- package/src/mcp/terraform/nativeScan.ts +67 -0
- package/src/mcp/terraform/noiseProfile.ts +183 -0
- package/src/mcp/terraform/normalization.ts +265 -0
- package/src/mcp/terraform/oscal.ts +283 -0
- package/src/mcp/terraform/packs/azurerm-3-4.ts +382 -0
- package/src/mcp/terraform/packs/cloudflare-4-5.ts +114 -0
- package/src/mcp/terraform/paths.ts +59 -0
- package/src/mcp/terraform/plan.ts +377 -0
- package/src/mcp/terraform/planDrift.ts +520 -0
- package/src/mcp/terraform/policyAuthor.ts +886 -0
- package/src/mcp/terraform/prowlerOcsf.ts +380 -0
- package/src/mcp/terraform/refactor.ts +879 -0
- package/src/mcp/terraform/registry.ts +317 -0
- package/src/mcp/terraform/risk.ts +99 -0
- package/src/mcp/terraform/scanSession.ts +242 -0
- package/src/mcp/terraform/scannerCache.ts +86 -0
- package/src/mcp/terraform/scannerEgress.ts +141 -0
- package/src/mcp/terraform/scannerJson.ts +22 -0
- package/src/mcp/terraform/scanners.ts +1134 -0
- package/src/mcp/terraform/signing.ts +151 -0
- package/src/mcp/terraform/stateLocking.ts +106 -0
- package/src/mcp/terraform/subprocess.ts +102 -0
- package/src/mcp/terraform/suppressions.ts +551 -0
- package/src/mcp/terraform/taxonomy.ts +0 -0
- package/src/mcp/terraform/tfquery.ts +159 -0
- package/src/mcp/terraform/toolchainIntegrity.ts +151 -0
- package/src/mcp/terraform/tools/authorPolicy.ts +185 -0
- package/src/mcp/terraform/tools/detectPlanDrift.ts +279 -0
- package/src/mcp/terraform/tools/emitCost.ts +130 -0
- package/src/mcp/terraform/tools/emitSarif.ts +108 -0
- package/src/mcp/terraform/tools/fixMemory.ts +82 -0
- package/src/mcp/terraform/tools/infracostDiff.ts +110 -0
- package/src/mcp/terraform/tools/ingestExternalFindings.ts +259 -0
- package/src/mcp/terraform/tools/moduleLookup.ts +86 -0
- package/src/mcp/terraform/tools/moduleSearch.ts +46 -0
- package/src/mcp/terraform/tools/plan.ts +384 -0
- package/src/mcp/terraform/tools/providerUpgrade.ts +217 -0
- package/src/mcp/terraform/tools/readFindings.ts +138 -0
- package/src/mcp/terraform/tools/scan.ts +303 -0
- package/src/mcp/terraform/tools/validate.ts +160 -0
- package/src/mcp/terraform/tools/verifyRemediation.ts +190 -0
- package/src/mcp/terraform/tools/versionCurrency.ts +64 -0
- package/src/mcp/terraform/tools.ts +62 -0
- package/src/mcp/terraform/transformPacks.ts +594 -0
- package/src/mcp/terraform/types.ts +382 -0
- package/src/mcp/terraform/verification.ts +150 -0
- package/src/mcp/terraform/vex.ts +367 -0
- package/src/mcp/terraform/writeOnly.ts +252 -0
- package/src/mcp/terraform.ts +52 -0
- package/src/mcp/terratest.ts +215 -0
- package/src/mcp/untrustedContent.ts +126 -0
- package/src/mcp/upload.ts +123 -0
- package/src/models.ts +753 -0
- package/src/modes/address-reviews.ts +35 -0
- package/src/modes/assess.ts +27 -0
- package/src/modes/build.ts +86 -0
- package/src/modes/compliance-audit.ts +25 -0
- package/src/modes/cost-optimization.ts +36 -0
- package/src/modes/drift-detect.ts +31 -0
- package/src/modes/fix.ts +29 -0
- package/src/modes/incremental-review.ts +103 -0
- package/src/modes/index.ts +94 -0
- package/src/modes/modernize-deprecated.ts +36 -0
- package/src/modes/plan.ts +21 -0
- package/src/modes/policy-gate.ts +27 -0
- package/src/modes/prFormats.ts +325 -0
- package/src/modes/refactor.ts +74 -0
- package/src/modes/refresh-remediation.ts +40 -0
- package/src/modes/remediate-and-refactor.ts +42 -0
- package/src/modes/remediate.ts +78 -0
- package/src/modes/resolve-conflicts.ts +32 -0
- package/src/modes/review.ts +135 -0
- package/src/modes/summarize-pr.ts +42 -0
- package/src/modes/task.ts +26 -0
- package/src/modes/terraform-code-review.ts +68 -0
- package/src/modes/types.ts +17 -0
- package/src/modes/update-dependencies.ts +44 -0
- package/src/prep/index.ts +96 -0
- package/src/prep/installNodeDependencies.ts +251 -0
- package/src/prep/installPythonDependencies.ts +235 -0
- package/src/prep/types.ts +38 -0
- package/src/reviewQuality.ts +237 -0
- package/src/runCli.ts +335 -0
- package/src/skills/terraform-best-practices/SKILL.md +379 -0
- package/src/toolState.ts +241 -0
- package/src/utils/activity.ts +210 -0
- package/src/utils/agent.ts +245 -0
- package/src/utils/agentHangReport.ts +180 -0
- package/src/utils/annotations.ts +56 -0
- package/src/utils/apiFetch.ts +19 -0
- package/src/utils/apiKeys.ts +305 -0
- package/src/utils/apiUrl.ts +42 -0
- package/src/utils/assets.ts +123 -0
- package/src/utils/baseRefConfig.ts +254 -0
- package/src/utils/billingErrors.ts +210 -0
- package/src/utils/body.ts +168 -0
- package/src/utils/buildInfraweaverFooter.ts +104 -0
- package/src/utils/byokFallback.ts +137 -0
- package/src/utils/changeImpact.ts +330 -0
- package/src/utils/claudeSubscription.ts +93 -0
- package/src/utils/cli.ts +36 -0
- package/src/utils/cloudClient.ts +57 -0
- package/src/utils/cloudFetchCore.ts +139 -0
- package/src/utils/cloudReport.ts +316 -0
- package/src/utils/codexHome.ts +204 -0
- package/src/utils/codexOAuth.ts +154 -0
- package/src/utils/codexRefreshDetect.ts +36 -0
- package/src/utils/diffCoverage.ts +404 -0
- package/src/utils/env.ts +84 -0
- package/src/utils/errorReport.ts +94 -0
- package/src/utils/exitHandler.ts +35 -0
- package/src/utils/fixDoubleEscapedString.ts +9 -0
- package/src/utils/ghaCore.ts +13 -0
- package/src/utils/gitAuth.ts +268 -0
- package/src/utils/gitAuthServer.ts +186 -0
- package/src/utils/github.ts +676 -0
- package/src/utils/globals.ts +9 -0
- package/src/utils/humanEditCapture.ts +198 -0
- package/src/utils/infraweaverConfig.ts +202 -0
- package/src/utils/install.ts +279 -0
- package/src/utils/instructions.ts +640 -0
- package/src/utils/keylessOidc.ts +47 -0
- package/src/utils/leapingComment.ts +20 -0
- package/src/utils/learnings.ts +145 -0
- package/src/utils/learningsTruncate.ts +42 -0
- package/src/utils/lifecycle.ts +198 -0
- package/src/utils/log.ts +452 -0
- package/src/utils/modelResolver.ts +105 -0
- package/src/utils/moduleFetch.ts +138 -0
- package/src/utils/normalizeEnv.ts +106 -0
- package/src/utils/openCodeModels.ts +86 -0
- package/src/utils/openaiCompatible.ts +52 -0
- package/src/utils/opentofuMcp.ts +61 -0
- package/src/utils/overrides.ts +100 -0
- package/src/utils/packageManager.ts +257 -0
- package/src/utils/patchWorkflowRunFields.ts +173 -0
- package/src/utils/payload.ts +1206 -0
- package/src/utils/prSummary.ts +148 -0
- package/src/utils/presets.ts +105 -0
- package/src/utils/progressComment.ts +266 -0
- package/src/utils/providerErrors.ts +189 -0
- package/src/utils/proxyModel.ts +239 -0
- package/src/utils/rangeDiff.ts +182 -0
- package/src/utils/redact.ts +86 -0
- package/src/utils/registryAuth.ts +115 -0
- package/src/utils/remediationCommand.ts +144 -0
- package/src/utils/retry.ts +180 -0
- package/src/utils/reviewCleanup.ts +116 -0
- package/src/utils/run.ts +100 -0
- package/src/utils/runContext.ts +276 -0
- package/src/utils/runContextData.ts +121 -0
- package/src/utils/runErrorRenderer.ts +282 -0
- package/src/utils/runFixture.ts +91 -0
- package/src/utils/runLifecycle.ts +360 -0
- package/src/utils/runStartupLog.ts +67 -0
- package/src/utils/secrets.ts +208 -0
- package/src/utils/setup.ts +368 -0
- package/src/utils/shell.ts +132 -0
- package/src/utils/skills.ts +67 -0
- package/src/utils/subprocess.ts +474 -0
- package/src/utils/terraformMcp.ts +99 -0
- package/src/utils/time.ts +59 -0
- package/src/utils/timer.ts +72 -0
- package/src/utils/todoTracking.ts +168 -0
- package/src/utils/token.ts +294 -0
- package/src/utils/toolLicensing.ts +152 -0
- package/src/utils/toolSelection.ts +239 -0
- package/src/utils/toon.ts +76 -0
- package/src/utils/version.ts +10 -0
- package/src/utils/versioning.ts +44 -0
- package/src/utils/vertex.ts +94 -0
- package/src/utils/workflow.ts +25 -0
|
@@ -0,0 +1,879 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Refactor equivalence proof (Phase A — the "safe floor").
|
|
3
|
+
*
|
|
4
|
+
* The `refactor` verb relocates resources behind a module boundary and asserts
|
|
5
|
+
* behaviour is unchanged. The PROOF that backs that assertion has two layers:
|
|
6
|
+
*
|
|
7
|
+
* - the keys-free STATIC proof in this file (no cloud creds): same managed
|
|
8
|
+
* resource set, identical per-resource argument names, and — the highest-risk
|
|
9
|
+
* invariant — EVERY relocated address covered by a `moved {}` block, plus
|
|
10
|
+
* `terraform validate` and `terraform fmt -check` clean. `computeEquivalence`
|
|
11
|
+
* hard-fails on a single uncovered move: a missed `moved {}` is a silent
|
|
12
|
+
* destroy/create on live infrastructure (a sin of commission), so the gate
|
|
13
|
+
* must refuse rather than warn.
|
|
14
|
+
* - the OPTIONAL stronger proof (cloud creds present): a real `terraform plan`
|
|
15
|
+
* that is a pure-move plan (`isPureMovePlan` in plan.ts). That is corroboration
|
|
16
|
+
* layered on top of this floor, never a substitute for it.
|
|
17
|
+
*
|
|
18
|
+
* Everything here is pure parsing + set arithmetic over file contents — the tool
|
|
19
|
+
* wrapper supplies the before-tree (via `git show <base>:<path>`) and the
|
|
20
|
+
* after-tree (the working tree on disk), mirroring moduleExtraction.ts.
|
|
21
|
+
*
|
|
22
|
+
* `moved {}` semantics anchor:
|
|
23
|
+
* https://developer.hashicorp.com/terraform/language/modules/develop/refactoring
|
|
24
|
+
* moved { from = aws_instance.example to = module.ec2.aws_instance.example }
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { readFileSync } from "node:fs";
|
|
28
|
+
import { join, posix } from "node:path";
|
|
29
|
+
import { type } from "arktype";
|
|
30
|
+
import { PRODUCT_NAME } from "#app/brand";
|
|
31
|
+
import type { LocalToolContext } from "#app/mcp/localContext";
|
|
32
|
+
import {
|
|
33
|
+
type ModuleSourceKind,
|
|
34
|
+
parseModuleBlocks,
|
|
35
|
+
walkTfFiles,
|
|
36
|
+
} from "#app/mcp/modules";
|
|
37
|
+
import { execute, tool, toolOk, toolSkip } from "#app/mcp/shared";
|
|
38
|
+
import { scanSessionOf } from "#app/mcp/terraform/scanSession";
|
|
39
|
+
import { scanFmt, scanValidate } from "#app/mcp/terraform/scanners";
|
|
40
|
+
import { run } from "#app/mcp/terraform/subprocess";
|
|
41
|
+
import { log } from "#app/utils/cli";
|
|
42
|
+
import { resolveModuleCredentialEnv } from "#app/utils/registryAuth";
|
|
43
|
+
|
|
44
|
+
// --- resource parsing with argument names (pure) -----------------------------
|
|
45
|
+
|
|
46
|
+
export interface ResourceRef {
|
|
47
|
+
/** full Terraform address, module-prefixed: `module.x.aws_s3_bucket.logs`. */
|
|
48
|
+
address: string;
|
|
49
|
+
/** the resource's logical identity, module-path-independent: `aws_s3_bucket.logs`. */
|
|
50
|
+
key: string;
|
|
51
|
+
type: string;
|
|
52
|
+
name: string;
|
|
53
|
+
/** sorted top-level argument names (attributes + nested block labels). */
|
|
54
|
+
argNames: string[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface MovedBlock {
|
|
58
|
+
from: string;
|
|
59
|
+
to: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface SourceFile {
|
|
63
|
+
/** repo-relative path, posix-separated. */
|
|
64
|
+
path: string;
|
|
65
|
+
hcl: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Meta-arguments are valid on every resource and never describe behaviour we
|
|
69
|
+
// compare for equivalence; mirrors RESOURCE_META_ARGUMENTS in scanners.ts.
|
|
70
|
+
const META_ARGUMENTS: ReadonlySet<string> = new Set([
|
|
71
|
+
"count",
|
|
72
|
+
"for_each",
|
|
73
|
+
"provider",
|
|
74
|
+
"depends_on",
|
|
75
|
+
"lifecycle",
|
|
76
|
+
"provisioner",
|
|
77
|
+
"connection",
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
/** strip a `"…"` string, `#`/`//` comment, or `<<EOF` heredoc starting at `i`;
|
|
81
|
+
* returns the index of the span's last char, or null when `i` isn't a span start.
|
|
82
|
+
* A compact local copy of scanners.ts's skipNonCode (not exported there) so the
|
|
83
|
+
* brace/argument scan can't be fooled by an interpolation brace or a commented `}`. */
|
|
84
|
+
function skipNonCode(s: string, i: number): number | null {
|
|
85
|
+
const ch = s[i];
|
|
86
|
+
if (ch === '"') {
|
|
87
|
+
let j = i + 1;
|
|
88
|
+
while (j < s.length && s[j] !== '"') {
|
|
89
|
+
if (s[j] === "\\") j++;
|
|
90
|
+
j++;
|
|
91
|
+
}
|
|
92
|
+
return j;
|
|
93
|
+
}
|
|
94
|
+
if (ch === "<" && s[i + 1] === "<") {
|
|
95
|
+
const m = /^<<[-~]?([A-Za-z_][A-Za-z0-9_]*)/.exec(s.slice(i, i + 80));
|
|
96
|
+
if (m) {
|
|
97
|
+
const openNl = s.indexOf("\n", i);
|
|
98
|
+
if (openNl === -1) return s.length - 1;
|
|
99
|
+
const cm = new RegExp(`\\n[ \\t]*${m[1]}[ \\t]*(?=\\n|$)`).exec(
|
|
100
|
+
s.slice(openNl),
|
|
101
|
+
);
|
|
102
|
+
return cm ? openNl + cm.index + cm[0].length - 1 : s.length - 1;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (ch === "#" || (ch === "/" && s[i + 1] === "/")) {
|
|
106
|
+
const nl = s.indexOf("\n", i);
|
|
107
|
+
return nl === -1 ? s.length - 1 : nl;
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** brace-match the body opened at `open`, string/comment/heredoc-aware. */
|
|
113
|
+
function matchBraceBody(
|
|
114
|
+
hcl: string,
|
|
115
|
+
open: number,
|
|
116
|
+
): { text: string; end: number } | null {
|
|
117
|
+
let depth = 0;
|
|
118
|
+
for (let i = open; i < hcl.length; i++) {
|
|
119
|
+
const span = skipNonCode(hcl, i);
|
|
120
|
+
if (span !== null) {
|
|
121
|
+
i = span;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const ch = hcl[i];
|
|
125
|
+
if (ch === "{") depth++;
|
|
126
|
+
else if (ch === "}") {
|
|
127
|
+
depth--;
|
|
128
|
+
if (depth === 0) return { text: hcl.slice(open + 1, i), end: i };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** depth-0 argument names of a resource body (attributes `name =`, nested blocks
|
|
135
|
+
* `name {`, and a `dynamic "x"` block's generated label `x`), excluding
|
|
136
|
+
* meta-arguments. String/comment/heredoc-aware so an interpolation or comment
|
|
137
|
+
* can't fabricate a name. Returned sorted for stable comparison. */
|
|
138
|
+
function topLevelArgNames(body: string): string[] {
|
|
139
|
+
const names = new Set<string>();
|
|
140
|
+
let depth = 0;
|
|
141
|
+
let atStmtStart = true;
|
|
142
|
+
for (let i = 0; i < body.length; i++) {
|
|
143
|
+
const span = skipNonCode(body, i);
|
|
144
|
+
if (span !== null) {
|
|
145
|
+
// a comment/heredoc ends the line; a string does not. Approximate by
|
|
146
|
+
// checking whether the span consumed a newline.
|
|
147
|
+
atStmtStart = body.slice(i, span + 1).includes("\n");
|
|
148
|
+
i = span;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const ch = body[i]!;
|
|
152
|
+
if (ch === "{") {
|
|
153
|
+
depth++;
|
|
154
|
+
atStmtStart = false;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (ch === "}") {
|
|
158
|
+
depth--;
|
|
159
|
+
atStmtStart = false;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (ch === "\n") {
|
|
163
|
+
atStmtStart = true;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (ch === " " || ch === "\t" || ch === "\r") continue;
|
|
167
|
+
if (depth === 0 && atStmtStart && /[A-Za-z_]/.test(ch)) {
|
|
168
|
+
let j = i;
|
|
169
|
+
while (j < body.length && /[A-Za-z0-9_-]/.test(body[j]!)) j++;
|
|
170
|
+
const ident = body.slice(i, j);
|
|
171
|
+
let k = j;
|
|
172
|
+
while (k < body.length && (body[k] === " " || body[k] === "\t")) k++;
|
|
173
|
+
const next = body[k];
|
|
174
|
+
if (next === "=" && body[k + 1] !== "=") {
|
|
175
|
+
if (!META_ARGUMENTS.has(ident)) names.add(ident);
|
|
176
|
+
} else if (next === "{") {
|
|
177
|
+
if (!META_ARGUMENTS.has(ident)) names.add(ident);
|
|
178
|
+
} else if (next === '"' && ident === "dynamic") {
|
|
179
|
+
const labelEnd = body.indexOf('"', k + 1);
|
|
180
|
+
if (labelEnd !== -1) names.add(body.slice(k + 1, labelEnd));
|
|
181
|
+
}
|
|
182
|
+
i = j - 1;
|
|
183
|
+
atStmtStart = false;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
atStmtStart = false;
|
|
187
|
+
}
|
|
188
|
+
return [...names].sort();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** parse every `resource "<type>" "<name>" { … }` block's type, name, and
|
|
192
|
+
* top-level argument names from one file. */
|
|
193
|
+
function parseResourcesWithArgs(
|
|
194
|
+
hcl: string,
|
|
195
|
+
): { type: string; name: string; argNames: string[] }[] {
|
|
196
|
+
const out: { type: string; name: string; argNames: string[] }[] = [];
|
|
197
|
+
const re = /(?:^|\n)\s*resource\s+"([^"]+)"\s+"([^"]+)"\s*\{/g;
|
|
198
|
+
let m: RegExpExecArray | null;
|
|
199
|
+
// biome-ignore lint/suspicious/noAssignInExpressions: idiomatic regex-exec iteration
|
|
200
|
+
while ((m = re.exec(hcl)) !== null) {
|
|
201
|
+
const [, resourceType = "", name = ""] = m;
|
|
202
|
+
const braceStart = hcl.indexOf("{", m.index);
|
|
203
|
+
if (braceStart === -1) break;
|
|
204
|
+
const body = matchBraceBody(hcl, braceStart);
|
|
205
|
+
if (!body) break;
|
|
206
|
+
re.lastIndex = body.end + 1;
|
|
207
|
+
out.push({
|
|
208
|
+
type: resourceType,
|
|
209
|
+
name,
|
|
210
|
+
argNames: topLevelArgNames(body.text),
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// --- moved {} block parsing (pure) -------------------------------------------
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Parse every top-level `moved { from = <addr> to = <addr> }` block from HCL.
|
|
220
|
+
* Addresses are unquoted Terraform traversals (`module.x.aws_s3_bucket.logs`,
|
|
221
|
+
* `aws_instance.example[0]`), so the value is captured raw up to the line end or
|
|
222
|
+
* a trailing comment. Brace-matched so a stray `{}` in a comment can't fool it.
|
|
223
|
+
* A block missing either `from` or `to` is skipped (incomplete, not a relocation).
|
|
224
|
+
*/
|
|
225
|
+
export function parseMovedBlocks(hcl: string): MovedBlock[] {
|
|
226
|
+
const out: MovedBlock[] = [];
|
|
227
|
+
const re = /(?:^|\n)\s*moved\s*\{/g;
|
|
228
|
+
let m: RegExpExecArray | null;
|
|
229
|
+
// biome-ignore lint/suspicious/noAssignInExpressions: idiomatic regex-exec iteration
|
|
230
|
+
while ((m = re.exec(hcl)) !== null) {
|
|
231
|
+
const braceStart = hcl.indexOf("{", m.index);
|
|
232
|
+
if (braceStart === -1) break;
|
|
233
|
+
const body = matchBraceBody(hcl, braceStart);
|
|
234
|
+
if (!body) break;
|
|
235
|
+
re.lastIndex = body.end + 1;
|
|
236
|
+
// capture the address TOKEN after `from =` / `to =` — a run of address
|
|
237
|
+
// characters terminated by whitespace, `}`, or a comment. Token-based (not
|
|
238
|
+
// line-based) so a single-line `moved { from = a to = b }` parses as well as
|
|
239
|
+
// the multi-line form. `\bfrom\b`/`\bto\b` anchor on the keyword, not a
|
|
240
|
+
// substring of an address.
|
|
241
|
+
const from = body.text.match(/\bfrom\b\s*=\s*([^\s}#]+)/)?.[1];
|
|
242
|
+
const to = body.text.match(/\bto\b\s*=\s*([^\s}#]+)/)?.[1];
|
|
243
|
+
if (from && to)
|
|
244
|
+
out.push({ from: normaliseAddress(from), to: normaliseAddress(to) });
|
|
245
|
+
}
|
|
246
|
+
return out;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** collapse incidental whitespace so two spellings of the same address compare
|
|
250
|
+
* equal (`module.x . aws_s3_bucket.logs` is not valid HCL, but defensive). */
|
|
251
|
+
export function normaliseAddress(addr: string): string {
|
|
252
|
+
return addr
|
|
253
|
+
.trim()
|
|
254
|
+
.replace(/\s*\.\s*/g, ".")
|
|
255
|
+
.replace(/\s+/g, " ");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// --- module attribution (pure) -----------------------------------------------
|
|
259
|
+
|
|
260
|
+
/** the leaf `<type>.<name>` of an address, stripping leading `module.X.` hops and
|
|
261
|
+
* any `[index]`/`["key"]` instance suffix. `module.db.aws_db_instance.main[0]`
|
|
262
|
+
* → `aws_db_instance.main`. */
|
|
263
|
+
export function addressKey(address: string): string {
|
|
264
|
+
const noModules = address.replace(/^(?:module\.[^.]+\.)+/, "");
|
|
265
|
+
return noModules.replace(/\[[^\]]*\]$/, "");
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
interface ModuleCall {
|
|
269
|
+
/** repo-relative posix dir the local module's source resolves to. */
|
|
270
|
+
dir: string;
|
|
271
|
+
callName: string;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** resolve every `module "<name>" { source = "./local/path" }` call to the dir
|
|
275
|
+
* its `.tf` files live in (local sources only — registry/git modules have no
|
|
276
|
+
* in-tree files to attribute). */
|
|
277
|
+
function localModuleCalls(files: SourceFile[]): ModuleCall[] {
|
|
278
|
+
const calls: ModuleCall[] = [];
|
|
279
|
+
for (const f of files) {
|
|
280
|
+
for (const block of parseModuleBlocks(f.hcl)) {
|
|
281
|
+
if (block.kind !== "local") continue;
|
|
282
|
+
const dir = posix
|
|
283
|
+
.normalize(posix.join(posix.dirname(f.path), block.source))
|
|
284
|
+
.replace(/\/+$/, "");
|
|
285
|
+
calls.push({ dir, callName: block.name });
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return calls;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** true when `file` lives inside module `dir` (or a nested file under it). */
|
|
292
|
+
function fileInDir(file: string, dir: string): boolean {
|
|
293
|
+
return dir === "."
|
|
294
|
+
? !file.includes("/")
|
|
295
|
+
: file === dir || file.startsWith(`${dir}/`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Attribute every resource in a file tree to its full Terraform address.
|
|
300
|
+
* A resource in a file under a single-caller local module dir is prefixed
|
|
301
|
+
* `module.<caller>.`; a resource at root keeps its bare `<type>.<name>`. A module
|
|
302
|
+
* dir with MULTIPLE callers (instantiated more than once) or NESTED module dirs
|
|
303
|
+
* can't be attributed to one address deterministically — those resources are
|
|
304
|
+
* returned in `ambiguous` so the caller degrades honestly rather than asserting a
|
|
305
|
+
* false address. Pure.
|
|
306
|
+
*/
|
|
307
|
+
export function attributeAddresses(files: SourceFile[]): {
|
|
308
|
+
resources: ResourceRef[];
|
|
309
|
+
ambiguous: string[];
|
|
310
|
+
} {
|
|
311
|
+
const calls = localModuleCalls(files);
|
|
312
|
+
const callersByDir = new Map<string, string[]>();
|
|
313
|
+
for (const c of calls) {
|
|
314
|
+
callersByDir.set(c.dir, [...(callersByDir.get(c.dir) ?? []), c.callName]);
|
|
315
|
+
}
|
|
316
|
+
const resources: ResourceRef[] = [];
|
|
317
|
+
const ambiguous: string[] = [];
|
|
318
|
+
for (const f of files) {
|
|
319
|
+
// the longest module dir that contains this file (handles `modules/x` vs
|
|
320
|
+
// `modules/x/sub` — the deepest wins).
|
|
321
|
+
const owningDirs = [...callersByDir.keys()]
|
|
322
|
+
.filter((dir) => fileInDir(f.path, dir))
|
|
323
|
+
.sort((a, b) => b.length - a.length);
|
|
324
|
+
const owningDir = owningDirs[0];
|
|
325
|
+
for (const r of parseResourcesWithArgs(f.hcl)) {
|
|
326
|
+
const key = `${r.type}.${r.name}`;
|
|
327
|
+
if (!owningDir) {
|
|
328
|
+
resources.push({
|
|
329
|
+
address: key,
|
|
330
|
+
key,
|
|
331
|
+
type: r.type,
|
|
332
|
+
name: r.name,
|
|
333
|
+
argNames: r.argNames,
|
|
334
|
+
});
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
const callers = callersByDir.get(owningDir) ?? [];
|
|
338
|
+
// a nested module dir (file owned by a dir that is itself under another
|
|
339
|
+
// module dir) or a multi-instantiated module yields >1 viable prefix.
|
|
340
|
+
if (callers.length !== 1 || owningDirs.length > 1) {
|
|
341
|
+
ambiguous.push(`${owningDir}:${key}`);
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
const address = `module.${callers[0]}.${key}`;
|
|
345
|
+
resources.push({
|
|
346
|
+
address,
|
|
347
|
+
key,
|
|
348
|
+
type: r.type,
|
|
349
|
+
name: r.name,
|
|
350
|
+
argNames: r.argNames,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return { resources, ambiguous };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// --- the equivalence computation (pure) --------------------------------------
|
|
358
|
+
|
|
359
|
+
export interface EquivalenceResult {
|
|
360
|
+
/** the overall keys-free verdict: all sub-checks below passed. The push
|
|
361
|
+
* guardrail hard-blocks when this is false. */
|
|
362
|
+
equivalent: boolean;
|
|
363
|
+
/** same multiset of `<type>.<name>` across the whole tree before and after. */
|
|
364
|
+
resource_set_equivalent: boolean;
|
|
365
|
+
/** identical top-level argument names for every resource present in both. */
|
|
366
|
+
arg_names_equivalent: boolean;
|
|
367
|
+
/** every relocated address is covered by a `moved {}` block (zero uncovered).
|
|
368
|
+
* THE highest-risk invariant — a false here means a silent destroy/create. */
|
|
369
|
+
moves_exhaustive: boolean;
|
|
370
|
+
/** count of address relocations detected between before and after. */
|
|
371
|
+
moves_detected: number;
|
|
372
|
+
/** the relocations we detected (the authoritative move set). */
|
|
373
|
+
expected_moves: MovedBlock[];
|
|
374
|
+
/** relocations with NO matching `moved {}` block — the hard-fail set. */
|
|
375
|
+
uncovered_moves: MovedBlock[];
|
|
376
|
+
/** `moved {}` blocks that match no detected relocation (likely a typo'd
|
|
377
|
+
* `to`/`from` — surfaced, but not on its own a hard-fail). */
|
|
378
|
+
spurious_moved_blocks: MovedBlock[];
|
|
379
|
+
/** logical keys added / removed between before and after (must be empty for
|
|
380
|
+
* an equivalence-preserving refactor; a non-empty `added` is the b.4 "adopting
|
|
381
|
+
* a module adds a KMS key" signal — that's an ALTERING change, not equivalence). */
|
|
382
|
+
resource_set_diff: { added: string[]; removed: string[] };
|
|
383
|
+
/** per-resource argument-name drift. */
|
|
384
|
+
arg_diffs: { resource: string; added: string[]; removed: string[] }[];
|
|
385
|
+
/** addresses that couldn't be attributed deterministically (multi-instantiated
|
|
386
|
+
* or nested modules) — equivalence is reported but flagged incomplete. */
|
|
387
|
+
ambiguous: string[];
|
|
388
|
+
/** human-readable reasons the verdict is what it is. */
|
|
389
|
+
reasons: string[];
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** multiset of a list of keys → Map<key, count>. */
|
|
393
|
+
function multiset(keys: string[]): Map<string, number> {
|
|
394
|
+
const m = new Map<string, number>();
|
|
395
|
+
for (const k of keys) m.set(k, (m.get(k) ?? 0) + 1);
|
|
396
|
+
return m;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Compute the keys-free equivalence verdict from the before/after resource
|
|
401
|
+
* address sets and the `moved {}` blocks present in the after tree. Validate/fmt
|
|
402
|
+
* results are merged in by the tool wrapper (they need a subprocess); this pure
|
|
403
|
+
* core proves the three static invariants — resource-set preservation, argument
|
|
404
|
+
* preservation, and moved-block exhaustiveness.
|
|
405
|
+
*/
|
|
406
|
+
export function computeEquivalence(
|
|
407
|
+
before: ResourceRef[],
|
|
408
|
+
after: ResourceRef[],
|
|
409
|
+
movedBlocks: MovedBlock[],
|
|
410
|
+
ambiguous: string[] = [],
|
|
411
|
+
): EquivalenceResult {
|
|
412
|
+
const reasons: string[] = [];
|
|
413
|
+
|
|
414
|
+
// 1. resource-set preservation (multiset of logical keys).
|
|
415
|
+
const beforeKeys = multiset(before.map((r) => r.key));
|
|
416
|
+
const afterKeys = multiset(after.map((r) => r.key));
|
|
417
|
+
const added: string[] = [];
|
|
418
|
+
const removed: string[] = [];
|
|
419
|
+
for (const [k, n] of afterKeys)
|
|
420
|
+
if (n > (beforeKeys.get(k) ?? 0)) added.push(k);
|
|
421
|
+
for (const [k, n] of beforeKeys)
|
|
422
|
+
if (n > (afterKeys.get(k) ?? 0)) removed.push(k);
|
|
423
|
+
added.sort();
|
|
424
|
+
removed.sort();
|
|
425
|
+
const resourceSetEquivalent = added.length === 0 && removed.length === 0;
|
|
426
|
+
if (added.length > 0)
|
|
427
|
+
reasons.push(
|
|
428
|
+
`resource set GAINED ${added.length} resource(s): ${added.join(", ")}`,
|
|
429
|
+
);
|
|
430
|
+
if (removed.length > 0)
|
|
431
|
+
reasons.push(
|
|
432
|
+
`resource set LOST ${removed.length} resource(s): ${removed.join(", ")}`,
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
// 2. argument-name preservation per logical key (compare the union of
|
|
436
|
+
// arg-name sets for each key — robust to count>1 without false drift).
|
|
437
|
+
const argDiffs: EquivalenceResult["arg_diffs"] = [];
|
|
438
|
+
const argsByKey = (refs: ResourceRef[]) => {
|
|
439
|
+
const m = new Map<string, Set<string>>();
|
|
440
|
+
for (const r of refs) {
|
|
441
|
+
const set = m.get(r.key) ?? new Set<string>();
|
|
442
|
+
for (const a of r.argNames) set.add(a);
|
|
443
|
+
m.set(r.key, set);
|
|
444
|
+
}
|
|
445
|
+
return m;
|
|
446
|
+
};
|
|
447
|
+
const beforeArgs = argsByKey(before);
|
|
448
|
+
const afterArgs = argsByKey(after);
|
|
449
|
+
for (const key of new Set([...beforeArgs.keys(), ...afterArgs.keys()])) {
|
|
450
|
+
const b = beforeArgs.get(key) ?? new Set<string>();
|
|
451
|
+
const a = afterArgs.get(key) ?? new Set<string>();
|
|
452
|
+
const gained = [...a].filter((x) => !b.has(x)).sort();
|
|
453
|
+
const lost = [...b].filter((x) => !a.has(x)).sort();
|
|
454
|
+
if (gained.length > 0 || lost.length > 0)
|
|
455
|
+
argDiffs.push({ resource: key, added: gained, removed: lost });
|
|
456
|
+
}
|
|
457
|
+
const argNamesEquivalent = argDiffs.length === 0;
|
|
458
|
+
if (argDiffs.length > 0)
|
|
459
|
+
reasons.push(
|
|
460
|
+
`argument drift on ${argDiffs.length} resource(s): ${argDiffs.map((d) => d.resource).join(", ")}`,
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
// 3. moved-block exhaustiveness — pair each logical key's before/after
|
|
464
|
+
// addresses; a key whose address changed is a relocation that MUST be covered.
|
|
465
|
+
const addrsByKey = (refs: ResourceRef[]) => {
|
|
466
|
+
const m = new Map<string, string[]>();
|
|
467
|
+
for (const r of refs) m.set(r.key, [...(m.get(r.key) ?? []), r.address]);
|
|
468
|
+
return m;
|
|
469
|
+
};
|
|
470
|
+
const beforeAddrs = addrsByKey(before);
|
|
471
|
+
const afterAddrs = addrsByKey(after);
|
|
472
|
+
const expected: MovedBlock[] = [];
|
|
473
|
+
for (const [key, bAddrs] of beforeAddrs) {
|
|
474
|
+
const aAddrs = afterAddrs.get(key) ?? [];
|
|
475
|
+
// froms = before addresses not still present after; tos = new addresses.
|
|
476
|
+
const froms = bAddrs.filter((x) => !aAddrs.includes(x));
|
|
477
|
+
const tos = aAddrs.filter((x) => !bAddrs.includes(x));
|
|
478
|
+
// pair positionally (the 1:1 mechanical-extraction case is exact); any
|
|
479
|
+
// surplus on either side is an ambiguous relocation flagged below.
|
|
480
|
+
const pairs = Math.min(froms.length, tos.length);
|
|
481
|
+
for (let i = 0; i < pairs; i++)
|
|
482
|
+
expected.push({ from: froms[i]!, to: tos[i]! });
|
|
483
|
+
if (froms.length !== tos.length)
|
|
484
|
+
reasons.push(
|
|
485
|
+
`ambiguous relocation for ${key} (${froms.length} from / ${tos.length} to)`,
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
const moveKey = (mv: MovedBlock) => `${mv.from}=>${mv.to}`;
|
|
489
|
+
const movedSet = new Set(movedBlocks.map(moveKey));
|
|
490
|
+
const expectedSet = new Set(expected.map(moveKey));
|
|
491
|
+
const uncovered = expected.filter((mv) => !movedSet.has(moveKey(mv)));
|
|
492
|
+
const spurious = movedBlocks.filter((mv) => !expectedSet.has(moveKey(mv)));
|
|
493
|
+
const movesExhaustive = uncovered.length === 0;
|
|
494
|
+
if (uncovered.length > 0)
|
|
495
|
+
reasons.push(
|
|
496
|
+
`${uncovered.length} relocated resource(s) have NO \`moved {}\` block — ` +
|
|
497
|
+
`this would DESTROY and RECREATE them on apply: ${uncovered.map((u) => u.from).join(", ")}`,
|
|
498
|
+
);
|
|
499
|
+
if (spurious.length > 0)
|
|
500
|
+
reasons.push(
|
|
501
|
+
`${spurious.length} \`moved {}\` block(s) match no detected relocation (check from/to): ${spurious.map(moveKey).join(", ")}`,
|
|
502
|
+
);
|
|
503
|
+
if (ambiguous.length > 0)
|
|
504
|
+
reasons.push(
|
|
505
|
+
`${ambiguous.length} resource(s) in multi-instantiated/nested modules couldn't be attributed — equivalence is incomplete for: ${ambiguous.join(", ")}`,
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
const equivalent =
|
|
509
|
+
resourceSetEquivalent &&
|
|
510
|
+
argNamesEquivalent &&
|
|
511
|
+
movesExhaustive &&
|
|
512
|
+
ambiguous.length === 0;
|
|
513
|
+
|
|
514
|
+
return {
|
|
515
|
+
equivalent,
|
|
516
|
+
resource_set_equivalent: resourceSetEquivalent,
|
|
517
|
+
arg_names_equivalent: argNamesEquivalent,
|
|
518
|
+
moves_exhaustive: movesExhaustive,
|
|
519
|
+
moves_detected: expected.length,
|
|
520
|
+
expected_moves: expected,
|
|
521
|
+
uncovered_moves: uncovered,
|
|
522
|
+
spurious_moved_blocks: spurious,
|
|
523
|
+
resource_set_diff: { added, removed },
|
|
524
|
+
arg_diffs: argDiffs,
|
|
525
|
+
ambiguous,
|
|
526
|
+
reasons,
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// --- moved {} block generation (pure) — A2 -----------------------------------
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Generate one `moved {}` block per resource being relocated under a module call,
|
|
534
|
+
* and the authoritative `expected_moves` list (the denominator
|
|
535
|
+
* `computeEquivalence` checks coverage against). Each input address keeps its
|
|
536
|
+
* leaf `<type>.<name>[index]`; the new address prefixes it with `module.<to_module>.`.
|
|
537
|
+
* Works for root→module and module→module — the
|
|
538
|
+
* leaf is taken after stripping any existing `module.X.` hops.
|
|
539
|
+
*/
|
|
540
|
+
export function generateMovedBlocks(
|
|
541
|
+
addresses: string[],
|
|
542
|
+
toModule: string,
|
|
543
|
+
): { hcl: string; expected_moves: MovedBlock[] } {
|
|
544
|
+
const moves: MovedBlock[] = [];
|
|
545
|
+
for (const raw of addresses) {
|
|
546
|
+
const from = normaliseAddress(raw);
|
|
547
|
+
if (!from) continue;
|
|
548
|
+
const leaf = from.replace(/^(?:module\.[^.]+\.)+/, "");
|
|
549
|
+
moves.push({ from, to: `module.${toModule}.${leaf}` });
|
|
550
|
+
}
|
|
551
|
+
const hcl = moves
|
|
552
|
+
.map((mv) => `moved {\n from = ${mv.from}\n to = ${mv.to}\n}`)
|
|
553
|
+
.join("\n\n");
|
|
554
|
+
return { hcl, expected_moves: moves };
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// --- module provenance (pure) — C3 -------------------------------------------
|
|
558
|
+
|
|
559
|
+
export interface ModuleProvenance {
|
|
560
|
+
/** the local `module "<name>"` call name. */
|
|
561
|
+
name: string;
|
|
562
|
+
source: string;
|
|
563
|
+
/** the pinned version (registry `version`, or a git `?ref=`), or null. */
|
|
564
|
+
version: string | null;
|
|
565
|
+
kind: ModuleSourceKind;
|
|
566
|
+
/** true when the source is pinned to an exact version/ref — the provenance
|
|
567
|
+
* discipline for adopting a third-party module (the analog to a SHA-pinned
|
|
568
|
+
* action). An unpinned external module is a supply-chain risk to flag. */
|
|
569
|
+
pinned: boolean;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Record the provenance of every EXTERNAL module the config calls (registry /
|
|
574
|
+
* git / remote — local modules have in-tree code, no provenance to record).
|
|
575
|
+
* Used by the refactor b.4 evidence: "adopted `<source>` at `<version>`",
|
|
576
|
+
* with `pinned` flagging an unpinned adoption. Pure over the file tree.
|
|
577
|
+
*/
|
|
578
|
+
export function collectModuleProvenance(
|
|
579
|
+
files: SourceFile[],
|
|
580
|
+
): ModuleProvenance[] {
|
|
581
|
+
const out: ModuleProvenance[] = [];
|
|
582
|
+
const seen = new Set<string>();
|
|
583
|
+
for (const f of files) {
|
|
584
|
+
for (const b of parseModuleBlocks(f.hcl)) {
|
|
585
|
+
if (b.kind === "local") continue;
|
|
586
|
+
const key = `${b.name}|${b.source}|${b.version ?? ""}`;
|
|
587
|
+
if (seen.has(key)) continue;
|
|
588
|
+
seen.add(key);
|
|
589
|
+
out.push({
|
|
590
|
+
name: b.name,
|
|
591
|
+
source: b.source,
|
|
592
|
+
version: b.version,
|
|
593
|
+
kind: b.kind,
|
|
594
|
+
pinned: b.version !== null,
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
return out;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/** read the workspace's `.tf`/`.tfvars` tree and record external-module
|
|
602
|
+
* provenance (the impure wrapper the evidence bundle calls). */
|
|
603
|
+
export function collectWorkspaceProvenance(cwd: string): ModuleProvenance[] {
|
|
604
|
+
return collectModuleProvenance(readWorkingTree(cwd));
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// --- the `proposed — unproven` honesty primitive — C4 ------------------------
|
|
608
|
+
|
|
609
|
+
/** the GitHub label an UNPROVEN b.4 change carries so it is never mistaken for a
|
|
610
|
+
* proven one. Applied to the PR and surfaced in the evidence bundle. */
|
|
611
|
+
export const PROPOSED_UNPROVEN_LABEL = "proposed-unproven";
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* The unmissable PR-body banner for a behaviour-ALTERING b.4 change that ran
|
|
615
|
+
* WITHOUT opt-in read creds (the default): Infraweaver proposes the change but
|
|
616
|
+
* proves nothing about its runtime effect. It must never silently read as a
|
|
617
|
+
* proven/equivalence change. Used verbatim at the top of such a PR's body and
|
|
618
|
+
* mirrored as the `proposed_unproven` marker in the evidence bundle.
|
|
619
|
+
*/
|
|
620
|
+
export const PROPOSED_UNPROVEN_BANNER =
|
|
621
|
+
"> [!CAUTION]\n" +
|
|
622
|
+
"> **proposed — unproven.** This change adopts a third-party module: it ALTERS behaviour " +
|
|
623
|
+
"(it can change defaults and add or remove resources), so equivalence is false and is NOT claimed. " +
|
|
624
|
+
`No \`terraform plan\` was run (no read-only state was supplied), so ${PRODUCT_NAME} asserts nothing about ` +
|
|
625
|
+
"its runtime effect — **review the plan yourself before merging.**";
|
|
626
|
+
|
|
627
|
+
// --- tool wiring -------------------------------------------------------------
|
|
628
|
+
|
|
629
|
+
/** read the `.tf`/`.tfvars` tree at a git ref into memory (the before-state).
|
|
630
|
+
* Returns null when the ref or git is unavailable (degrade to "no baseline"). */
|
|
631
|
+
function readTreeAtRef(cwd: string, ref: string): SourceFile[] | null {
|
|
632
|
+
const list = run("git", ["ls-tree", "-r", "--name-only", ref], cwd);
|
|
633
|
+
if (list.status !== 0) return null;
|
|
634
|
+
const files: SourceFile[] = [];
|
|
635
|
+
for (const raw of list.stdout.split("\n")) {
|
|
636
|
+
const path = raw.trim().replace(/\\/g, "/");
|
|
637
|
+
if (!path.endsWith(".tf") && !path.endsWith(".tfvars")) continue;
|
|
638
|
+
const show = run("git", ["show", `${ref}:${path}`], cwd);
|
|
639
|
+
if (show.status === 0) files.push({ path, hcl: show.stdout });
|
|
640
|
+
}
|
|
641
|
+
return files;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/** read the working-tree `.tf`/`.tfvars` files on disk (the after-state) —
|
|
645
|
+
* the working tree, including the agent's uncommitted edits. */
|
|
646
|
+
function readWorkingTree(cwd: string): SourceFile[] {
|
|
647
|
+
const files: SourceFile[] = [];
|
|
648
|
+
for (const rel of walkTfFiles(cwd)) {
|
|
649
|
+
if (!rel.endsWith(".tf") && !rel.endsWith(".tfvars")) continue;
|
|
650
|
+
try {
|
|
651
|
+
files.push({
|
|
652
|
+
path: rel.replace(/\\/g, "/"),
|
|
653
|
+
hcl: readFileSync(join(cwd, rel), "utf8"),
|
|
654
|
+
});
|
|
655
|
+
} catch {
|
|
656
|
+
/* skip unreadable file */
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
return files;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/** the mode that remediates AND refactors on one branch — its refactor half is
|
|
663
|
+
* measured against the COMMITTED fix, not the run-start commit (see below). */
|
|
664
|
+
const REMEDIATE_AND_REFACTOR_MODE = "RemediateAndRefactor";
|
|
665
|
+
|
|
666
|
+
/** resolve the commit the refactor's equivalence is measured against. Normally
|
|
667
|
+
* the run-start commit (mirrors guardrails.runStartSha), so the before-tree is
|
|
668
|
+
* exactly the state this run began from. In the combined Remediate+Refactor mode
|
|
669
|
+
* the remediation fix is COMMITTED before the refactor begins, so the baseline is
|
|
670
|
+
* that post-fix commit (the current HEAD): the fix's argument changes are the
|
|
671
|
+
* refactor's starting point and must NOT be counted against its equivalence. */
|
|
672
|
+
function resolveBaseSha(ctx: LocalToolContext, cwd: string): string | null {
|
|
673
|
+
if (ctx.toolState.selectedMode === REMEDIATE_AND_REFACTOR_MODE) {
|
|
674
|
+
const r = run("git", ["rev-parse", "HEAD"], cwd);
|
|
675
|
+
return r.status === 0 && r.stdout.trim() ? r.stdout.trim() : null;
|
|
676
|
+
}
|
|
677
|
+
const head = ctx.toolState.initialHead;
|
|
678
|
+
if (!head) return null;
|
|
679
|
+
const ref = head.kind === "branch" ? head.name : head.sha;
|
|
680
|
+
const r = run("git", ["rev-parse", ref], cwd);
|
|
681
|
+
return r.status === 0 && r.stdout.trim() ? r.stdout.trim() : null;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
export const TerraformEquivalenceCheckParams = type({});
|
|
685
|
+
|
|
686
|
+
export function TerraformEquivalenceCheckTool(ctx: LocalToolContext) {
|
|
687
|
+
return tool({
|
|
688
|
+
name: "terraform_equivalence_check",
|
|
689
|
+
description:
|
|
690
|
+
"Keys-free EQUIVALENCE PROOF for a `refactor` PR (no cloud credentials). Compares the working tree " +
|
|
691
|
+
"against the run-start commit and proves the refactor preserved behaviour: (1) the same multiset of " +
|
|
692
|
+
"managed resources, (2) identical top-level argument names per resource, and (3) — the highest-risk " +
|
|
693
|
+
"invariant — that EVERY relocated address is covered by a `moved {}` block (zero uncovered moves), " +
|
|
694
|
+
"then runs `terraform validate` + `terraform fmt -check`. Returns `equivalent` (the overall verdict; " +
|
|
695
|
+
"the push guardrail HARD-BLOCKS when false), plus `uncovered_moves` (a missed `moved {}` = a silent " +
|
|
696
|
+
"destroy/recreate on apply — fix before pushing), `expected_moves`, `resource_set_diff`, and " +
|
|
697
|
+
"`arg_diffs`. For a behaviour-ALTERING change (b.4, adopting a third-party module) equivalence is " +
|
|
698
|
+
"expected to be false — do not claim it. Optional stronger proof: a real `terraform_plan` that is a " +
|
|
699
|
+
"pure-move plan (`refactor_safe: true`) corroborates this floor when cloud creds are present.",
|
|
700
|
+
parameters: TerraformEquivalenceCheckParams,
|
|
701
|
+
execute: execute(async () => {
|
|
702
|
+
const cwd = ctx.payload.cwd ?? process.cwd();
|
|
703
|
+
const base = resolveBaseSha(ctx, cwd);
|
|
704
|
+
if (!base) {
|
|
705
|
+
return toolSkip(
|
|
706
|
+
"no_baseline",
|
|
707
|
+
"could not resolve the run-start commit — an equivalence proof needs the before-state to diff against. Ensure the run started from a clean git checkout.",
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
// Combined Remediate+Refactor: the baseline is the COMMITTED fix, so the
|
|
711
|
+
// refactor edits must still be UNCOMMITTED for this to measure anything. A
|
|
712
|
+
// clean working tree here means the refactor wasn't made yet, or was
|
|
713
|
+
// committed before proving — either way diffing the baseline against an
|
|
714
|
+
// identical working tree would vacuously report `equivalent: true`. Refuse
|
|
715
|
+
// that false positive rather than emit a proof of nothing.
|
|
716
|
+
if (
|
|
717
|
+
ctx.toolState.selectedMode === REMEDIATE_AND_REFACTOR_MODE &&
|
|
718
|
+
changedFilesSinceBase(cwd, base).length === 0
|
|
719
|
+
) {
|
|
720
|
+
return toolSkip(
|
|
721
|
+
"refactor_uncommitted_required",
|
|
722
|
+
"combined Remediate+Refactor: the equivalence proof diffs the working tree against the COMMITTED fix, but the working tree has no uncommitted changes. Commit the fix first, then make the refactor edits, then run this check BEFORE committing them.",
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
const beforeFiles = readTreeAtRef(cwd, base);
|
|
726
|
+
if (!beforeFiles) {
|
|
727
|
+
return toolSkip(
|
|
728
|
+
"no_baseline",
|
|
729
|
+
`could not read the Terraform tree at ${base.slice(0, 8)} — equivalence cannot be proven without the before-state.`,
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
const afterFiles = readWorkingTree(cwd);
|
|
733
|
+
|
|
734
|
+
const before = attributeAddresses(beforeFiles);
|
|
735
|
+
const after = attributeAddresses(afterFiles);
|
|
736
|
+
// `moved {}` blocks live in ROOT files (a module can't relocate its own
|
|
737
|
+
// caller's addresses), so collect them from after files NOT inside a module.
|
|
738
|
+
const afterCalls = new Set(
|
|
739
|
+
afterFiles.flatMap((f) =>
|
|
740
|
+
parseModuleBlocks(f.hcl)
|
|
741
|
+
.filter((b) => b.kind === "local")
|
|
742
|
+
.map((b) =>
|
|
743
|
+
posix
|
|
744
|
+
.normalize(posix.join(posix.dirname(f.path), b.source))
|
|
745
|
+
.replace(/\/+$/, ""),
|
|
746
|
+
),
|
|
747
|
+
),
|
|
748
|
+
);
|
|
749
|
+
const rootFiles = afterFiles.filter(
|
|
750
|
+
(f) => ![...afterCalls].some((dir) => fileInDir(f.path, dir)),
|
|
751
|
+
);
|
|
752
|
+
const movedBlocks = rootFiles.flatMap((f) => parseMovedBlocks(f.hcl));
|
|
753
|
+
|
|
754
|
+
const result = computeEquivalence(
|
|
755
|
+
before.resources,
|
|
756
|
+
after.resources,
|
|
757
|
+
movedBlocks,
|
|
758
|
+
[...new Set([...before.ambiguous, ...after.ambiguous])],
|
|
759
|
+
);
|
|
760
|
+
|
|
761
|
+
// validate + fmt (the keys-free proof's correctness gates). The credentials
|
|
762
|
+
// let a private cross-repo `git::` module or a private-registry module
|
|
763
|
+
// resolve during init, mirroring validate.
|
|
764
|
+
const terraformEnv = resolveModuleCredentialEnv(ctx.payload, {
|
|
765
|
+
sshKeyDir: ctx.tmpdir,
|
|
766
|
+
});
|
|
767
|
+
const fmtOutcome = scanFmt(cwd);
|
|
768
|
+
const validateOutcome = scanValidate(cwd, terraformEnv);
|
|
769
|
+
const validateRan = validateOutcome.ran;
|
|
770
|
+
const validatePassed =
|
|
771
|
+
validateRan &&
|
|
772
|
+
validateOutcome.concerns.length === 0 &&
|
|
773
|
+
(validateOutcome.unvalidated ?? 0) === 0;
|
|
774
|
+
// fmt-clean is scoped to the files this run changed — pre-existing drift in
|
|
775
|
+
// untouched files is not the refactor's to fix and must not fail its proof.
|
|
776
|
+
const changed = new Set(changedFilesSinceBase(cwd, base));
|
|
777
|
+
const fmtDriftFiles = [
|
|
778
|
+
...new Set(
|
|
779
|
+
fmtOutcome.concerns
|
|
780
|
+
.map((c) => c.location.file)
|
|
781
|
+
.filter((f) => changed.size === 0 || changed.has(f)),
|
|
782
|
+
),
|
|
783
|
+
];
|
|
784
|
+
const fmtClean = fmtOutcome.ran && fmtDriftFiles.length === 0;
|
|
785
|
+
|
|
786
|
+
const reasons = [...result.reasons];
|
|
787
|
+
if (!validatePassed)
|
|
788
|
+
reasons.push(
|
|
789
|
+
validateRan
|
|
790
|
+
? "`terraform validate` did not pass"
|
|
791
|
+
: "`terraform validate` did not run (terraform not installed) — the equivalence proof is incomplete",
|
|
792
|
+
);
|
|
793
|
+
if (!fmtClean && fmtOutcome.ran && fmtDriftFiles.length > 0)
|
|
794
|
+
reasons.push(
|
|
795
|
+
`\`terraform fmt\` reports drift in changed file(s): ${fmtDriftFiles.join(", ")}`,
|
|
796
|
+
);
|
|
797
|
+
|
|
798
|
+
const equivalent = result.equivalent && validatePassed && fmtClean;
|
|
799
|
+
|
|
800
|
+
// record for the push-time hard-fail guardrail (read in guardrails.ts) and
|
|
801
|
+
// the evidence bundle (read in terraform_emit_evidence).
|
|
802
|
+
scanSessionOf(ctx.toolState).refactorEquivalence = {
|
|
803
|
+
equivalent,
|
|
804
|
+
resource_set_equivalent: result.resource_set_equivalent,
|
|
805
|
+
arg_names_equivalent: result.arg_names_equivalent,
|
|
806
|
+
moves_exhaustive: result.moves_exhaustive,
|
|
807
|
+
moves_detected: result.moves_detected,
|
|
808
|
+
validate_passed: validatePassed,
|
|
809
|
+
fmt_clean: fmtClean,
|
|
810
|
+
uncovered_moves: result.uncovered_moves,
|
|
811
|
+
reasons,
|
|
812
|
+
};
|
|
813
|
+
|
|
814
|
+
log.info(
|
|
815
|
+
`» terraform_equivalence_check: equivalent=${equivalent} (moves ${result.expected_moves.length}, uncovered ${result.uncovered_moves.length}, resource-set ${result.resource_set_equivalent}, args ${result.arg_names_equivalent}, validate ${validatePassed}, fmt ${fmtClean})`,
|
|
816
|
+
);
|
|
817
|
+
|
|
818
|
+
return toolOk({
|
|
819
|
+
...result,
|
|
820
|
+
equivalent,
|
|
821
|
+
validate_passed: validatePassed,
|
|
822
|
+
validate_ran: validateRan,
|
|
823
|
+
fmt_clean: fmtClean,
|
|
824
|
+
fmt_drift_files: fmtDriftFiles,
|
|
825
|
+
base: base.slice(0, 8),
|
|
826
|
+
reasons,
|
|
827
|
+
note: equivalent
|
|
828
|
+
? "behaviour-preserving refactor PROVEN (keys-free). For the stronger proof, run terraform_plan — refactor_safe:true corroborates it."
|
|
829
|
+
: "NOT equivalent — the push will be blocked. Fix the issues in `reasons` (an uncovered move is the dangerous one — add the missing `moved {}` block).",
|
|
830
|
+
});
|
|
831
|
+
}),
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/** files changed (tracked) between the base commit and the working tree. */
|
|
836
|
+
function changedFilesSinceBase(cwd: string, base: string): string[] {
|
|
837
|
+
const r = run("git", ["diff", "--name-only", base], cwd);
|
|
838
|
+
if (r.status !== 0) return [];
|
|
839
|
+
return r.stdout
|
|
840
|
+
.split("\n")
|
|
841
|
+
.map((l) => l.trim().replace(/\\/g, "/"))
|
|
842
|
+
.filter(Boolean);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
export const TerraformGenerateMovedParams = type({
|
|
846
|
+
resources: type.string
|
|
847
|
+
.array()
|
|
848
|
+
.describe(
|
|
849
|
+
"the CURRENT addresses of the resources being relocated, e.g. ['aws_s3_bucket.logs', 'aws_s3_bucket_policy.logs'] (root) or ['module.old.aws_vpc.main'] (module→module).",
|
|
850
|
+
),
|
|
851
|
+
to_module: type.string.describe(
|
|
852
|
+
'the local name of the `module "<name>"` call the resources are moving INTO (the new address prefix).',
|
|
853
|
+
),
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
export function TerraformGenerateMovedTool(ctx: LocalToolContext) {
|
|
857
|
+
return tool({
|
|
858
|
+
name: "terraform_generate_moved",
|
|
859
|
+
description:
|
|
860
|
+
"Generate the EXHAUSTIVE `moved {}` blocks for a resources→module refactor — one per relocated " +
|
|
861
|
+
"address (old → `module.<to_module>.<leaf>`), so the move set is complete by construction. Returns " +
|
|
862
|
+
"the ready-to-paste `hcl` (put it in a ROOT file) and `expected_moves` (the authoritative mapping " +
|
|
863
|
+
"`terraform_equivalence_check` verifies coverage against). A missing `moved {}` block is the single " +
|
|
864
|
+
"highest-risk refactor defect (a silent destroy/recreate) — generate them here rather than hand-" +
|
|
865
|
+
"writing so none is forgotten.",
|
|
866
|
+
parameters: TerraformGenerateMovedParams,
|
|
867
|
+
// pure string generation — no state change, safe for subagents.
|
|
868
|
+
execute: execute(async ({ resources, to_module }) => {
|
|
869
|
+
void ctx;
|
|
870
|
+
const { hcl, expected_moves } = generateMovedBlocks(resources, to_module);
|
|
871
|
+
return toolOk({
|
|
872
|
+
count: expected_moves.length,
|
|
873
|
+
expected_moves,
|
|
874
|
+
hcl,
|
|
875
|
+
note: "paste `hcl` into a root .tf file, then prove the refactor with terraform_equivalence_check (and terraform_plan when cloud creds are available).",
|
|
876
|
+
});
|
|
877
|
+
}),
|
|
878
|
+
});
|
|
879
|
+
}
|