@mhingston5/lasso 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/README.md +707 -0
- package/docs/agent-wrangling.png +0 -0
- package/package.json +26 -0
- package/src/capabilities/matcher.ts +25 -0
- package/src/capabilities/registry.ts +103 -0
- package/src/capabilities/types.ts +15 -0
- package/src/cir/lower.ts +253 -0
- package/src/cir/optimize.ts +251 -0
- package/src/cir/types.ts +131 -0
- package/src/cir/validate.ts +265 -0
- package/src/compiler/compile.ts +601 -0
- package/src/compiler/feedback.ts +471 -0
- package/src/compiler/runtime-helpers.ts +455 -0
- package/src/composition/chain.ts +58 -0
- package/src/composition/conditional.ts +76 -0
- package/src/composition/parallel.ts +75 -0
- package/src/composition/types.ts +105 -0
- package/src/environment/analyzer.ts +56 -0
- package/src/environment/discovery.ts +179 -0
- package/src/environment/types.ts +68 -0
- package/src/failures/classifiers.ts +134 -0
- package/src/failures/generator.ts +421 -0
- package/src/failures/map-reference-failures.ts +23 -0
- package/src/failures/ontology.ts +210 -0
- package/src/failures/recovery.ts +214 -0
- package/src/failures/types.ts +14 -0
- package/src/index.ts +67 -0
- package/src/memory/advisor.ts +132 -0
- package/src/memory/extractor.ts +166 -0
- package/src/memory/store.ts +107 -0
- package/src/memory/types.ts +53 -0
- package/src/metaharness/engine.ts +256 -0
- package/src/metaharness/predictor.ts +168 -0
- package/src/metaharness/types.ts +40 -0
- package/src/mutation/derive.ts +308 -0
- package/src/mutation/diff.ts +52 -0
- package/src/mutation/engine.ts +256 -0
- package/src/mutation/types.ts +84 -0
- package/src/pi/command-input.ts +209 -0
- package/src/pi/commands.ts +351 -0
- package/src/pi/extension.ts +16 -0
- package/src/planner/synthesize.ts +83 -0
- package/src/planner/template-rules.ts +183 -0
- package/src/planner/types.ts +42 -0
- package/src/reference/catalog.ts +128 -0
- package/src/reference/patch-validation-strategies.ts +170 -0
- package/src/reference/patch-validation.ts +174 -0
- package/src/reference/pr-review-merge.ts +155 -0
- package/src/reference/strategies.ts +126 -0
- package/src/reference/types.ts +33 -0
- package/src/replanner/risk-rules.ts +161 -0
- package/src/replanner/runtime.ts +308 -0
- package/src/replanner/synthesize.ts +619 -0
- package/src/replanner/types.ts +73 -0
- package/src/spec/schema.ts +254 -0
- package/src/spec/types.ts +319 -0
- package/src/spec/validate.ts +296 -0
- package/src/state/snapshots.ts +43 -0
- package/src/state/types.ts +12 -0
- package/src/synthesis/graph-builder.ts +267 -0
- package/src/synthesis/harness-builder.ts +113 -0
- package/src/synthesis/intent-ir.ts +63 -0
- package/src/synthesis/policy-builder.ts +320 -0
- package/src/synthesis/risk-analyzer.ts +182 -0
- package/src/synthesis/skill-parser.ts +441 -0
- package/src/verification/engine.ts +230 -0
- package/src/versioning/file-store.ts +103 -0
- package/src/versioning/history.ts +43 -0
- package/src/versioning/store.ts +16 -0
- package/src/versioning/types.ts +31 -0
- package/test/capabilities/matcher.test.ts +67 -0
- package/test/capabilities/registry.test.ts +136 -0
- package/test/capabilities/synthesis.test.ts +264 -0
- package/test/cir/lower.test.ts +417 -0
- package/test/cir/optimize.test.ts +266 -0
- package/test/cir/validate.test.ts +368 -0
- package/test/compiler/adaptive-runtime.test.ts +157 -0
- package/test/compiler/compile.test.ts +1198 -0
- package/test/compiler/feedback.test.ts +784 -0
- package/test/compiler/guardrails.test.ts +191 -0
- package/test/compiler/trace.test.ts +404 -0
- package/test/composition/chain.test.ts +328 -0
- package/test/composition/conditional.test.ts +241 -0
- package/test/composition/parallel.test.ts +215 -0
- package/test/environment/analyzer.test.ts +204 -0
- package/test/environment/discovery.test.ts +149 -0
- package/test/failures/classifiers.test.ts +287 -0
- package/test/failures/generator.test.ts +203 -0
- package/test/failures/ontology.test.ts +439 -0
- package/test/failures/recovery.test.ts +300 -0
- package/test/helpers/createFixtureRepo.ts +84 -0
- package/test/helpers/createPatchValidationFixture.ts +144 -0
- package/test/helpers/runCompiledWorkflow.ts +208 -0
- package/test/memory/advisor.test.ts +332 -0
- package/test/memory/extractor.test.ts +295 -0
- package/test/memory/store.test.ts +244 -0
- package/test/metaharness/engine.test.ts +575 -0
- package/test/metaharness/predictor.test.ts +436 -0
- package/test/mutation/derive-failure.test.ts +209 -0
- package/test/mutation/engine.test.ts +622 -0
- package/test/package-smoke.test.ts +29 -0
- package/test/pi/command-input.test.ts +153 -0
- package/test/pi/commands.test.ts +623 -0
- package/test/planner/classify-template.test.ts +32 -0
- package/test/planner/synthesize.test.ts +901 -0
- package/test/reference/PatchValidation.failures.test.ts +137 -0
- package/test/reference/PatchValidation.test.ts +326 -0
- package/test/reference/PrReviewMerge.failures.test.ts +121 -0
- package/test/reference/PrReviewMerge.test.ts +55 -0
- package/test/reference/catalog-open.test.ts +70 -0
- package/test/replanner/runtime.test.ts +207 -0
- package/test/replanner/synthesize.test.ts +303 -0
- package/test/spec/validate.test.ts +1056 -0
- package/test/state/snapshots.test.ts +264 -0
- package/test/synthesis/custom-workflow.test.ts +264 -0
- package/test/synthesis/graph-builder.test.ts +370 -0
- package/test/synthesis/harness-builder.test.ts +128 -0
- package/test/synthesis/policy-builder.test.ts +149 -0
- package/test/synthesis/risk-analyzer.test.ts +230 -0
- package/test/synthesis/skill-parser.test.ts +796 -0
- package/test/verification/engine.test.ts +509 -0
- package/test/versioning/history.test.ts +144 -0
- package/test/versioning/store.test.ts +254 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { HarnessSpec } from "../spec/types.js";
|
|
2
|
+
import {
|
|
3
|
+
buildHumanApprovedCondition,
|
|
4
|
+
buildLoadDiffTool,
|
|
5
|
+
buildMergeSucceededCondition,
|
|
6
|
+
buildMergeTool,
|
|
7
|
+
buildPostMergeCheckTool,
|
|
8
|
+
buildReviewPrompt,
|
|
9
|
+
buildVerificationPassedCondition,
|
|
10
|
+
buildVerificationTool,
|
|
11
|
+
buildVerifyPostMergeTool,
|
|
12
|
+
prReviewMergeNodeIds,
|
|
13
|
+
} from "./strategies.js";
|
|
14
|
+
import type { LocalPrBundle } from "./types.js";
|
|
15
|
+
|
|
16
|
+
export function buildPrReviewMergeHarnessSpec(input: LocalPrBundle): HarnessSpec {
|
|
17
|
+
return {
|
|
18
|
+
name: "pr-review-merge",
|
|
19
|
+
executionPolicy: {
|
|
20
|
+
timeout: 180,
|
|
21
|
+
},
|
|
22
|
+
humanPolicy: {
|
|
23
|
+
defaultTimeout: 300,
|
|
24
|
+
},
|
|
25
|
+
observabilityPolicy: {
|
|
26
|
+
tracing: true,
|
|
27
|
+
logLevel: "info",
|
|
28
|
+
},
|
|
29
|
+
graph: {
|
|
30
|
+
entryNodeId: prReviewMergeNodeIds.loadPr,
|
|
31
|
+
nodes: [
|
|
32
|
+
{
|
|
33
|
+
id: prReviewMergeNodeIds.loadPr,
|
|
34
|
+
label: "Load PR context",
|
|
35
|
+
kind: "tool",
|
|
36
|
+
...buildLoadDiffTool(input),
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: prReviewMergeNodeIds.reviewPr,
|
|
40
|
+
kind: "llm",
|
|
41
|
+
provider: "anthropic",
|
|
42
|
+
model: "claude-sonnet",
|
|
43
|
+
prompt: buildReviewPrompt(input),
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: prReviewMergeNodeIds.runVerification,
|
|
47
|
+
kind: "tool",
|
|
48
|
+
...buildVerificationTool(input),
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: prReviewMergeNodeIds.mergeResults,
|
|
52
|
+
kind: "merge",
|
|
53
|
+
waitFor: [prReviewMergeNodeIds.reviewPr, prReviewMergeNodeIds.runVerification],
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: prReviewMergeNodeIds.gateMerge,
|
|
57
|
+
kind: "condition",
|
|
58
|
+
condition: buildVerificationPassedCondition(),
|
|
59
|
+
thenNodeId: prReviewMergeNodeIds.humanApprove,
|
|
60
|
+
elseNodeId: prReviewMergeNodeIds.rejectVerification,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: prReviewMergeNodeIds.humanApprove,
|
|
64
|
+
kind: "human",
|
|
65
|
+
prompt: `Approve merge of ${input.sourceBranch} into ${input.targetBranch}?`,
|
|
66
|
+
interactionType: "approval",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: prReviewMergeNodeIds.checkHumanApproval,
|
|
70
|
+
kind: "condition",
|
|
71
|
+
condition: buildHumanApprovedCondition(),
|
|
72
|
+
thenNodeId: prReviewMergeNodeIds.mergeBranch,
|
|
73
|
+
elseNodeId: prReviewMergeNodeIds.rejectHuman,
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: prReviewMergeNodeIds.mergeBranch,
|
|
77
|
+
kind: "tool",
|
|
78
|
+
...buildMergeTool(input),
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: prReviewMergeNodeIds.checkMergeResult,
|
|
82
|
+
kind: "condition",
|
|
83
|
+
condition: buildMergeSucceededCondition(),
|
|
84
|
+
thenNodeId: prReviewMergeNodeIds.postMergeCheck,
|
|
85
|
+
elseNodeId: prReviewMergeNodeIds.mergeConflict,
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: prReviewMergeNodeIds.postMergeCheck,
|
|
89
|
+
kind: "tool",
|
|
90
|
+
...buildPostMergeCheckTool(input),
|
|
91
|
+
retryPolicy: {
|
|
92
|
+
maxAttempts: 2,
|
|
93
|
+
backoff: "constant",
|
|
94
|
+
initialDelay: 0,
|
|
95
|
+
retryOn: ["transient"],
|
|
96
|
+
},
|
|
97
|
+
executionPolicy: {
|
|
98
|
+
failureClassification: [
|
|
99
|
+
{
|
|
100
|
+
pattern: "retryable post-merge failure",
|
|
101
|
+
category: "transient",
|
|
102
|
+
retry: true,
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
verificationPolicy: {
|
|
107
|
+
rules: [
|
|
108
|
+
{
|
|
109
|
+
kind: "tool",
|
|
110
|
+
checkNodeId: prReviewMergeNodeIds.verifyPostMerge,
|
|
111
|
+
onFail: "block",
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: prReviewMergeNodeIds.verifyPostMerge,
|
|
118
|
+
kind: "tool",
|
|
119
|
+
...buildVerifyPostMergeTool(input),
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
id: prReviewMergeNodeIds.completeSuccess,
|
|
123
|
+
kind: "subworkflow",
|
|
124
|
+
specRef: prReviewMergeNodeIds.completeSuccess,
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
id: prReviewMergeNodeIds.rejectVerification,
|
|
128
|
+
kind: "subworkflow",
|
|
129
|
+
specRef: prReviewMergeNodeIds.rejectVerification,
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
id: prReviewMergeNodeIds.rejectHuman,
|
|
133
|
+
kind: "subworkflow",
|
|
134
|
+
specRef: prReviewMergeNodeIds.rejectHuman,
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
id: prReviewMergeNodeIds.mergeConflict,
|
|
138
|
+
kind: "subworkflow",
|
|
139
|
+
specRef: prReviewMergeNodeIds.mergeConflict,
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
edges: [
|
|
143
|
+
{ from: prReviewMergeNodeIds.loadPr, to: prReviewMergeNodeIds.reviewPr },
|
|
144
|
+
{ from: prReviewMergeNodeIds.loadPr, to: prReviewMergeNodeIds.runVerification },
|
|
145
|
+
{ from: prReviewMergeNodeIds.reviewPr, to: prReviewMergeNodeIds.mergeResults },
|
|
146
|
+
{ from: prReviewMergeNodeIds.runVerification, to: prReviewMergeNodeIds.mergeResults },
|
|
147
|
+
{ from: prReviewMergeNodeIds.mergeResults, to: prReviewMergeNodeIds.gateMerge },
|
|
148
|
+
{ from: prReviewMergeNodeIds.humanApprove, to: prReviewMergeNodeIds.checkHumanApproval },
|
|
149
|
+
{ from: prReviewMergeNodeIds.mergeBranch, to: prReviewMergeNodeIds.checkMergeResult },
|
|
150
|
+
{ from: prReviewMergeNodeIds.postMergeCheck, to: prReviewMergeNodeIds.verifyPostMerge },
|
|
151
|
+
{ from: prReviewMergeNodeIds.verifyPostMerge, to: prReviewMergeNodeIds.completeSuccess },
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { LocalPrBundle } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export const prReviewMergeNodeIds = {
|
|
4
|
+
loadPr: "load-pr",
|
|
5
|
+
reviewPr: "review-pr",
|
|
6
|
+
runVerification: "run-verification",
|
|
7
|
+
mergeResults: "merge-results",
|
|
8
|
+
gateMerge: "gate-merge",
|
|
9
|
+
humanApprove: "human-approve",
|
|
10
|
+
checkHumanApproval: "check-human-approval",
|
|
11
|
+
mergeBranch: "merge-branch",
|
|
12
|
+
checkMergeResult: "check-merge-result",
|
|
13
|
+
postMergeCheck: "post-merge-check",
|
|
14
|
+
verifyPostMerge: "verify-post-merge",
|
|
15
|
+
completeSuccess: "complete-success",
|
|
16
|
+
rejectVerification: "reject-verification",
|
|
17
|
+
rejectHuman: "reject-human",
|
|
18
|
+
mergeConflict: "merge-conflict",
|
|
19
|
+
} as const;
|
|
20
|
+
|
|
21
|
+
export function buildReviewPrompt(bundle: LocalPrBundle): string {
|
|
22
|
+
return [
|
|
23
|
+
"Review this local pull request and return JSON with a single boolean field `approved` and a short `summary`.",
|
|
24
|
+
`Repository: ${bundle.repoPath}`,
|
|
25
|
+
`Target branch: ${bundle.targetBranch}`,
|
|
26
|
+
`Source branch: ${bundle.sourceBranch}`,
|
|
27
|
+
`Instructions: ${bundle.reviewInstructions}`,
|
|
28
|
+
].join("\n");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function buildLoadDiffTool(bundle: LocalPrBundle) {
|
|
32
|
+
return buildBashTool(
|
|
33
|
+
[
|
|
34
|
+
`git checkout ${shellQuote(bundle.sourceBranch)} >/dev/null 2>&1`,
|
|
35
|
+
`git diff --stat ${shellQuote(`${bundle.targetBranch}...${bundle.sourceBranch}`)}`,
|
|
36
|
+
].join("\n"),
|
|
37
|
+
bundle.repoPath,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function buildVerificationTool(bundle: LocalPrBundle) {
|
|
42
|
+
const lines = [
|
|
43
|
+
`git checkout ${shellQuote(bundle.sourceBranch)} >/dev/null 2>&1`,
|
|
44
|
+
...bundle.verificationCommands.flatMap(command => [
|
|
45
|
+
`if ! (${command}); then`,
|
|
46
|
+
` printf '%s\\n' ${shellQuote(JSON.stringify({ passed: false, command }))}`,
|
|
47
|
+
" exit 0",
|
|
48
|
+
"fi",
|
|
49
|
+
]),
|
|
50
|
+
`printf '%s\\n' ${shellQuote(JSON.stringify({ passed: true }))}`,
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
return buildBashTool(lines.join("\n"), bundle.repoPath);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function buildMergeTool(bundle: LocalPrBundle) {
|
|
57
|
+
return buildBashTool(
|
|
58
|
+
[
|
|
59
|
+
`git checkout ${shellQuote(bundle.targetBranch)} >/dev/null 2>&1`,
|
|
60
|
+
`if git merge --no-ff --no-edit ${shellQuote(bundle.sourceBranch)} >/dev/null 2>&1; then`,
|
|
61
|
+
` printf '%s\\n' ${shellQuote(JSON.stringify({ success: true, conflict: false }))}`,
|
|
62
|
+
" exit 0",
|
|
63
|
+
"fi",
|
|
64
|
+
"if git status --porcelain | grep -Eq '^(AA|DD|UU|AU|UA|DU|UD) '; then",
|
|
65
|
+
" git merge --abort >/dev/null 2>&1 || true",
|
|
66
|
+
` printf '%s\\n' ${shellQuote(JSON.stringify({ success: false, conflict: true }))}`,
|
|
67
|
+
" exit 0",
|
|
68
|
+
"fi",
|
|
69
|
+
`printf '%s\\n' ${shellQuote(JSON.stringify({ success: false, conflict: false }))}`,
|
|
70
|
+
"exit 1",
|
|
71
|
+
].join("\n"),
|
|
72
|
+
bundle.repoPath,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function buildPostMergeCheckTool(bundle: LocalPrBundle) {
|
|
77
|
+
return buildBashTool(
|
|
78
|
+
[
|
|
79
|
+
`git checkout ${shellQuote(bundle.targetBranch)} >/dev/null 2>&1`,
|
|
80
|
+
"if [ -f .lasso-post-merge-fail ]; then",
|
|
81
|
+
" echo 'retryable post-merge failure' >&2",
|
|
82
|
+
" exit 1",
|
|
83
|
+
"fi",
|
|
84
|
+
`printf '%s\\n' ${shellQuote(JSON.stringify({ passed: true }))}`,
|
|
85
|
+
].join("\n"),
|
|
86
|
+
bundle.repoPath,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function buildVerifyPostMergeTool(bundle: LocalPrBundle) {
|
|
91
|
+
return buildBashTool(
|
|
92
|
+
[
|
|
93
|
+
`git checkout ${shellQuote(bundle.targetBranch)} >/dev/null 2>&1`,
|
|
94
|
+
`printf '%s\\n' ${shellQuote(JSON.stringify({ passed: true }))}`,
|
|
95
|
+
].join("\n"),
|
|
96
|
+
bundle.repoPath,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function buildVerificationPassedCondition(): string {
|
|
101
|
+
return `${prReviewMergeNodeIds.mergeResults}.${prReviewMergeNodeIds.runVerification}.passed`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function buildHumanApprovedCondition(): string {
|
|
105
|
+
return `${prReviewMergeNodeIds.humanApprove}.approved`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function buildMergeSucceededCondition(): string {
|
|
109
|
+
return `${prReviewMergeNodeIds.mergeBranch}.success`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function buildBashTool(script: string, cwd: string) {
|
|
113
|
+
return {
|
|
114
|
+
tool: "bash",
|
|
115
|
+
args: ["-lc", script],
|
|
116
|
+
cwd,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function shellQuote(value: string): string {
|
|
121
|
+
if (/^[A-Za-z0-9_./:=+-]+$/.test(value)) {
|
|
122
|
+
return value;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return `'${value.replace(/'/g, `'\"'\"'`)}'`;
|
|
126
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface LocalPrBundle {
|
|
2
|
+
repoPath: string;
|
|
3
|
+
sourceBranch: string;
|
|
4
|
+
targetBranch: string;
|
|
5
|
+
reviewInstructions: string;
|
|
6
|
+
verificationCommands: string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type LocalCandidateSource =
|
|
10
|
+
| { kind: "branch"; value: string }
|
|
11
|
+
| { kind: "patchFile"; value: string };
|
|
12
|
+
|
|
13
|
+
export interface LocalPatchValidationBundle {
|
|
14
|
+
/** Absolute path to the local repository to validate against */
|
|
15
|
+
repoPath: string;
|
|
16
|
+
/**
|
|
17
|
+
* Git ref to check out as the baseline before applying the candidate.
|
|
18
|
+
* Precondition: this ref must resolve cleanly in the provided `repoPath`
|
|
19
|
+
* worktree. A checkout failure is treated as an unrecoverable setup error
|
|
20
|
+
* and is not routed to any terminal node — the workflow aborts immediately.
|
|
21
|
+
*/
|
|
22
|
+
baselineRef: string;
|
|
23
|
+
/** The candidate fix to validate — a branch or a patch file */
|
|
24
|
+
candidateSource: LocalCandidateSource;
|
|
25
|
+
/** Commands that reproduce the bug; expected to fail on baseline, pass after fix */
|
|
26
|
+
reproduceCommands: string[];
|
|
27
|
+
/** Commands that must still pass after the fix is applied (regression guard) */
|
|
28
|
+
verificationCommands: string[];
|
|
29
|
+
/** Instructions for the LLM summary / human review prompt */
|
|
30
|
+
reviewInstructions: string;
|
|
31
|
+
/** When true, a human approval gate is inserted before the validated-fix terminal */
|
|
32
|
+
approvalRequired: boolean;
|
|
33
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import type { LocalPatchValidationBundle, LocalPrBundle } from "../reference/types.js";
|
|
2
|
+
import type {
|
|
3
|
+
PatchValidationObservedOutcome,
|
|
4
|
+
PrReviewMergeObservedOutcome,
|
|
5
|
+
ReplanAbortReason,
|
|
6
|
+
RiskLevel,
|
|
7
|
+
} from "./types.js";
|
|
8
|
+
|
|
9
|
+
const HIGH_RISK_KEYWORDS = ["prod", "production", "hotfix", "security", "critical"];
|
|
10
|
+
const MEDIUM_RISK_KEYWORDS = ["regression", "verification", "verify", "customer"];
|
|
11
|
+
|
|
12
|
+
export interface RiskAssessment {
|
|
13
|
+
riskLevel: RiskLevel;
|
|
14
|
+
reasons: string[];
|
|
15
|
+
warnings: string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function classifyPatchValidationRisk(
|
|
19
|
+
request: LocalPatchValidationBundle,
|
|
20
|
+
outcome: PatchValidationObservedOutcome,
|
|
21
|
+
): RiskAssessment {
|
|
22
|
+
const notes = normalizeNotes(outcome.notes);
|
|
23
|
+
const highKeywords = collectMatchedKeywords(notes, HIGH_RISK_KEYWORDS);
|
|
24
|
+
const mediumKeywords = collectMatchedKeywords(notes, MEDIUM_RISK_KEYWORDS);
|
|
25
|
+
const reasons: string[] = [];
|
|
26
|
+
const warnings: string[] = [];
|
|
27
|
+
let riskLevel: RiskLevel = "low";
|
|
28
|
+
|
|
29
|
+
if (request.candidateSource.kind === "patchFile") {
|
|
30
|
+
riskLevel = "high";
|
|
31
|
+
reasons.push("Patch-file candidates are treated as high risk for adaptive replanning.");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (highKeywords.length > 0) {
|
|
35
|
+
riskLevel = "high";
|
|
36
|
+
reasons.push(`Operator notes mention high-risk terms: ${highKeywords.join(", ")}.`);
|
|
37
|
+
} else if (mediumKeywords.length > 0 && riskLevel === "low") {
|
|
38
|
+
riskLevel = "medium";
|
|
39
|
+
reasons.push(`Operator notes mention elevated-risk terms: ${mediumKeywords.join(", ")}.`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (outcome.terminalNodeId === "rejected") {
|
|
43
|
+
riskLevel = "high";
|
|
44
|
+
reasons.push("The previous attempt was explicitly rejected by a human reviewer.");
|
|
45
|
+
} else if (
|
|
46
|
+
outcome.terminalNodeId === "not-reproduced"
|
|
47
|
+
|| outcome.terminalNodeId === "apply-failed"
|
|
48
|
+
|| outcome.terminalNodeId === "candidate-failed"
|
|
49
|
+
) {
|
|
50
|
+
riskLevel = escalateRisk(riskLevel, "medium");
|
|
51
|
+
reasons.push(`The previous attempt ended at \`${outcome.terminalNodeId}\`.`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (outcome.aborted) {
|
|
55
|
+
riskLevel = escalateRisk(riskLevel, outcome.abortReason === "manual-stop" ? "high" : "medium");
|
|
56
|
+
reasons.push(`The previous attempt aborted due to ${describeAbortReason(outcome.abortReason)}.`);
|
|
57
|
+
if (outcome.abortReason === "unknown") {
|
|
58
|
+
warnings.push("Abort reason is unknown; operator notes may be needed before retrying safely.");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (reasons.length === 0) {
|
|
63
|
+
reasons.push("No elevated risk signals were detected from the previous patch-validation attempt.");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { riskLevel, reasons, warnings };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function classifyPrReviewMergeRisk(
|
|
70
|
+
request: LocalPrBundle,
|
|
71
|
+
outcome: PrReviewMergeObservedOutcome,
|
|
72
|
+
): RiskAssessment {
|
|
73
|
+
const notes = normalizeNotes(outcome.notes);
|
|
74
|
+
const highKeywords = collectMatchedKeywords(notes, HIGH_RISK_KEYWORDS);
|
|
75
|
+
const mediumKeywords = collectMatchedKeywords(notes, MEDIUM_RISK_KEYWORDS);
|
|
76
|
+
const reasons: string[] = [];
|
|
77
|
+
const warnings: string[] = [];
|
|
78
|
+
let riskLevel: RiskLevel = "low";
|
|
79
|
+
|
|
80
|
+
if (highKeywords.length > 0) {
|
|
81
|
+
riskLevel = "high";
|
|
82
|
+
reasons.push(`Operator notes mention high-risk terms: ${highKeywords.join(", ")}.`);
|
|
83
|
+
} else if (mediumKeywords.length > 0) {
|
|
84
|
+
riskLevel = "medium";
|
|
85
|
+
reasons.push(`Operator notes mention elevated-risk terms: ${mediumKeywords.join(", ")}.`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (outcome.terminalNodeId === "reject-human") {
|
|
89
|
+
riskLevel = "high";
|
|
90
|
+
reasons.push("The previous attempt was explicitly rejected by a human reviewer.");
|
|
91
|
+
} else if (
|
|
92
|
+
outcome.terminalNodeId === "reject-verification"
|
|
93
|
+
|| outcome.terminalNodeId === "merge-conflict"
|
|
94
|
+
) {
|
|
95
|
+
riskLevel = escalateRisk(riskLevel, "medium");
|
|
96
|
+
reasons.push(`The previous attempt ended at \`${outcome.terminalNodeId}\`.`);
|
|
97
|
+
} else if (outcome.terminalNodeId === "complete-success") {
|
|
98
|
+
reasons.push("The previous PR review + merge attempt completed successfully.");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (outcome.aborted) {
|
|
102
|
+
riskLevel = escalateRisk(riskLevel, outcome.abortReason === "manual-stop" ? "high" : "medium");
|
|
103
|
+
reasons.push(`The previous attempt aborted due to ${describeAbortReason(outcome.abortReason)}.`);
|
|
104
|
+
if (outcome.abortReason === "retry-exhaustion") {
|
|
105
|
+
warnings.push("Retry exhaustion usually needs human diagnosis before another merge attempt.");
|
|
106
|
+
} else if (outcome.abortReason === "unknown") {
|
|
107
|
+
warnings.push("Abort reason is unknown; operator notes may be needed before retrying safely.");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (reasons.length === 0) {
|
|
112
|
+
reasons.push(
|
|
113
|
+
`No elevated risk signals were detected for merging \`${request.sourceBranch}\` into \`${request.targetBranch}\`.`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return { riskLevel, reasons, warnings };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function normalizeNotes(notes?: string[]): string[] {
|
|
121
|
+
return (notes ?? []).map(note => note.trim()).filter(note => note.length > 0);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function notesContainAny(notes: string[] | undefined, keywords: string[]): boolean {
|
|
125
|
+
const normalized = normalizeNotes(notes).map(note => note.toLowerCase());
|
|
126
|
+
return keywords.some(keyword => normalized.some(note => note.includes(keyword)));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function describeAbortReason(reason: ReplanAbortReason | undefined): string {
|
|
130
|
+
switch (reason) {
|
|
131
|
+
case "setup-failure":
|
|
132
|
+
return "a setup failure";
|
|
133
|
+
case "retry-exhaustion":
|
|
134
|
+
return "retry exhaustion";
|
|
135
|
+
case "timeout":
|
|
136
|
+
return "a timeout";
|
|
137
|
+
case "manual-stop":
|
|
138
|
+
return "a manual stop";
|
|
139
|
+
case "unknown":
|
|
140
|
+
default:
|
|
141
|
+
return "an unknown abort";
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function collectMatchedKeywords(notes: string[], keywords: string[]): string[] {
|
|
146
|
+
const loweredNotes = notes.map(note => note.toLowerCase());
|
|
147
|
+
const matches = new Set<string>();
|
|
148
|
+
|
|
149
|
+
for (const keyword of keywords) {
|
|
150
|
+
if (loweredNotes.some(note => note.includes(keyword))) {
|
|
151
|
+
matches.add(keyword);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return Array.from(matches);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function escalateRisk(current: RiskLevel, next: RiskLevel): RiskLevel {
|
|
159
|
+
const order: RiskLevel[] = ["low", "medium", "high"];
|
|
160
|
+
return order[Math.max(order.indexOf(current), order.indexOf(next))];
|
|
161
|
+
}
|