terramend 0.2.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 +145 -0
- package/dist/agents/claude.d.ts +73 -0
- package/dist/agents/claudePretoolGate.d.ts +99 -0
- package/dist/agents/gateServer.d.ts +7 -0
- package/dist/agents/index.d.ts +6 -0
- package/dist/agents/nativeFsDenies.d.ts +28 -0
- package/dist/agents/opencode.d.ts +231 -0
- package/dist/agents/opencodePlugin.d.ts +85 -0
- package/dist/agents/opencodeShared.d.ts +40 -0
- package/dist/agents/postRun.d.ts +132 -0
- package/dist/agents/reviewer.d.ts +38 -0
- package/dist/agents/sessionLabeler.d.ts +97 -0
- package/dist/agents/shared.d.ts +189 -0
- package/dist/agents/subagentModels.d.ts +19 -0
- package/dist/agents/subagentToolGates.d.ts +55 -0
- package/dist/cli.mjs +197426 -0
- package/dist/external.d.ts +227 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +196783 -0
- package/dist/internal/index.d.ts +18 -0
- package/dist/internal.js +1714 -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/checkSuite.d.ts +25 -0
- package/dist/mcp/checkout.d.ts +77 -0
- package/dist/mcp/comment.d.ts +119 -0
- package/dist/mcp/commitInfo.d.ts +9 -0
- package/dist/mcp/crosswalk.d.ts +105 -0
- package/dist/mcp/dependencies.d.ts +8 -0
- package/dist/mcp/geminiSanitizer.d.ts +28 -0
- package/dist/mcp/git.d.ts +46 -0
- package/dist/mcp/guardrails.d.ts +104 -0
- package/dist/mcp/issue.d.ts +18 -0
- package/dist/mcp/issueComments.d.ts +9 -0
- package/dist/mcp/issueEvents.d.ts +9 -0
- package/dist/mcp/issueInfo.d.ts +9 -0
- package/dist/mcp/labels.d.ts +12 -0
- package/dist/mcp/localContext.d.ts +19 -0
- package/dist/mcp/moduleExtraction.d.ts +71 -0
- package/dist/mcp/moduleTests.d.ts +104 -0
- package/dist/mcp/modules.d.ts +179 -0
- package/dist/mcp/output.d.ts +12 -0
- package/dist/mcp/pathSafety.d.ts +14 -0
- package/dist/mcp/policy.d.ts +48 -0
- package/dist/mcp/pr.d.ts +49 -0
- package/dist/mcp/prInfo.d.ts +9 -0
- package/dist/mcp/providerSchema.d.ts +50 -0
- package/dist/mcp/review.d.ts +199 -0
- package/dist/mcp/reviewComments.d.ts +178 -0
- package/dist/mcp/roots.d.ts +58 -0
- package/dist/mcp/scope.d.ts +15 -0
- package/dist/mcp/selectMode.d.ts +18 -0
- package/dist/mcp/server.d.ts +48 -0
- package/dist/mcp/shared.d.ts +47 -0
- package/dist/mcp/shell.d.ts +37 -0
- package/dist/mcp/staleFix.d.ts +51 -0
- package/dist/mcp/terraform/cost.d.ts +55 -0
- package/dist/mcp/terraform/currency.d.ts +94 -0
- package/dist/mcp/terraform/decisions.d.ts +178 -0
- package/dist/mcp/terraform/findings.d.ts +75 -0
- package/dist/mcp/terraform/plan.d.ts +157 -0
- package/dist/mcp/terraform/scanners.d.ts +131 -0
- package/dist/mcp/terraform/tools.d.ts +63 -0
- package/dist/mcp/terraform/types.d.ts +172 -0
- package/dist/mcp/terraform.d.ts +22 -0
- package/dist/mcp/terratest.d.ts +83 -0
- package/dist/mcp/upload.d.ts +6 -0
- package/dist/models.d.ts +171 -0
- package/dist/modes.d.ts +26 -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 +64 -0
- package/dist/skills/terraform-best-practices/SKILL.md +369 -0
- package/dist/toolState.d.ts +135 -0
- package/dist/utils/activity.d.ts +40 -0
- package/dist/utils/agent.d.ts +20 -0
- package/dist/utils/agentHangReport.d.ts +38 -0
- package/dist/utils/apiFetch.d.ts +19 -0
- package/dist/utils/apiKeys.d.ts +41 -0
- package/dist/utils/apiUrl.d.ts +20 -0
- package/dist/utils/assets.d.ts +8 -0
- package/dist/utils/billingErrors.d.ts +85 -0
- package/dist/utils/body.d.ts +34 -0
- package/dist/utils/buildTerramendFooter.d.ts +25 -0
- package/dist/utils/byokFallback.d.ts +85 -0
- package/dist/utils/claudeSubscription.d.ts +30 -0
- package/dist/utils/cli.d.ts +10 -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/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 +78 -0
- package/dist/utils/globals.d.ts +3 -0
- package/dist/utils/install.d.ts +60 -0
- package/dist/utils/instructions.d.ts +48 -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/normalizeEnv.d.ts +30 -0
- package/dist/utils/openCodeModels.d.ts +11 -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 +105 -0
- package/dist/utils/prSummary.d.ts +61 -0
- package/dist/utils/progressComment.d.ts +146 -0
- package/dist/utils/providerErrors.d.ts +31 -0
- package/dist/utils/rangeDiff.d.ts +51 -0
- package/dist/utils/remediationCommand.d.ts +55 -0
- package/dist/utils/retry.d.ts +13 -0
- package/dist/utils/reviewCleanup.d.ts +14 -0
- package/dist/utils/run.d.ts +9 -0
- package/dist/utils/runContext.d.ts +60 -0
- package/dist/utils/runContextData.d.ts +23 -0
- package/dist/utils/runErrorRenderer.d.ts +64 -0
- package/dist/utils/runLifecycle.d.ts +86 -0
- package/dist/utils/runStartupLog.d.ts +15 -0
- package/dist/utils/secrets.d.ts +22 -0
- package/dist/utils/setup.d.ts +90 -0
- package/dist/utils/shell.d.ts +32 -0
- package/dist/utils/skills.d.ts +10 -0
- package/dist/utils/subprocess.d.ts +80 -0
- package/dist/utils/terraformMcp.d.ts +42 -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 +39 -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 +119 -0
- package/src/agents/claude.test.ts +1016 -0
- package/src/agents/claude.ts +1246 -0
- package/src/agents/claudePretoolGate.test.ts +28 -0
- package/src/agents/claudePretoolGate.ts +173 -0
- package/src/agents/gateServer.test.ts +204 -0
- package/src/agents/gateServer.ts +124 -0
- package/src/agents/index.ts +10 -0
- package/src/agents/nativeFsDenies.ts +82 -0
- package/src/agents/opencode.test.ts +1440 -0
- package/src/agents/opencode.ts +1312 -0
- package/src/agents/opencodePlugin.ts +222 -0
- package/src/agents/opencodeShared.test.ts +34 -0
- package/src/agents/opencodeShared.ts +121 -0
- package/src/agents/postRun.test.ts +549 -0
- package/src/agents/postRun.ts +535 -0
- package/src/agents/reviewer.ts +104 -0
- package/src/agents/sessionLabeler.test.ts +247 -0
- package/src/agents/sessionLabeler.ts +178 -0
- package/src/agents/shared.test.ts +76 -0
- package/src/agents/shared.ts +292 -0
- package/src/agents/subagentModels.test.ts +113 -0
- package/src/agents/subagentModels.ts +40 -0
- package/src/agents/subagentRegistration.test.ts +41 -0
- package/src/agents/subagentToolGates.ts +114 -0
- package/src/cli.test.ts +129 -0
- package/src/cli.ts +105 -0
- package/src/commands/gha.test.ts +192 -0
- package/src/commands/gha.ts +188 -0
- package/src/commands/mcp.ts +122 -0
- package/src/config.ts +1 -0
- package/src/entry.ts +7 -0
- package/src/entryPost.stdlibOnly.test.ts +109 -0
- package/src/entryPost.ts +99 -0
- package/src/external.test.ts +16 -0
- package/src/external.ts +302 -0
- package/src/index.ts +11 -0
- package/src/internal/index.ts +71 -0
- package/src/lifecycle.ts +2 -0
- package/src/main.test.ts +873 -0
- package/src/main.ts +712 -0
- package/src/mcp/__fixtures__/terramend-scratch-pr-49-review-3485940013.json +110 -0
- package/src/mcp/__fixtures__/terramend-scratch-pr-64-review-3531000326.json +14 -0
- package/src/mcp/__fixtures__/terramend-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/checkSuite.test.ts +245 -0
- package/src/mcp/checkSuite.ts +255 -0
- package/src/mcp/checkout.test.ts +752 -0
- package/src/mcp/checkout.ts +886 -0
- package/src/mcp/comment.test.ts +772 -0
- package/src/mcp/comment.ts +582 -0
- package/src/mcp/commitInfo.test.ts +127 -0
- package/src/mcp/commitInfo.ts +61 -0
- package/src/mcp/crosswalk.test.ts +106 -0
- package/src/mcp/crosswalk.ts +339 -0
- package/src/mcp/dependencies.test.ts +309 -0
- package/src/mcp/dependencies.ts +189 -0
- package/src/mcp/geminiSanitizer.test.ts +287 -0
- package/src/mcp/geminiSanitizer.ts +207 -0
- package/src/mcp/git.test.ts +1083 -0
- package/src/mcp/git.ts +890 -0
- package/src/mcp/guardrails.test.ts +705 -0
- package/src/mcp/guardrails.ts +465 -0
- package/src/mcp/issue.test.ts +113 -0
- package/src/mcp/issue.ts +73 -0
- package/src/mcp/issueComments.test.ts +69 -0
- package/src/mcp/issueComments.ts +48 -0
- package/src/mcp/issueEvents.test.ts +134 -0
- package/src/mcp/issueEvents.ts +100 -0
- package/src/mcp/issueInfo.test.ts +104 -0
- package/src/mcp/issueInfo.ts +72 -0
- package/src/mcp/labels.test.ts +52 -0
- package/src/mcp/labels.ts +34 -0
- package/src/mcp/localContext.ts +28 -0
- package/src/mcp/localServer.test.ts +75 -0
- package/src/mcp/localServer.ts +131 -0
- package/src/mcp/moduleExtraction.test.ts +261 -0
- package/src/mcp/moduleExtraction.ts +313 -0
- package/src/mcp/moduleTests.test.ts +269 -0
- package/src/mcp/moduleTests.ts +421 -0
- package/src/mcp/modules.test.ts +640 -0
- package/src/mcp/modules.ts +696 -0
- package/src/mcp/output.test.ts +96 -0
- package/src/mcp/output.ts +70 -0
- package/src/mcp/pathSafety.test.ts +44 -0
- package/src/mcp/pathSafety.ts +28 -0
- package/src/mcp/policy.test.ts +282 -0
- package/src/mcp/policy.ts +199 -0
- package/src/mcp/pr.test.ts +387 -0
- package/src/mcp/pr.ts +194 -0
- package/src/mcp/prInfo.test.ts +96 -0
- package/src/mcp/prInfo.ts +91 -0
- package/src/mcp/providerSchema.test.ts +85 -0
- package/src/mcp/providerSchema.ts +175 -0
- package/src/mcp/review.test.ts +936 -0
- package/src/mcp/review.ts +923 -0
- package/src/mcp/reviewComments.test.ts +549 -0
- package/src/mcp/reviewComments.ts +896 -0
- package/src/mcp/roots.test.ts +175 -0
- package/src/mcp/roots.ts +217 -0
- package/src/mcp/scope.test.ts +59 -0
- package/src/mcp/scope.ts +65 -0
- package/src/mcp/security.test.ts +720 -0
- package/src/mcp/selectMode.test.ts +210 -0
- package/src/mcp/selectMode.ts +181 -0
- package/src/mcp/server.test.ts +292 -0
- package/src/mcp/server.ts +403 -0
- package/src/mcp/shared.ts +100 -0
- package/src/mcp/shell.test.ts +520 -0
- package/src/mcp/shell.ts +505 -0
- package/src/mcp/staleFix.test.ts +237 -0
- package/src/mcp/staleFix.ts +277 -0
- package/src/mcp/terraform/cost.ts +163 -0
- package/src/mcp/terraform/currency.test.ts +338 -0
- package/src/mcp/terraform/currency.ts +336 -0
- package/src/mcp/terraform/decisions.ts +527 -0
- package/src/mcp/terraform/findings.ts +333 -0
- package/src/mcp/terraform/plan.ts +348 -0
- package/src/mcp/terraform/scanners.ts +809 -0
- package/src/mcp/terraform/tools.test.ts +1071 -0
- package/src/mcp/terraform/tools.ts +908 -0
- package/src/mcp/terraform/types.ts +305 -0
- package/src/mcp/terraform.test.ts +1957 -0
- package/src/mcp/terraform.ts +23 -0
- package/src/mcp/terratest.test.ts +105 -0
- package/src/mcp/terratest.ts +196 -0
- package/src/mcp/toolFiltering.test.ts +85 -0
- package/src/mcp/upload.test.ts +180 -0
- package/src/mcp/upload.ts +112 -0
- package/src/models.test.ts +300 -0
- package/src/models.ts +708 -0
- package/src/modes.test.ts +107 -0
- package/src/modes.ts +880 -0
- package/src/prep/index.ts +43 -0
- package/src/prep/installNodeDependencies.test.ts +298 -0
- package/src/prep/installNodeDependencies.ts +196 -0
- package/src/prep/installPythonDependencies.test.ts +268 -0
- package/src/prep/installPythonDependencies.ts +199 -0
- package/src/prep/types.ts +38 -0
- package/src/reviewQuality.test.ts +63 -0
- package/src/reviewQuality.ts +134 -0
- package/src/runCli.test.ts +214 -0
- package/src/runCli.ts +282 -0
- package/src/skills/terraform-best-practices/SKILL.md +369 -0
- package/src/toolState.test.ts +45 -0
- package/src/toolState.ts +252 -0
- package/src/utils/activity.test.ts +188 -0
- package/src/utils/activity.ts +210 -0
- package/src/utils/agent.test.ts +251 -0
- package/src/utils/agent.ts +139 -0
- package/src/utils/agentHangReport.test.ts +203 -0
- package/src/utils/agentHangReport.ts +170 -0
- package/src/utils/apiFetch.test.ts +115 -0
- package/src/utils/apiFetch.ts +62 -0
- package/src/utils/apiKeys.test.ts +344 -0
- package/src/utils/apiKeys.ts +206 -0
- package/src/utils/apiUrl.test.ts +30 -0
- package/src/utils/apiUrl.ts +59 -0
- package/src/utils/assets.test.ts +153 -0
- package/src/utils/assets.ts +107 -0
- package/src/utils/billingErrors.test.ts +121 -0
- package/src/utils/billingErrors.ts +189 -0
- package/src/utils/body.test.ts +217 -0
- package/src/utils/body.ts +168 -0
- package/src/utils/buildTerramendFooter.test.ts +38 -0
- package/src/utils/buildTerramendFooter.ts +82 -0
- package/src/utils/byokFallback.test.ts +205 -0
- package/src/utils/byokFallback.ts +128 -0
- package/src/utils/claudeSubscription.test.ts +179 -0
- package/src/utils/claudeSubscription.ts +93 -0
- package/src/utils/cli.ts +31 -0
- package/src/utils/codexHome.test.ts +190 -0
- package/src/utils/codexHome.ts +191 -0
- package/src/utils/codexOAuth.ts +147 -0
- package/src/utils/codexRefreshDetect.test.ts +85 -0
- package/src/utils/codexRefreshDetect.ts +35 -0
- package/src/utils/diffCoverage.test.ts +468 -0
- package/src/utils/diffCoverage.ts +404 -0
- package/src/utils/errorReport.test.ts +135 -0
- package/src/utils/errorReport.ts +83 -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.test.ts +322 -0
- package/src/utils/gitAuth.ts +263 -0
- package/src/utils/gitAuthServer.test.ts +260 -0
- package/src/utils/gitAuthServer.ts +182 -0
- package/src/utils/github.test.ts +615 -0
- package/src/utils/github.ts +538 -0
- package/src/utils/globals.ts +9 -0
- package/src/utils/humanEditCapture.test.ts +100 -0
- package/src/utils/humanEditCapture.ts +193 -0
- package/src/utils/install.test.ts +768 -0
- package/src/utils/install.ts +492 -0
- package/src/utils/instructions.test.ts +240 -0
- package/src/utils/instructions.ts +543 -0
- package/src/utils/leapingComment.test.ts +51 -0
- package/src/utils/leapingComment.ts +18 -0
- package/src/utils/learnings.test.ts +87 -0
- package/src/utils/learnings.ts +138 -0
- package/src/utils/learningsTocRender.test.ts +116 -0
- package/src/utils/learningsTruncate.test.ts +39 -0
- package/src/utils/learningsTruncate.ts +42 -0
- package/src/utils/lifecycle.test.ts +195 -0
- package/src/utils/lifecycle.ts +198 -0
- package/src/utils/log.test.ts +402 -0
- package/src/utils/log.ts +432 -0
- package/src/utils/normalizeEnv.test.ts +91 -0
- package/src/utils/normalizeEnv.ts +106 -0
- package/src/utils/openCodeModels.ts +82 -0
- package/src/utils/overrides.test.ts +89 -0
- package/src/utils/overrides.ts +98 -0
- package/src/utils/packageManager.test.ts +321 -0
- package/src/utils/packageManager.ts +257 -0
- package/src/utils/patchWorkflowRunFields.test.ts +92 -0
- package/src/utils/patchWorkflowRunFields.ts +150 -0
- package/src/utils/payload.test.ts +497 -0
- package/src/utils/payload.ts +371 -0
- package/src/utils/postApiFetch.ts +51 -0
- package/src/utils/prSummary.test.ts +224 -0
- package/src/utils/prSummary.ts +147 -0
- package/src/utils/progressComment.ts +261 -0
- package/src/utils/providerErrors.test.ts +315 -0
- package/src/utils/providerErrors.ts +172 -0
- package/src/utils/rangeDiff.test.ts +236 -0
- package/src/utils/rangeDiff.ts +182 -0
- package/src/utils/remediationCommand.test.ts +163 -0
- package/src/utils/remediationCommand.ts +119 -0
- package/src/utils/retry.test.ts +153 -0
- package/src/utils/retry.ts +58 -0
- package/src/utils/reviewCleanup.ts +106 -0
- package/src/utils/run.ts +99 -0
- package/src/utils/runContext.ts +145 -0
- package/src/utils/runContextData.ts +58 -0
- package/src/utils/runErrorRenderer.test.ts +95 -0
- package/src/utils/runErrorRenderer.ts +259 -0
- package/src/utils/runFixture.ts +76 -0
- package/src/utils/runLifecycle.ts +237 -0
- package/src/utils/runStartupLog.ts +60 -0
- package/src/utils/secrets.test.ts +103 -0
- package/src/utils/secrets.ts +177 -0
- package/src/utils/setup.test.ts +509 -0
- package/src/utils/setup.ts +352 -0
- package/src/utils/shell.ts +103 -0
- package/src/utils/skills.test.ts +46 -0
- package/src/utils/skills.ts +67 -0
- package/src/utils/subprocess.test.ts +170 -0
- package/src/utils/subprocess.ts +438 -0
- package/src/utils/terraformMcp.test.ts +63 -0
- package/src/utils/terraformMcp.ts +83 -0
- package/src/utils/time.test.ts +105 -0
- package/src/utils/time.ts +59 -0
- package/src/utils/timer.test.ts +91 -0
- package/src/utils/timer.ts +72 -0
- package/src/utils/todoTracking.test.ts +223 -0
- package/src/utils/todoTracking.ts +167 -0
- package/src/utils/token.test.ts +239 -0
- package/src/utils/token.ts +186 -0
- package/src/utils/version.ts +10 -0
- package/src/utils/versioning.test.ts +34 -0
- package/src/utils/versioning.ts +44 -0
- package/src/utils/vertex.ts +85 -0
- package/src/utils/workflow.ts +25 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { postProcessRangeDiff } from "#app/utils/rangeDiff";
|
|
3
|
+
|
|
4
|
+
describe("postProcessRangeDiff", () => {
|
|
5
|
+
it("returns null for identical patches", () => {
|
|
6
|
+
const input = "1: abc1234 = 1: def5678 x";
|
|
7
|
+
expect(postProcessRangeDiff(input)).toBeNull();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("returns null for empty input", () => {
|
|
11
|
+
expect(postProcessRangeDiff("")).toBeNull();
|
|
12
|
+
expect(postProcessRangeDiff(" ")).toBeNull();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("returns null when no changes exist between versions", () => {
|
|
16
|
+
const input = [
|
|
17
|
+
"1: abc1234 ! 1: def5678 x",
|
|
18
|
+
" ## src/file.ts ##",
|
|
19
|
+
" @@ src/file.ts",
|
|
20
|
+
" +const a = 1;",
|
|
21
|
+
" +const b = 2;",
|
|
22
|
+
].join("\n");
|
|
23
|
+
expect(postProcessRangeDiff(input)).toBeNull();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("strips inner diff prefix from content lines", () => {
|
|
27
|
+
const input = [
|
|
28
|
+
"1: abc1234 ! 1: def5678 x",
|
|
29
|
+
" ## src/math.ts ##",
|
|
30
|
+
" @@ src/math.ts",
|
|
31
|
+
" + const a = 1;",
|
|
32
|
+
" -+ const b = 2;",
|
|
33
|
+
" ++ const b = 3;",
|
|
34
|
+
" + const c = 4;",
|
|
35
|
+
].join("\n");
|
|
36
|
+
expect(postProcessRangeDiff(input)).toMatchInlineSnapshot(`
|
|
37
|
+
" ## src/math.ts ##
|
|
38
|
+
@@ src/math.ts
|
|
39
|
+
const a = 1;
|
|
40
|
+
- const b = 2;
|
|
41
|
+
+ const b = 3;
|
|
42
|
+
const c = 4;"
|
|
43
|
+
`);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("handles context lines (inner space prefix)", () => {
|
|
47
|
+
const input = [
|
|
48
|
+
"1: abc1234 ! 1: def5678 x",
|
|
49
|
+
" ## src/file.ts ##",
|
|
50
|
+
" @@ src/file.ts",
|
|
51
|
+
" const base = true;",
|
|
52
|
+
" - const old = true;",
|
|
53
|
+
" + const new_ = true;",
|
|
54
|
+
" const end = true;",
|
|
55
|
+
].join("\n");
|
|
56
|
+
expect(postProcessRangeDiff(input)).toMatchInlineSnapshot(`
|
|
57
|
+
" ## src/file.ts ##
|
|
58
|
+
@@ src/file.ts
|
|
59
|
+
const base = true;
|
|
60
|
+
-const old = true;
|
|
61
|
+
+const new_ = true;
|
|
62
|
+
const end = true;"
|
|
63
|
+
`);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("trims context lines around changes", () => {
|
|
67
|
+
const contextBefore = Array.from(
|
|
68
|
+
{ length: 9 },
|
|
69
|
+
(_, i) => ` +const line${i + 1} = ${i + 1};`,
|
|
70
|
+
);
|
|
71
|
+
const contextAfter = Array.from(
|
|
72
|
+
{ length: 6 },
|
|
73
|
+
(_, i) => ` +const line${i + 11} = ${i + 11};`,
|
|
74
|
+
);
|
|
75
|
+
const input = [
|
|
76
|
+
"1: abc1234 ! 1: def5678 x",
|
|
77
|
+
" ## src/large.ts ##",
|
|
78
|
+
" @@ src/large.ts",
|
|
79
|
+
...contextBefore,
|
|
80
|
+
" -+const line10 = 10;",
|
|
81
|
+
" ++const line10 = 100;",
|
|
82
|
+
...contextAfter,
|
|
83
|
+
].join("\n");
|
|
84
|
+
expect(postProcessRangeDiff(input)).toMatchInlineSnapshot(`
|
|
85
|
+
" ## src/large.ts ##
|
|
86
|
+
@@ src/large.ts
|
|
87
|
+
...
|
|
88
|
+
const line7 = 7;
|
|
89
|
+
const line8 = 8;
|
|
90
|
+
const line9 = 9;
|
|
91
|
+
-const line10 = 10;
|
|
92
|
+
+const line10 = 100;
|
|
93
|
+
const line11 = 11;
|
|
94
|
+
const line12 = 12;
|
|
95
|
+
const line13 = 13;"
|
|
96
|
+
`);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("handles multiple files", () => {
|
|
100
|
+
const input = [
|
|
101
|
+
"1: abc ! 1: def x",
|
|
102
|
+
" ## src/a.ts ##",
|
|
103
|
+
" @@ src/a.ts",
|
|
104
|
+
" -+old line a",
|
|
105
|
+
" ++new line a",
|
|
106
|
+
" ## src/b.ts ##",
|
|
107
|
+
" @@ src/b.ts",
|
|
108
|
+
" +unchanged",
|
|
109
|
+
" -+old line b",
|
|
110
|
+
" ++new line b",
|
|
111
|
+
].join("\n");
|
|
112
|
+
expect(postProcessRangeDiff(input)).toMatchInlineSnapshot(`
|
|
113
|
+
" ## src/a.ts ##
|
|
114
|
+
@@ src/a.ts
|
|
115
|
+
-old line a
|
|
116
|
+
+new line a
|
|
117
|
+
## src/b.ts ##
|
|
118
|
+
@@ src/b.ts
|
|
119
|
+
unchanged
|
|
120
|
+
-old line b
|
|
121
|
+
+new line b"
|
|
122
|
+
`);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("handles new file added in new version", () => {
|
|
126
|
+
const input = [
|
|
127
|
+
"1: abc ! 1: def x",
|
|
128
|
+
" +## src/new.ts (new) ##",
|
|
129
|
+
" +@@ src/new.ts (new)",
|
|
130
|
+
" ++export const x = 1;",
|
|
131
|
+
" ++export const y = 2;",
|
|
132
|
+
].join("\n");
|
|
133
|
+
expect(postProcessRangeDiff(input)).toMatchInlineSnapshot(`
|
|
134
|
+
"+## src/new.ts (new) ##
|
|
135
|
+
+@@ src/new.ts (new)
|
|
136
|
+
+export const x = 1;
|
|
137
|
+
+export const y = 2;"
|
|
138
|
+
`);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("handles file removed in new version", () => {
|
|
142
|
+
const input = [
|
|
143
|
+
"1: abc ! 1: def x",
|
|
144
|
+
" -## src/old.ts ##",
|
|
145
|
+
" -@@ src/old.ts",
|
|
146
|
+
" --export const x = 1;",
|
|
147
|
+
].join("\n");
|
|
148
|
+
expect(postProcessRangeDiff(input)).toMatchInlineSnapshot(`
|
|
149
|
+
"-## src/old.ts ##
|
|
150
|
+
-@@ src/old.ts
|
|
151
|
+
-export const x = 1;"
|
|
152
|
+
`);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("filters out metadata section via context trimming", () => {
|
|
156
|
+
const input = [
|
|
157
|
+
"1: abc ! 1: def x",
|
|
158
|
+
" @@ Metadata",
|
|
159
|
+
" Author: Test <test@test.com>",
|
|
160
|
+
" ## Commit message ##",
|
|
161
|
+
" x",
|
|
162
|
+
" ## src/file.ts ##",
|
|
163
|
+
" @@ src/file.ts",
|
|
164
|
+
" +const a = 1;",
|
|
165
|
+
" -+const b = 2;",
|
|
166
|
+
" ++const b = 3;",
|
|
167
|
+
" +const c = 4;",
|
|
168
|
+
].join("\n");
|
|
169
|
+
expect(postProcessRangeDiff(input)).toMatchInlineSnapshot(`
|
|
170
|
+
" ## src/file.ts ##
|
|
171
|
+
@@ src/file.ts
|
|
172
|
+
const a = 1;
|
|
173
|
+
-const b = 2;
|
|
174
|
+
+const b = 3;
|
|
175
|
+
const c = 4;"
|
|
176
|
+
`);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("uses custom context line count", () => {
|
|
180
|
+
const contextBefore = Array.from(
|
|
181
|
+
{ length: 5 },
|
|
182
|
+
(_, i) => ` +const line${i + 1} = ${i + 1};`,
|
|
183
|
+
);
|
|
184
|
+
const contextAfter = Array.from(
|
|
185
|
+
{ length: 5 },
|
|
186
|
+
(_, i) => ` +const line${i + 7} = ${i + 7};`,
|
|
187
|
+
);
|
|
188
|
+
const input = [
|
|
189
|
+
"1: abc1234 ! 1: def5678 x",
|
|
190
|
+
" ## src/file.ts ##",
|
|
191
|
+
" @@ src/file.ts",
|
|
192
|
+
...contextBefore,
|
|
193
|
+
" -+const changed = old;",
|
|
194
|
+
" ++const changed = new;",
|
|
195
|
+
...contextAfter,
|
|
196
|
+
].join("\n");
|
|
197
|
+
expect(postProcessRangeDiff(input, 1)).toMatchInlineSnapshot(`
|
|
198
|
+
" ## src/file.ts ##
|
|
199
|
+
@@ src/file.ts
|
|
200
|
+
...
|
|
201
|
+
const line5 = 5;
|
|
202
|
+
-const changed = old;
|
|
203
|
+
+const changed = new;
|
|
204
|
+
const line7 = 7;"
|
|
205
|
+
`);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("handles two separate change regions in the same file", () => {
|
|
209
|
+
const middle = Array.from({ length: 10 }, (_, i) => ` +const mid${i + 1} = ${i + 1};`);
|
|
210
|
+
const input = [
|
|
211
|
+
"1: abc ! 1: def x",
|
|
212
|
+
" ## src/file.ts ##",
|
|
213
|
+
" @@ src/file.ts",
|
|
214
|
+
" -+const first = old;",
|
|
215
|
+
" ++const first = new;",
|
|
216
|
+
...middle,
|
|
217
|
+
" -+const second = old;",
|
|
218
|
+
" ++const second = new;",
|
|
219
|
+
].join("\n");
|
|
220
|
+
expect(postProcessRangeDiff(input)).toMatchInlineSnapshot(`
|
|
221
|
+
" ## src/file.ts ##
|
|
222
|
+
@@ src/file.ts
|
|
223
|
+
-const first = old;
|
|
224
|
+
+const first = new;
|
|
225
|
+
const mid1 = 1;
|
|
226
|
+
const mid2 = 2;
|
|
227
|
+
const mid3 = 3;
|
|
228
|
+
...
|
|
229
|
+
const mid8 = 8;
|
|
230
|
+
const mid9 = 9;
|
|
231
|
+
const mid10 = 10;
|
|
232
|
+
-const second = old;
|
|
233
|
+
+const second = new;"
|
|
234
|
+
`);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { log } from "#app/utils/cli";
|
|
2
|
+
import { $ } from "#app/utils/shell";
|
|
3
|
+
|
|
4
|
+
type ComputeIncrementalDiffParams = {
|
|
5
|
+
baseBranch: string;
|
|
6
|
+
beforeSha: string;
|
|
7
|
+
headSha: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* computes the incremental diff between two versions of a PR using range-diff
|
|
12
|
+
* on virtual squash commits created via `git commit-tree`.
|
|
13
|
+
*
|
|
14
|
+
* each PR version is squashed into a single synthetic commit (merge-base → tip tree),
|
|
15
|
+
* then range-diff compares those two single-commit ranges. this:
|
|
16
|
+
* - isolates each version's net effect (base branch noise eliminated via per-version merge bases)
|
|
17
|
+
* - avoids commit-matching issues that raw range-diff has with rebases/squashes/reordering
|
|
18
|
+
* - creates only loose git objects, no branches or refs (unlike temp-branch squash approaches)
|
|
19
|
+
*
|
|
20
|
+
* unlike fetchAndFormatPrDiff/formatFilesWithLineNumbers, this output has no line numbers.
|
|
21
|
+
* range-diff compares *patches* (diffs-of-diffs), not file trees — its hunk headers are
|
|
22
|
+
* `@@ file.ts` breadcrumbs, not positional `@@ -X,Y +A,B @@` markers. reconstructing
|
|
23
|
+
* line numbers would require cross-referencing with the v2 diff or content-matching against
|
|
24
|
+
* file trees, both of which are fragile (duplicate lines, hunk boundary shifts after rebase).
|
|
25
|
+
* a structured interdiff approach (diff two parsed patches, compare only +/- keys via Myers)
|
|
26
|
+
* could approximate line numbers but loses semantic precision: range-diff understands patch
|
|
27
|
+
* structure natively (rename detection, hunk-aware matching, dual-prefix inner/outer changes),
|
|
28
|
+
* while flat key-sequence comparison can misalign duplicate lines and can't distinguish
|
|
29
|
+
* "new addition to the PR" from "existing code newly modified by the PR". range-diff is the
|
|
30
|
+
* right abstraction here — the incremental diff answers "how did the changeset evolve?",
|
|
31
|
+
* not "where in the file is this?", and forcing positional line numbers onto it would be
|
|
32
|
+
* semantically misleading.
|
|
33
|
+
*
|
|
34
|
+
* alternatives considered:
|
|
35
|
+
* - plain git diff (two-tree or three-dot): includes base branch changes, no PR isolation
|
|
36
|
+
* - patch-text diffing (interdiff / diff-of-diffs): fragile, hunk offset noise on rebase
|
|
37
|
+
* - range-diff on raw commit ranges: confused by commit reorganization across force-pushes
|
|
38
|
+
*/
|
|
39
|
+
export function computeIncrementalDiff(params: ComputeIncrementalDiffParams): string | null {
|
|
40
|
+
try {
|
|
41
|
+
// $1=beforeSha, $2=baseBranch, $3=headSha
|
|
42
|
+
const raw = $(
|
|
43
|
+
"sh",
|
|
44
|
+
[
|
|
45
|
+
"-c",
|
|
46
|
+
'old_base=$(git merge-base "$1" "origin/$2") && ' +
|
|
47
|
+
'new_base=$(git merge-base "$3" "origin/$2") && ' +
|
|
48
|
+
"git range-diff --no-color " +
|
|
49
|
+
'"$old_base..$(git commit-tree "$1^{tree}" -p "$old_base" -m x)" ' +
|
|
50
|
+
'"$new_base..$(git commit-tree "$3^{tree}" -p "$new_base" -m x)"',
|
|
51
|
+
"--",
|
|
52
|
+
params.beforeSha,
|
|
53
|
+
params.baseBranch,
|
|
54
|
+
params.headSha,
|
|
55
|
+
],
|
|
56
|
+
{ log: false },
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
return postProcessRangeDiff(raw);
|
|
60
|
+
} catch (e) {
|
|
61
|
+
log.debug(`» range-diff failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isDiffPrefix(ch: string): boolean {
|
|
67
|
+
return ch === " " || ch === "+" || ch === "-";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* transforms git range-diff output into a clean incremental diff.
|
|
72
|
+
*
|
|
73
|
+
* range-diff content lines have two prefix characters:
|
|
74
|
+
* 1st (outer): range-diff level — space (same in both), + (new only), - (old only)
|
|
75
|
+
* 2nd (inner): original diff level — space (context), + (added), - (removed)
|
|
76
|
+
*
|
|
77
|
+
* stripping the inner prefix produces a standard unified-diff-like output where
|
|
78
|
+
* +/- means "changed between PR versions" rather than "changed vs base branch".
|
|
79
|
+
*
|
|
80
|
+
* uses a streaming approach: a ring buffer of before-context lines is flushed when
|
|
81
|
+
* a change is hit, then afterCount lines of after-context are emitted directly.
|
|
82
|
+
* nearest preceding ## / @@ headers are force-included when outside the context window.
|
|
83
|
+
*/
|
|
84
|
+
export function postProcessRangeDiff(raw: string, contextLines = 3): string | null {
|
|
85
|
+
if (!raw.trim()) return null;
|
|
86
|
+
if (/^\d+:\s+\w+\s+=\s+\d+:/m.test(raw)) return null;
|
|
87
|
+
|
|
88
|
+
type Line = { prefix: string; from: number; to: number; seq: number };
|
|
89
|
+
|
|
90
|
+
const beforeBuf: Line[] = [];
|
|
91
|
+
let lastFileHdr: Line | null = null;
|
|
92
|
+
let lastHunkHdr: Line | null = null;
|
|
93
|
+
let fileHdrEmitted = true;
|
|
94
|
+
let hunkHdrEmitted = true;
|
|
95
|
+
|
|
96
|
+
let out = "";
|
|
97
|
+
let afterRemaining = 0;
|
|
98
|
+
let lastEmittedSeq = -2;
|
|
99
|
+
let seq = 0;
|
|
100
|
+
let hasChanges = false;
|
|
101
|
+
|
|
102
|
+
function emit(line: Line) {
|
|
103
|
+
if (lastEmittedSeq >= 0 && line.seq > lastEmittedSeq + 1) out += `${out ? "\n" : ""}...`;
|
|
104
|
+
out += (out ? "\n" : "") + line.prefix + raw.slice(line.from, line.to);
|
|
105
|
+
lastEmittedSeq = line.seq;
|
|
106
|
+
if (lastFileHdr?.seq === line.seq) fileHdrEmitted = true;
|
|
107
|
+
if (lastHunkHdr?.seq === line.seq) hunkHdrEmitted = true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function flushBefore() {
|
|
111
|
+
if (lastFileHdr && !fileHdrEmitted) emit(lastFileHdr);
|
|
112
|
+
if (lastHunkHdr && !hunkHdrEmitted) emit(lastHunkHdr);
|
|
113
|
+
for (const line of beforeBuf) {
|
|
114
|
+
if (line.seq > lastEmittedSeq) emit(line);
|
|
115
|
+
}
|
|
116
|
+
beforeBuf.length = 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let cursor = 0;
|
|
120
|
+
while (cursor < raw.length) {
|
|
121
|
+
const eol = raw.indexOf("\n", cursor);
|
|
122
|
+
const lineEnd = eol === -1 ? raw.length : eol;
|
|
123
|
+
|
|
124
|
+
if (raw.charCodeAt(cursor) >= 48 && raw.charCodeAt(cursor) <= 57) {
|
|
125
|
+
cursor = lineEnd + 1;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (lineEnd - cursor >= 5 && raw.startsWith(" ", cursor)) {
|
|
130
|
+
const prefix = raw[cursor + 4]!;
|
|
131
|
+
if (isDiffPrefix(prefix)) {
|
|
132
|
+
const contentPos = cursor + 5;
|
|
133
|
+
const isOuterChange = prefix !== " ";
|
|
134
|
+
let line: Line;
|
|
135
|
+
let isChange = false;
|
|
136
|
+
|
|
137
|
+
if (contentPos >= lineEnd) {
|
|
138
|
+
line = { prefix, from: lineEnd, to: lineEnd, seq };
|
|
139
|
+
} else if (isDiffPrefix(raw[contentPos]!)) {
|
|
140
|
+
isChange = isOuterChange;
|
|
141
|
+
line = { prefix, from: contentPos + 1, to: lineEnd, seq };
|
|
142
|
+
} else {
|
|
143
|
+
line = { prefix, from: contentPos, to: lineEnd, seq };
|
|
144
|
+
if (
|
|
145
|
+
raw.startsWith("## ", contentPos) &&
|
|
146
|
+
!raw.startsWith("## Commit message", contentPos)
|
|
147
|
+
) {
|
|
148
|
+
lastFileHdr = line;
|
|
149
|
+
fileHdrEmitted = false;
|
|
150
|
+
lastHunkHdr = null;
|
|
151
|
+
hunkHdrEmitted = true;
|
|
152
|
+
} else if (
|
|
153
|
+
raw.startsWith("@@", contentPos) &&
|
|
154
|
+
!raw.startsWith("@@ Metadata", contentPos)
|
|
155
|
+
) {
|
|
156
|
+
lastHunkHdr = line;
|
|
157
|
+
hunkHdrEmitted = false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (isChange) {
|
|
162
|
+
hasChanges = true;
|
|
163
|
+
flushBefore();
|
|
164
|
+
emit(line);
|
|
165
|
+
afterRemaining = contextLines;
|
|
166
|
+
} else if (afterRemaining > 0) {
|
|
167
|
+
emit(line);
|
|
168
|
+
afterRemaining--;
|
|
169
|
+
} else {
|
|
170
|
+
if (beforeBuf.length >= contextLines) beforeBuf.shift();
|
|
171
|
+
beforeBuf.push(line);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
seq++;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
cursor = lineEnd + 1;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return hasChanges ? out : null;
|
|
182
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { parseRemediationCommand, STRATEGY_REPLY_HINT } from "#app/utils/remediationCommand";
|
|
3
|
+
|
|
4
|
+
describe("parseRemediationCommand (§3.12)", () => {
|
|
5
|
+
it("returns null when the body has no mention", () => {
|
|
6
|
+
expect(parseRemediationCommand("please fix #abc123")).toBeNull();
|
|
7
|
+
expect(parseRemediationCommand(undefined)).toBeNull();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("parses `fix #<id>` and `fix <id>` into a concern command (lowercased)", () => {
|
|
11
|
+
expect(parseRemediationCommand("@terramend fix #3a9f1c2")).toEqual({
|
|
12
|
+
kind: "concern",
|
|
13
|
+
concernRef: "3a9f1c2",
|
|
14
|
+
});
|
|
15
|
+
expect(parseRemediationCommand("@terramend fix ABCDEF12")).toEqual({
|
|
16
|
+
kind: "concern",
|
|
17
|
+
concernRef: "abcdef12",
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("parses `fix all <sev>-severity` and `fix all <sev>`", () => {
|
|
22
|
+
expect(parseRemediationCommand("@terramend fix all high-severity")).toEqual({
|
|
23
|
+
kind: "severity",
|
|
24
|
+
severity: "high",
|
|
25
|
+
});
|
|
26
|
+
expect(parseRemediationCommand("@terramend fix all low")).toEqual({
|
|
27
|
+
kind: "severity",
|
|
28
|
+
severity: "low",
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("parses `fix <sev>-severity` without `all`", () => {
|
|
33
|
+
expect(parseRemediationCommand("@terramend fix critical-severity")).toEqual({
|
|
34
|
+
kind: "severity",
|
|
35
|
+
severity: "critical",
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("parses `fix all` as everything", () => {
|
|
40
|
+
expect(parseRemediationCommand("@terramend fix all")).toEqual({ kind: "all" });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("parses a file target", () => {
|
|
44
|
+
expect(parseRemediationCommand("@terramend fix modules/net/main.tf")).toEqual({
|
|
45
|
+
kind: "file",
|
|
46
|
+
file: "modules/net/main.tf",
|
|
47
|
+
});
|
|
48
|
+
expect(parseRemediationCommand("@terramend fix prod.tfvars")).toEqual({
|
|
49
|
+
kind: "file",
|
|
50
|
+
file: "prod.tfvars",
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("tolerates surrounding prose and the [bot] suffix", () => {
|
|
55
|
+
expect(
|
|
56
|
+
parseRemediationCommand("hey @terramend[bot] please fix all medium-severity thanks"),
|
|
57
|
+
).toEqual({
|
|
58
|
+
kind: "severity",
|
|
59
|
+
severity: "medium",
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("returns null for a mention that isn't a fix command", () => {
|
|
64
|
+
expect(parseRemediationCommand("@terramend what's the status?")).toBeNull();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("prefers `fix all <sev>` over a bare file/concern interpretation", () => {
|
|
68
|
+
// "all" is a keyword, not a file/concern — must not be read as kind:file.
|
|
69
|
+
expect(parseRemediationCommand("@terramend fix all")).toEqual({ kind: "all" });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("does NOT read prose 'fix all the bugs' as the fix-all command", () => {
|
|
73
|
+
// regression: a non-severity word after "all" is prose, not a command.
|
|
74
|
+
expect(parseRemediationCommand("@terramend please fix all the bugs")).toBeNull();
|
|
75
|
+
expect(parseRemediationCommand("@terramend fix all outstanding issues")).toBeNull();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("classifies a hex-named .tf file as a file, not a concern id", () => {
|
|
79
|
+
// regression: `deadbeef.tf` stem is all-hex but it's a filename.
|
|
80
|
+
expect(parseRemediationCommand("@terramend fix deadbeef.tf")).toEqual({
|
|
81
|
+
kind: "file",
|
|
82
|
+
file: "deadbeef.tf",
|
|
83
|
+
});
|
|
84
|
+
expect(parseRemediationCommand("@terramend fix 0badf00d.tfvars")).toEqual({
|
|
85
|
+
kind: "file",
|
|
86
|
+
file: "0badf00d.tfvars",
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("still reads a bare hex id (no extension) as a concern", () => {
|
|
91
|
+
expect(parseRemediationCommand("@terramend fix #deadbeef please")).toEqual({
|
|
92
|
+
kind: "concern",
|
|
93
|
+
concernRef: "deadbeef",
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("parseRemediationCommand — strategy selection (§26)", () => {
|
|
99
|
+
it("attaches a strategy label to a `fix #<id>` command (letter, upper-normalised)", () => {
|
|
100
|
+
expect(parseRemediationCommand("@terramend fix #3a9f1c2 with strategy B")).toEqual({
|
|
101
|
+
kind: "concern",
|
|
102
|
+
concernRef: "3a9f1c2",
|
|
103
|
+
strategy: "B",
|
|
104
|
+
});
|
|
105
|
+
expect(parseRemediationCommand("@terramend fix #3a9f1c2 using strategy a")).toEqual({
|
|
106
|
+
kind: "concern",
|
|
107
|
+
concernRef: "3a9f1c2",
|
|
108
|
+
strategy: "A",
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("accepts `option` / `approach` and a digit label", () => {
|
|
113
|
+
expect(parseRemediationCommand("@terramend fix #deadbeef option 2")).toEqual({
|
|
114
|
+
kind: "concern",
|
|
115
|
+
concernRef: "deadbeef",
|
|
116
|
+
strategy: "2",
|
|
117
|
+
});
|
|
118
|
+
expect(parseRemediationCommand("@terramend fix #deadbeef approach C")).toEqual({
|
|
119
|
+
kind: "concern",
|
|
120
|
+
concernRef: "deadbeef",
|
|
121
|
+
strategy: "C",
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("leaves strategy unset on a plain `fix #<id>`", () => {
|
|
126
|
+
const result = parseRemediationCommand("@terramend fix #3a9f1c2");
|
|
127
|
+
expect(result).toEqual({ kind: "concern", concernRef: "3a9f1c2" });
|
|
128
|
+
expect((result as { strategy?: string }).strategy).toBeUndefined();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("reads a bare strategy reply (concern comes from the thread)", () => {
|
|
132
|
+
expect(parseRemediationCommand("@terramend strategy B")).toEqual({
|
|
133
|
+
kind: "strategy",
|
|
134
|
+
strategy: "B",
|
|
135
|
+
});
|
|
136
|
+
expect(parseRemediationCommand("@terramend let's go with option 3")).toEqual({
|
|
137
|
+
kind: "strategy",
|
|
138
|
+
strategy: "3",
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("keeps an explicit #<id> when a strategy reply names one without `fix`", () => {
|
|
143
|
+
expect(parseRemediationCommand("@terramend apply strategy A to #deadbeef")).toEqual({
|
|
144
|
+
kind: "concern",
|
|
145
|
+
concernRef: "deadbeef",
|
|
146
|
+
strategy: "A",
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("does not read prose as a strategy pick", () => {
|
|
151
|
+
expect(parseRemediationCommand("@terramend this is a solid strategy overall")).toBeNull();
|
|
152
|
+
expect(parseRemediationCommand("@terramend what's the best approach here?")).toBeNull();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("round-trips the canonical reply hint through the parser", () => {
|
|
156
|
+
const replied = STRATEGY_REPLY_HINT.replace("<concern-id>", "3a9f1c2").replace("<A|B|C>", "B");
|
|
157
|
+
expect(parseRemediationCommand(replied)).toEqual({
|
|
158
|
+
kind: "concern",
|
|
159
|
+
concernRef: "3a9f1c2",
|
|
160
|
+
strategy: "B",
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §3.12 Comment-command interface. A developer can scope a remediation run from
|
|
3
|
+
* a PR/issue comment that mentions the bot:
|
|
4
|
+
*
|
|
5
|
+
* @terramend fix #3a9f1c2 → fix exactly one concern (by id or short id)
|
|
6
|
+
* @terramend fix all high-severity → fix every concern at/above a severity
|
|
7
|
+
* @terramend fix all → fix everything (still bounded by max_prs)
|
|
8
|
+
* @terramend fix main.tf → fix one file's group
|
|
9
|
+
*
|
|
10
|
+
* §26 Propose, then let me steer — when a non-trivial finding has several
|
|
11
|
+
* genuinely distinct valid fixes, Remediate proposes 2–3 labelled strategies in
|
|
12
|
+
* a comment instead of guessing, and the reviewer picks one by replying:
|
|
13
|
+
*
|
|
14
|
+
* @terramend fix #3a9f1c2 with strategy B → apply strategy B to that concern
|
|
15
|
+
* @terramend strategy 2 → pick strategy 2 (concern from the thread)
|
|
16
|
+
*
|
|
17
|
+
* The parsing is pure + deterministic so the scoping doesn't depend on the
|
|
18
|
+
* model's reading of the comment. The Remediate mode applies the parsed scope
|
|
19
|
+
* (which group(s) to act on, and which strategy) instead of the default
|
|
20
|
+
* "highest-severity group, agent's-choice fix".
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export type RemediationCommand =
|
|
24
|
+
| { kind: "concern"; concernRef: string; strategy?: string }
|
|
25
|
+
| { kind: "severity"; severity: Severity }
|
|
26
|
+
| { kind: "file"; file: string }
|
|
27
|
+
// §26 — a bare strategy pick (e.g. an in-thread reply to a proposal); the
|
|
28
|
+
// concern is resolved from the comment thread the run was triggered on.
|
|
29
|
+
| { kind: "strategy"; strategy: string }
|
|
30
|
+
| { kind: "all" };
|
|
31
|
+
|
|
32
|
+
const SEVERITIES = ["critical", "high", "medium", "low", "info"] as const;
|
|
33
|
+
type Severity = (typeof SEVERITIES)[number];
|
|
34
|
+
|
|
35
|
+
/** the bot handles a mention can use (with or without the `[bot]` suffix). */
|
|
36
|
+
const MENTION = /@terramend(?:\[bot\])?\b/i;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* §26 — the canonical phrasing the proposal comment must tell reviewers to reply
|
|
40
|
+
* with. Lives next to the parser that accepts it so the proposal template (in
|
|
41
|
+
* the Remediate prompt) and the parser can never drift apart. `<concern-id>` and
|
|
42
|
+
* `<A|B|C>` are placeholders the agent fills with the real id and the offered
|
|
43
|
+
* strategy labels.
|
|
44
|
+
*/
|
|
45
|
+
export const STRATEGY_REPLY_HINT = "@terramend fix #<concern-id> with strategy <A|B|C>";
|
|
46
|
+
|
|
47
|
+
// a strategy label is a single letter (A–Z, normalised to upper) or digit (1–9),
|
|
48
|
+
// introduced by `strategy` / `option` / `approach`. The single-char + `\b` bound
|
|
49
|
+
// keeps it off prose ("a good strategy overall" has no single-char boundary).
|
|
50
|
+
const STRATEGY = /\b(?:strategy|option|approach)\s+#?([A-Za-z]|[1-9])\b/i;
|
|
51
|
+
|
|
52
|
+
function parseStrategyToken(text: string): string | undefined {
|
|
53
|
+
const m = text.match(STRATEGY);
|
|
54
|
+
if (!m) return undefined;
|
|
55
|
+
const tok = m[1]!;
|
|
56
|
+
return /[A-Za-z]/.test(tok) ? tok.toUpperCase() : tok;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Parse a `@terramend fix …` command out of a comment body. Returns null when
|
|
61
|
+
* the body isn't a recognised fix command (the run then falls back to its
|
|
62
|
+
* default scope). Tolerant of surrounding prose — it scans for the mention then
|
|
63
|
+
* the `fix` verb and its argument.
|
|
64
|
+
*/
|
|
65
|
+
export function parseRemediationCommand(body: string | undefined): RemediationCommand | null {
|
|
66
|
+
if (!body) return null;
|
|
67
|
+
if (!MENTION.test(body)) return null;
|
|
68
|
+
|
|
69
|
+
// everything after the mention; we keep original case for the file/concern-ref
|
|
70
|
+
// capture (the lower-casing happens per match).
|
|
71
|
+
const afterMention = body.slice(body.search(MENTION));
|
|
72
|
+
const isSeverity = (s: string): s is Severity => (SEVERITIES as readonly string[]).includes(s);
|
|
73
|
+
|
|
74
|
+
// `fix all <sev>[-severity]` / `fix all` — but a NON-severity word after "all"
|
|
75
|
+
// (prose like "fix all the bugs") is NOT the command: fall through rather than
|
|
76
|
+
// silently treating it as "fix everything".
|
|
77
|
+
const all = afterMention.match(/\bfix\s+all\b(?:\s+([a-z]+)(?:\s*-?\s*severity)?)?/i);
|
|
78
|
+
if (all) {
|
|
79
|
+
const trailing = all[1]?.toLowerCase();
|
|
80
|
+
if (!trailing) return { kind: "all" };
|
|
81
|
+
if (isSeverity(trailing)) return { kind: "severity", severity: trailing };
|
|
82
|
+
// a non-severity word followed "all" → prose, not a command. fall through.
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// `fix <sev>-severity` (without "all")
|
|
86
|
+
const sevOnly = afterMention.match(/\bfix\s+([a-z]+)\s*-?\s*severity\b/i);
|
|
87
|
+
if (sevOnly) {
|
|
88
|
+
const sev = sevOnly[1]!.toLowerCase();
|
|
89
|
+
if (isSeverity(sev)) return { kind: "severity", severity: sev };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// `fix <path>.tf` / `fix <path>.tfvars` — a specific file's group. Checked
|
|
93
|
+
// BEFORE the concern-id form so an all-hex filename stem (e.g. `deadbeef.tf`)
|
|
94
|
+
// isn't mis-read as a concern id.
|
|
95
|
+
const file = afterMention.match(/\bfix\s+([^\s#]+\.tf(?:vars)?)\b/i);
|
|
96
|
+
if (file) return { kind: "file", file: file[1]! };
|
|
97
|
+
|
|
98
|
+
// `fix #<id>` or `fix <id>` — a concern id is hex (content hash, 6–40 chars).
|
|
99
|
+
// §26 — carries an optional strategy label when the comment selects one.
|
|
100
|
+
const concern = afterMention.match(/\bfix\s+#?([0-9a-f]{6,40})\b/i);
|
|
101
|
+
if (concern) {
|
|
102
|
+
const concernRef = concern[1]!.toLowerCase();
|
|
103
|
+
const strategy = parseStrategyToken(afterMention);
|
|
104
|
+
return strategy ? { kind: "concern", concernRef, strategy } : { kind: "concern", concernRef };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// §26 — a strategy pick with no `fix` verb (e.g. an in-thread reply). If the
|
|
108
|
+
// reviewer still named a `#<id>`, keep it as a scoped concern; otherwise the
|
|
109
|
+
// concern is resolved from the thread the comment lives on.
|
|
110
|
+
const strategy = parseStrategyToken(afterMention);
|
|
111
|
+
if (strategy) {
|
|
112
|
+
const id = afterMention.match(/#([0-9a-f]{6,40})\b/i);
|
|
113
|
+
return id
|
|
114
|
+
? { kind: "concern", concernRef: id[1]!.toLowerCase(), strategy }
|
|
115
|
+
: { kind: "strategy", strategy };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return null;
|
|
119
|
+
}
|