@gitkraken/conflict-tools 0.0.1-beta.1
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/CHANGELOG.md +8 -0
- package/LICENSE +19 -0
- package/README.md +271 -0
- package/dist/errors/ai.d.ts +10 -0
- package/dist/errors/ai.js +1 -0
- package/dist/errors/conflict.d.ts +10 -0
- package/dist/errors/conflict.js +1 -0
- package/dist/extraction/extract.d.ts +10 -0
- package/dist/extraction/extract.js +1 -0
- package/dist/extraction/parser.d.ts +6 -0
- package/dist/extraction/parser.js +5 -0
- package/dist/git/port-dispatch.d.ts +15 -0
- package/dist/git/port-dispatch.js +2 -0
- package/dist/git/truncation.d.ts +16 -0
- package/dist/git/truncation.js +8 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +1 -0
- package/dist/ports/git.d.ts +75 -0
- package/dist/ports/git.js +1 -0
- package/dist/ports/model.d.ts +39 -0
- package/dist/ports/model.js +1 -0
- package/dist/resolution/chunk-apply.d.ts +7 -0
- package/dist/resolution/chunk-apply.js +9 -0
- package/dist/resolution/create-resolution.d.ts +12 -0
- package/dist/resolution/create-resolution.js +1 -0
- package/dist/resolution/take-strategy.d.ts +8 -0
- package/dist/resolution/take-strategy.js +1 -0
- package/dist/resolver/apply-resolutions.d.ts +9 -0
- package/dist/resolver/apply-resolutions.js +1 -0
- package/dist/resolver/deps.d.ts +14 -0
- package/dist/resolver/deps.js +1 -0
- package/dist/resolver/loop.d.ts +14 -0
- package/dist/resolver/loop.js +1 -0
- package/dist/resolver/parse-response.d.ts +11 -0
- package/dist/resolver/parse-response.js +1 -0
- package/dist/resolver/prompt.d.ts +11 -0
- package/dist/resolver/prompt.js +37 -0
- package/dist/resolver/resolve-conflict.d.ts +8 -0
- package/dist/resolver/resolve-conflict.js +1 -0
- package/dist/resolver/resolve-conflicts.d.ts +9 -0
- package/dist/resolver/resolve-conflicts.js +1 -0
- package/dist/resolver/three-way-diff.d.ts +10 -0
- package/dist/resolver/three-way-diff.js +1 -0
- package/dist/resolver/tools.d.ts +11 -0
- package/dist/resolver/tools.js +1 -0
- package/dist/resolver/validate.d.ts +11 -0
- package/dist/resolver/validate.js +2 -0
- package/dist/types/conflict.d.ts +22 -0
- package/dist/types/conflict.js +1 -0
- package/dist/types/events.d.ts +43 -0
- package/dist/types/events.js +1 -0
- package/dist/types/resolution.d.ts +48 -0
- package/dist/types/resolution.js +1 -0
- package/dist/types/step.d.ts +17 -0
- package/dist/types/step.js +1 -0
- package/dist/types/tool.d.ts +18 -0
- package/dist/types/tool.js +1 -0
- package/package.json +38 -0
package/CHANGELOG.md
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Core Tools License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026 Axosoft, LLC dba GitKraken ("GitKraken")
|
|
4
|
+
|
|
5
|
+
This software and associated documentation files (the "Software") may be compiled as part of GitKraken software products, including the gitkraken/vscode-gitlens open source project (collectively, the "GitKraken Products"), to the extent the Software is a required component of the GitKraken Products; provided, however, that the Software and its functionality may only be used if you (and any entity that you represent) have agreed to, and are in compliance with, the GitKraken End User License Agreement, available at https://gitkraken.com/eula (the "EULA"), or other agreement governing the use of the Software, as agreed by you and GitKraken, and otherwise have a valid subscription for the correct number of user seats for the applicable version of the Software (e.g., GitLens Free+, GitLens Pro, GitLens Teams, and GitLens Enterprise).
|
|
6
|
+
|
|
7
|
+
The Software is licensed, not sold. This license only gives you some rights to use the Software. GitKraken reserves all other rights. Unless applicable law gives you more rights despite this limitation, you may use the Software only as expressly permitted in this license. In doing so, you must comply with any technical limitations in the Software that only allow you to use it in certain ways. You may not (i) work around any technical limitations in the Software, (ii) reverse engineer, decompile or disassemble the Software, or otherwise attempt to derive the source code for the Software, except and to the extent required by third party licensing terms governing use of certain open source components that may be included with the Software, (iii) remove, minimize, block or modify any notices of GitKraken or its suppliers in the Software, (iv) use the software in any way that is against the law, (v) host, share, publish, rent or lease the Software; (vi) distribute the Software as a stand-alone or integrated offering or combine it with any of your applications for others to use, (vii) use the Software other than in connection with validly licensed GitKraken Products or (viii) use any portion of the Software to create software with the same or similar functionality.
|
|
8
|
+
|
|
9
|
+
You agree that GitKraken and/or its licensors (as applicable) retain all right, title and interest in and to the Software and all modifications and/or patches thereto. You are not granted any other rights beyond what is expressly stated herein. Except as set forth above, it is forbidden to copy, merge, publish, distribute, sublicense, modify and/or sell the Software.
|
|
10
|
+
|
|
11
|
+
If you give feedback about the Software to GitKraken, you give to GitKraken, without charge, the right to use, share and commercialize your feedback in any way and for any purpose. You will not give feedback that is subject to a license that requires GitKraken to license its software or documentation to third parties because we include your feedback in them. These rights survive this License.
|
|
12
|
+
|
|
13
|
+
The full text of this Core Tools License shall be included in all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
This License and the EULA, if applicable, are the entire agreement for the Software. You must comply with all domestic and international export laws and regulations that apply to the Software, which include restrictions on destinations, end users and end use. This License is governed and construed by the laws of the State of Arizona, United States without regard to any choice or conflict of law principles or rules.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL GITKRAKEN, THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
18
|
+
|
|
19
|
+
For all third party components incorporated into the Software, those components are licensed under the original license provided by the owner of the applicable component.
|
package/README.md
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# @gitkraken/conflict-tools
|
|
2
|
+
|
|
3
|
+
AI-powered git conflict resolution. Parses conflict markers, resolves conflicts with an agentic AI loop that has access to git context (blame, grep, diff, log, file content), and writes results back to the working tree.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
Three entry points, each serving a different consumer need:
|
|
8
|
+
|
|
9
|
+
- `extractConflict()` — parses conflict markers from a single file and returns a structured representation. No AI, no resolution. Used by editors, eval harnesses, and any consumer that needs the raw conflict data.
|
|
10
|
+
|
|
11
|
+
- `resolveConflict()` — takes one `Conflict` and an optional `ResolutionContext`, runs the AI tool-use loop, returns a `Resolution` with resolved content, confidence, and metrics. The single-file workhorse.
|
|
12
|
+
|
|
13
|
+
- `resolveConflicts() + applyResolutions()` — batch-resolve every currently unmerged file in the working tree, then write and stage results. The library does NOT orchestrate `git rebase`/`merge`/`cherry-pick`. The consumer drives the operation lifecycle and calls these in its own loop.
|
|
14
|
+
|
|
15
|
+
The library never calls `git` or an AI provider directly. Consumers supply adapters via ports.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pnpm add @gitkraken/conflict-tools @gitkraken/shared-tools
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Ports
|
|
24
|
+
|
|
25
|
+
### `ConflictGitPort`
|
|
26
|
+
|
|
27
|
+
A bag of optional high-level git ops, plus an optional raw `exec` fallback. Provide `exec` only (the library's dispatchers will build the git commands themselves), provide individual ops (your adapter's native API is used directly), or mix both.
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import type { ConflictGitPort } from '@gitkraken/conflict-tools';
|
|
31
|
+
|
|
32
|
+
const git: ConflictGitPort = {
|
|
33
|
+
exec: async (args, options) => simpleGit.raw(...args),
|
|
34
|
+
};
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
High-level ops the library dispatches through:
|
|
38
|
+
|
|
39
|
+
| Op | Purpose |
|
|
40
|
+
|---|---|
|
|
41
|
+
| `readFile(path)` | Read working-tree file content (used during conflict extraction) |
|
|
42
|
+
| `showFile(ref, path, opts?)` | Read file content at a specific ref. Supports `startLine`/`endLine` |
|
|
43
|
+
| `unmergedEntries()` | List files git considers unresolved |
|
|
44
|
+
| `mergeBase(a, b)` | Common ancestor between two refs |
|
|
45
|
+
| `blame(path, opts?)` | Line-level authorship. Supports `startLine`/`endLine` (maps to `git blame -L`) |
|
|
46
|
+
| `grep(pattern, opts?)` | Content search across the tree. Supports `maxResults` |
|
|
47
|
+
| `diff(from, to, opts?)` | Unified diff between refs, optionally scoped to a path |
|
|
48
|
+
| `log(opts?)` | Commit history. Supports `ref`, `path`, `maxCount` |
|
|
49
|
+
| `writeFile(path, content)` | Write resolved content to the working tree |
|
|
50
|
+
| `deleteFile(path)` | Remove a file from the working tree |
|
|
51
|
+
| `stageFiles(paths)` | Stage resolved files (`git add`) |
|
|
52
|
+
| `checkoutFile(path, 'ours' \| 'theirs')` | Apply a side directly via git |
|
|
53
|
+
|
|
54
|
+
Outputs from `showFile` and `blame` are capped at 1000 lines; `grep` and `log` at 100 results. When the cap fires, the library prepends an actionable header (e.g. `[Output capped at 1000 lines. Use startLine/endLine to read other regions of the file.]`) so the AI can re-query with a narrower range.
|
|
55
|
+
|
|
56
|
+
If a flow needs an op that the consumer didn't supply and `exec` is unavailable, the library throws `ConflictGitPortMissingOpError`.
|
|
57
|
+
|
|
58
|
+
### `ConflictModelPort`
|
|
59
|
+
|
|
60
|
+
A single-method port for AI inference. The library owns the tool-use loop — it calls `generate`, dispatches any `toolCalls` it sees against `ConflictGitPort`, feeds the results back, and repeats until the model returns a final structured answer.
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
import type { ConflictModelPort } from '@gitkraken/conflict-tools';
|
|
64
|
+
|
|
65
|
+
const model: ConflictModelPort = {
|
|
66
|
+
generate: async ({ system, messages, tools, temperature, signal }) => {
|
|
67
|
+
// Map to your AI SDK (Vercel AI SDK, Anthropic, OpenAI, VS Code LLM API, ...)
|
|
68
|
+
// Return { text?, toolCalls?, usage? }
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The shape of `tools`, `toolCalls`, and `toolResults` follows abstract types (`ToolDefinition`, `ToolCall`, `ToolResult`) so consumers can adapt any SDK. The library does not require streaming or any provider-specific feature.
|
|
74
|
+
|
|
75
|
+
## Entry points
|
|
76
|
+
|
|
77
|
+
### `extractConflict(filePath, deps)` — parse only
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
import { extractConflict } from '@gitkraken/conflict-tools';
|
|
81
|
+
|
|
82
|
+
const conflict = await extractConflict('src/auth.ts', { git });
|
|
83
|
+
|
|
84
|
+
// conflict.filePath — 'src/auth.ts'
|
|
85
|
+
// conflict.markers[] — position, sides, and surrounding context per marker block
|
|
86
|
+
// conflict.type — 'text' | 'binary'
|
|
87
|
+
// conflict.rawContent — original file content with markers preserved
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Returns `null` for files without conflict markers (including delete-modify situations — the consumer decides what to do with those, see `UnmergedEntry.reason`).
|
|
91
|
+
|
|
92
|
+
### `resolveConflict(conflict, context, deps)` — single file
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
import { resolveConflict } from '@gitkraken/conflict-tools';
|
|
96
|
+
|
|
97
|
+
const resolution = await resolveConflict(conflict, context, {
|
|
98
|
+
model,
|
|
99
|
+
git,
|
|
100
|
+
config: { maxSteps: 15, temperature: 0 },
|
|
101
|
+
onProgress: (event) => console.log(event),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// resolution.content — resolved file content (markers replaced)
|
|
105
|
+
// resolution.strategy — 'ai' | 'take-ours' | 'take-theirs' | 'skipped'
|
|
106
|
+
// resolution.confidence — 0..1, from the AI's own self-assessment
|
|
107
|
+
// resolution.description — one-or-two sentence rationale, from the AI
|
|
108
|
+
// resolution.chunks? — per-marker decisions (for observability and validation)
|
|
109
|
+
// resolution.metrics? — { inputTokens, outputTokens, stepCount, toolCallCount, ... }
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`ResolutionContext` carries optional hints the AI can use:
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
interface ResolutionContext {
|
|
116
|
+
commitMessage?: string;
|
|
117
|
+
prDescription?: string;
|
|
118
|
+
previousResolutions?: Resolution[];
|
|
119
|
+
fileNeighbors?: string[];
|
|
120
|
+
refs?: { ours: string; theirs: string; base?: string };
|
|
121
|
+
threeWayDiff?: { oursDiff: string; theirsDiff: string };
|
|
122
|
+
metadata?: Record<string, unknown>;
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
When `refs` is provided, the library computes `git diff base..ours -- file` and `git diff base..theirs -- file` automatically and includes both in the prompt. This typically lets the AI resolve without any tool calls. Consumers that already have these diffs cached can pass `threeWayDiff` directly to skip the computation.
|
|
127
|
+
|
|
128
|
+
### `resolveConflicts(deps, context?) + applyResolutions(resolutions, deps)` — batch
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
import { resolveConflicts, applyResolutions } from '@gitkraken/conflict-tools';
|
|
132
|
+
|
|
133
|
+
const step = await resolveConflicts(
|
|
134
|
+
{
|
|
135
|
+
model,
|
|
136
|
+
git,
|
|
137
|
+
config: {
|
|
138
|
+
excludeFiles: ['*.lock', 'pnpm-lock.yaml', '*.generated.ts'],
|
|
139
|
+
fallbackStrategy: 'take-theirs',
|
|
140
|
+
maxSteps: 15,
|
|
141
|
+
},
|
|
142
|
+
onProgress: (event) => updateUI(event),
|
|
143
|
+
},
|
|
144
|
+
{ commitMessage, refs },
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
// step.resolutions[] — successful resolutions
|
|
148
|
+
// step.errors[] — { filePath, error } for files that failed (no fallback or AI exhausted)
|
|
149
|
+
|
|
150
|
+
await applyResolutions(step.resolutions, { git });
|
|
151
|
+
// All non-skipped resolutions are written via writeFile and staged via stageFiles.
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
`StepConfig` extends `ResolverConfig` with two batch-only fields:
|
|
155
|
+
- `excludeFiles` — glob patterns (matched on basename via `picomatch`); matched files are skipped before AI is invoked.
|
|
156
|
+
- `fallbackStrategy` — when AI fails for a file, fall back to `'take-ours'` or `'take-theirs'` instead of recording an error.
|
|
157
|
+
|
|
158
|
+
The function inspects the working tree once (via `unmergedEntries`), processes each conflict sequentially, and returns. It does not call `git rebase --continue` or any other operation command — that is the consumer's job.
|
|
159
|
+
|
|
160
|
+
## Consumer examples
|
|
161
|
+
|
|
162
|
+
### GitLens — single-file editor command
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
const git = createVSCodeConflictGit(vscodeGitApi);
|
|
166
|
+
const model = createVSCodeAiModel(vscodeLlmApi);
|
|
167
|
+
|
|
168
|
+
const conflict = await extractConflict(activeFilePath, { git });
|
|
169
|
+
if (conflict) {
|
|
170
|
+
const resolution = await resolveConflict(conflict, { commitMessage }, { model, git });
|
|
171
|
+
showInDiffEditor(resolution);
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### GitLens / GKD — batch "resolve all" (user-driven)
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
const step = await resolveConflicts({ git, model, onProgress: webview.post });
|
|
179
|
+
const approved = await reviewDialog(step.resolutions);
|
|
180
|
+
await applyResolutions(approved, { git });
|
|
181
|
+
// User clicks "continue rebase" themselves.
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### CLI / GitHub Action — automated rebase loop
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
// The rebase loop lives in the consumer (e.g. @merge-mate/core), not in conflict-tools.
|
|
188
|
+
for (const commit of commits) {
|
|
189
|
+
const result = await git.exec(['rebase', '--onto', target, `${commit}^`, commit]);
|
|
190
|
+
if (result.exitCode === 0) continue;
|
|
191
|
+
|
|
192
|
+
const step = await resolveConflicts({
|
|
193
|
+
git, model,
|
|
194
|
+
config: { excludeFiles: ['*.lock'], fallbackStrategy: 'take-ours' },
|
|
195
|
+
onProgress: emit,
|
|
196
|
+
});
|
|
197
|
+
await applyResolutions(step.resolutions, { git });
|
|
198
|
+
await git.exec(['rebase', '--continue']);
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Eval harness
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
const conflict = await extractConflict(fixturePath, { git });
|
|
206
|
+
const resolution = await resolveConflict(conflict!, context, { model, git });
|
|
207
|
+
score(resolution, goldenFile);
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Observability
|
|
211
|
+
|
|
212
|
+
`resolveConflicts` and `resolveConflict` accept an `onProgress` callback that receives a discriminated `ConflictProgressEvent`:
|
|
213
|
+
|
|
214
|
+
| Event | When |
|
|
215
|
+
|---|---|
|
|
216
|
+
| `conflict:found` | A conflict file has been parsed |
|
|
217
|
+
| `resolution:applied` | A resolution was produced for a file |
|
|
218
|
+
| `resolution:fallback` | AI failed; fallback strategy applied |
|
|
219
|
+
| `resolution:failed` | AI failed and no fallback configured |
|
|
220
|
+
| `resolver:tool-call` | The AI invoked a tool (show, grep, blame, diff, log) |
|
|
221
|
+
| `resolver:step-usage` | Token usage for one model call |
|
|
222
|
+
| `resolver:completed` | The resolver finished a single file |
|
|
223
|
+
|
|
224
|
+
Operation-level events (rebase started, step completed, etc.) are the consumer's responsibility — the library has no opinion about the surrounding workflow.
|
|
225
|
+
|
|
226
|
+
## Error handling
|
|
227
|
+
|
|
228
|
+
| Error | Codes | When |
|
|
229
|
+
|---|---|---|
|
|
230
|
+
| `ConflictError` | `PARSE_FAILED`, `BINARY_CONFLICT` | Conflict extraction failed |
|
|
231
|
+
| `AIError` | `VALIDATION_EXHAUSTED`, `INCOMPLETE_RESOLUTION` | Model could not produce a valid resolution after retries |
|
|
232
|
+
| `ConflictGitPortMissingOpError` | — | A required git op is missing and no `exec` fallback was provided |
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
import { resolveConflict, AIError, ConflictError } from '@gitkraken/conflict-tools';
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
await resolveConflict(conflict, context, { model, git });
|
|
239
|
+
} catch (error) {
|
|
240
|
+
if (error instanceof ConflictError && error.code === 'BINARY_CONFLICT') {
|
|
241
|
+
// Binary file — apply your own fallback (take-ours, take-theirs, skip).
|
|
242
|
+
} else if (error instanceof AIError) {
|
|
243
|
+
// Model exhausted retries. Configure StepConfig.fallbackStrategy or handle here.
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
In the batch entry point, errors that have a configured fallback do not throw — they are downgraded to `resolution:fallback` events, and the resulting resolutions appear in `step.resolutions`. Errors without a fallback land in `step.errors` and the loop continues with the next file.
|
|
249
|
+
|
|
250
|
+
## Semver policy
|
|
251
|
+
|
|
252
|
+
| Change | Bump |
|
|
253
|
+
|---|---|
|
|
254
|
+
| New optional field on an exported type | patch |
|
|
255
|
+
| New variant in `ConflictProgressEvent` | minor |
|
|
256
|
+
| New optional method on `ConflictGitOps` | patch |
|
|
257
|
+
| New required method on a port | major |
|
|
258
|
+
| Remove or rename an exported field | major |
|
|
259
|
+
| Change the signature of a port method | major |
|
|
260
|
+
| Bundled prompt change (no API change) | patch |
|
|
261
|
+
| New required field on a config | major |
|
|
262
|
+
|
|
263
|
+
## Contributing
|
|
264
|
+
|
|
265
|
+
This package follows the monorepo's standard tooling — see the root [CONTRIBUTING.md](../../CONTRIBUTING.md). Local commands:
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
pnpm --filter @gitkraken/conflict-tools build
|
|
269
|
+
pnpm --filter @gitkraken/conflict-tools typecheck
|
|
270
|
+
pnpm --filter @gitkraken/conflict-tools test
|
|
271
|
+
```
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//#region src/errors/ai.d.ts
|
|
2
|
+
type AIErrorCode = 'VALIDATION_EXHAUSTED' | 'INCOMPLETE_RESOLUTION';
|
|
3
|
+
declare class AIError extends Error {
|
|
4
|
+
readonly code: AIErrorCode;
|
|
5
|
+
constructor(code: AIErrorCode, message: string, options?: {
|
|
6
|
+
cause?: unknown;
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
//#endregion
|
|
10
|
+
export { AIError, AIErrorCode };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var e=class extends Error{code;constructor(e,t,n){super(t,n),this.name=`AIError`,this.code=e}};export{e as AIError};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//#region src/errors/conflict.d.ts
|
|
2
|
+
type ConflictErrorCode = 'PARSE_FAILED';
|
|
3
|
+
declare class ConflictError extends Error {
|
|
4
|
+
readonly code: ConflictErrorCode;
|
|
5
|
+
constructor(code: ConflictErrorCode, message: string, options?: {
|
|
6
|
+
cause?: unknown;
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
//#endregion
|
|
10
|
+
export { ConflictError, ConflictErrorCode };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var e=class extends Error{code;constructor(e,t,n){super(t,n),this.name=`ConflictError`,this.code=e}};export{e as ConflictError};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ConflictGitPort } from "../ports/git.js";
|
|
2
|
+
import { Conflict } from "../types/conflict.js";
|
|
3
|
+
|
|
4
|
+
//#region src/extraction/extract.d.ts
|
|
5
|
+
declare function extractConflict(filePath: string, deps: {
|
|
6
|
+
git: ConflictGitPort;
|
|
7
|
+
signal?: AbortSignal;
|
|
8
|
+
}): Promise<Conflict | null>;
|
|
9
|
+
//#endregion
|
|
10
|
+
export { extractConflict };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{readFile as e}from"../git/port-dispatch.js";import{parseConflictMarkers as t}from"./parser.js";async function n(n,r){let i=await e(r.git,n,{signal:r.signal}),a=t(i);return a.length===0?null:{filePath:n,markers:a,type:`text`,rawContent:i}}export{n as extractConflict};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import{ConflictError as e}from"../errors/conflict.js";const t=`<<<<<<<`,n=`|||||||`,r=`=======`,i=`>>>>>>>`;function a(a,l=3){let u=a.split(`
|
|
2
|
+
`),d=[],f=0;for(;f<u.length;){let a=u[f];if(a===void 0||!o(a,t)){f++;continue}let p=f,m=s(a,t);f++;let h=[],g=null,_=null;for(;f<u.length;){let i=u[f];if(i===void 0)break;if(o(i,t))throw new e(`PARSE_FAILED`,`Nested conflict marker at line ${f+1}`);if(o(i,n)){_=s(i,n),f++,g=[];break}if(o(i,r)){f++;break}h.push(i),f++}if(f>=u.length)throw new e(`PARSE_FAILED`,`Unterminated conflict marker starting at line ${p+1}`);if(g!==null){for(;f<u.length;){let n=u[f];if(n===void 0)break;if(o(n,t))throw new e(`PARSE_FAILED`,`Nested conflict marker at line ${f+1}`);if(o(n,r)){f++;break}g.push(n),f++}if(f>=u.length)throw new e(`PARSE_FAILED`,`Unterminated conflict marker starting at line ${p+1}`)}let v=[];for(;f<u.length;){let n=u[f];if(n===void 0)break;if(o(n,t))throw new e(`PARSE_FAILED`,`Nested conflict marker at line ${f+1}`);if(o(n,i))break;v.push(n),f++}if(f>=u.length)throw new e(`PARSE_FAILED`,`Unterminated conflict marker starting at line ${p+1}`);let y=f,b=u[f],x=b===void 0?``:s(b,i);f++;let S=d[d.length-1],C=S?S.endLine+1:0,w=Math.max(C,p-l),T=u.slice(w,p).join(`
|
|
3
|
+
`);d.push({startLine:p,endLine:y,ours:c(h),base:g===null?null:c(g),theirs:c(v),oursLabel:m,theirsLabel:x,baseLabel:_,contextBefore:T,contextAfter:``})}for(let e=0;e<d.length;e++){let t=d[e];if(!t)continue;let n=d[e+1],r=n?n.startLine:u.length,i=Math.min(r,t.endLine+1+l);d[e]={...t,contextAfter:u.slice(t.endLine+1,i).join(`
|
|
4
|
+
`)}}return d}function o(e,t){return e.startsWith(t)&&(e.length===t.length||e[t.length]===` `)}function s(e,t){return e.slice(t.length).trim()}function c(e){return e.join(`
|
|
5
|
+
`)}export{a as parseConflictMarkers};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { BlameOptions, ConflictDiffOptions, ConflictGitPort, GrepOptions, LogOptions, OpOptions, ShowFileOptions, UnmergedEntry } from "../ports/git.js";
|
|
2
|
+
|
|
3
|
+
//#region src/git/port-dispatch.d.ts
|
|
4
|
+
declare function readFile(git: ConflictGitPort, path: string, opts?: OpOptions): Promise<string>;
|
|
5
|
+
declare function showFile(git: ConflictGitPort, ref: string, path: string, opts?: ShowFileOptions): Promise<string>;
|
|
6
|
+
declare function unmergedEntries(git: ConflictGitPort, opts?: OpOptions): Promise<UnmergedEntry[]>;
|
|
7
|
+
declare function mergeBase(git: ConflictGitPort, a: string, b: string, opts?: OpOptions): Promise<string>;
|
|
8
|
+
declare function blame(git: ConflictGitPort, path: string, opts?: BlameOptions): Promise<string>;
|
|
9
|
+
declare function grep(git: ConflictGitPort, pattern: string, opts?: GrepOptions): Promise<string>;
|
|
10
|
+
declare function diff(git: ConflictGitPort, from: string, to: string, opts?: ConflictDiffOptions): Promise<string>;
|
|
11
|
+
declare function log(git: ConflictGitPort, opts?: LogOptions): Promise<string>;
|
|
12
|
+
declare function writeFile(git: ConflictGitPort, path: string, content: string, opts?: OpOptions): Promise<void>;
|
|
13
|
+
declare function stageFiles(git: ConflictGitPort, paths: string[], opts?: OpOptions): Promise<void>;
|
|
14
|
+
//#endregion
|
|
15
|
+
export { blame, diff, grep, log, mergeBase, readFile, showFile, stageFiles, unmergedEntries, writeFile };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{ConflictGitPortMissingOpError as e}from"../ports/git.js";import{sliceLines as t,truncateLines as n,truncateMatches as r}from"./truncation.js";function i(e){return`Search capped at ${e} matches. There may be more — narrow with a more specific pattern or path.`}function a(t,n){throw new e(t,n)}function o(e,t){return e!=null&&t!=null?`startLine=${e}, endLine=${t}`:e==null?`endLine=${t}`:`startLine=${e}`}async function s(e,t,n){return e.readFile?e.readFile(t,n):a(`readFile`)}async function c(e,r,i,s){let c;if(e.showFile)c=await e.showFile(r,i,s);else if(e.exec){let n=t(await e.exec([`show`,`${r}:${i}`],{signal:s?.signal}),s?.startLine,s?.endLine);if(!n.ok)return n.reason===`invalid-range`?`[Invalid range: ${o(n.startLine,n.endLine)} (startLine must be <= endLine).]`:`[Range out of bounds: file has ${n.total} lines, requested ${o(n.startLine,n.endLine)}.]`;c=n.content}else return a(`showFile`);return n(c,1e3,`Output capped at 1000 lines. Use startLine/endLine to read other regions of the file.`)}async function l(e,t){if(e.unmergedEntries)return e.unmergedEntries(t);if(e.exec){let n=await e.exec([`status`,`--porcelain=v2`],{signal:t?.signal}),r=[];for(let e of n.split(`
|
|
2
|
+
`)){if(!e.startsWith(`u `))continue;let t=e.split(` `),n=t[1]??``,i=t.slice(10).join(` `);r.push({path:i,reason:_(n)})}return r}return a(`unmergedEntries`)}async function u(e,t,n,r){return e.mergeBase?e.mergeBase(t,n,r):e.exec?(await e.exec([`merge-base`,`--`,t,n],{signal:r?.signal})).trim():a(`mergeBase`)}async function d(e,t,r){let i=r?.startLine,s=r?.endLine,c=Number.isFinite(i),l=Number.isFinite(s);if(c&&l&&i>s)return`[Invalid range: ${o(i,s)} (startLine must be <= endLine).]`;let u;if(e.blame)u=await e.blame(t,r);else if(e.exec){let n=[`blame`,`--porcelain`],a=c?Math.max(1,i):void 0,o=l?Math.max(1,s):void 0;a!=null&&o!=null?n.push(`-L`,`${a},${o}`):a==null?o!=null&&n.push(`-L`,`1,${o}`):n.push(`-L`,`${a},`),r?.ref&&n.push(r.ref),n.push(`--`,t),u=await e.exec(n,{signal:r?.signal})}else return a(`blame`);return n(u,1e3,`Output capped at 1000 lines. Use startLine/endLine to inspect other regions.`)}async function f(e,t,n){let o=n?.maxResults,s=o!=null&&Number.isFinite(o)?Math.min(Math.max(1,o),100):100,c;if(e.grep)c=await e.grep(t,n);else if(e.exec){let r=[`grep`,`-n`,`--`,t];n?.ref&&r.push(n.ref),c=await e.exec(r,{signal:n?.signal})}else return a(`grep`);return r(c,s,i(s))}async function p(e,t,n,r){if(e.diff)return e.diff(t,n,r);if(e.exec){let i=[`diff`,t,n];return r?.path&&i.push(`--`,r.path),e.exec(i,{signal:r?.signal})}return a(`diff`)}async function m(e,t){let r=t?.maxCount,i=r!=null&&Number.isFinite(r)?Math.min(Math.max(1,r),100):100,o;if(e.log)o=await e.log({...t,maxCount:i});else if(e.exec){let n=[`log`,`--oneline`,`-n`,String(i)];t?.ref&&n.push(t.ref),t?.path&&n.push(`--`,t.path),o=await e.exec(n,{signal:t?.signal})}else return a(`log`);return n(o,100,`Output capped at 100 commits. Use path to scope to a file or refine the ref range.`)}async function h(e,t,n,r){return e.writeFile?e.writeFile(t,n,r):a(`writeFile`)}async function g(e,t,n){if(e.stageFiles)return e.stageFiles(t,n);if(e.exec){await e.exec([`add`,`--`,...t],{signal:n?.signal});return}return a(`stageFiles`)}function _(e){switch(e){case`DD`:return`both-deleted`;case`AU`:return`added-by-us`;case`UD`:return`deleted-by-them`;case`UA`:return`added-by-them`;case`DU`:return`deleted-by-us`;case`AA`:return`both-added`;case`UU`:return`both-modified`;default:return e}}export{d as blame,p as diff,f as grep,m as log,u as mergeBase,s as readFile,c as showFile,g as stageFiles,l as unmergedEntries,h as writeFile};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/git/truncation.d.ts
|
|
2
|
+
type SliceResult = {
|
|
3
|
+
ok: true;
|
|
4
|
+
content: string;
|
|
5
|
+
} | {
|
|
6
|
+
ok: false;
|
|
7
|
+
reason: 'out-of-range' | 'invalid-range';
|
|
8
|
+
total: number;
|
|
9
|
+
startLine?: number;
|
|
10
|
+
endLine?: number;
|
|
11
|
+
};
|
|
12
|
+
declare function truncateLines(text: string, cap: number, hint: string): string;
|
|
13
|
+
declare function truncateMatches(text: string, cap: number, hint: string): string;
|
|
14
|
+
declare function sliceLines(text: string, startLine?: number, endLine?: number): SliceResult;
|
|
15
|
+
//#endregion
|
|
16
|
+
export { SliceResult, sliceLines, truncateLines, truncateMatches };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
function e(e,t,n){if(t<=0||e.length===0)return e;let r=e.split(`
|
|
2
|
+
`);return(r.length>0&&r[r.length-1]===``?r.length-1:r.length)<=t?e:`[${n}]\n\n${r.slice(0,t).join(`
|
|
3
|
+
`)}`}function t(e,t,n){if(t<=0||e.length===0)return e;let r=e.split(`
|
|
4
|
+
`).filter(e=>e.length>0);return r.length<=t?r.join(`
|
|
5
|
+
`):`[${n}]\n\n${r.slice(0,t).join(`
|
|
6
|
+
`)}`}function n(e,t,n){let r=Number.isFinite(t),i=Number.isFinite(n);if(!r&&!i)return{ok:!0,content:e};let a=e.split(`
|
|
7
|
+
`),o=a.length>0&&a[a.length-1]===``,s=o?a.slice(0,-1):a,c=s.length;if(r&&i&&t>n)return{ok:!1,reason:`invalid-range`,total:c,startLine:t,endLine:n};let l=r?Math.max(1,t):1;if(l>c)return{ok:!1,reason:`out-of-range`,total:c,startLine:t,endLine:n};let u=i?Math.min(c,Math.max(1,n)):c,d=s.slice(l-1,u).join(`
|
|
8
|
+
`);return{ok:!0,content:o?`${d}\n`:d}}export{n as sliceLines,e as truncateLines,t as truncateMatches};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { AIError, AIErrorCode } from "./errors/ai.js";
|
|
2
|
+
import { ConflictError, ConflictErrorCode } from "./errors/conflict.js";
|
|
3
|
+
import { BlameOptions, ConflictDiffOptions, ConflictGitOps, ConflictGitPort, ConflictGitPortMissingOpError, GrepOptions, LogOptions, OpOptions, ShowFileOptions, UnmergedEntry } from "./ports/git.js";
|
|
4
|
+
import { Conflict, ConflictMarker, ConflictType } from "./types/conflict.js";
|
|
5
|
+
import { extractConflict } from "./extraction/extract.js";
|
|
6
|
+
import { ToolCall, ToolDefinition, ToolResult } from "./types/tool.js";
|
|
7
|
+
import { ConflictModelMessage, ConflictModelParams, ConflictModelPort, ConflictModelResult, ResolverConfig } from "./ports/model.js";
|
|
8
|
+
import { Resolution, ResolutionContext, ResolutionMetrics, ResolutionRefs, ResolutionStrategy, ResolvedChunk, ThreeWayDiff } from "./types/resolution.js";
|
|
9
|
+
import { createResolution } from "./resolution/create-resolution.js";
|
|
10
|
+
import { takeOurs, takeTheirs } from "./resolution/take-strategy.js";
|
|
11
|
+
import { applyResolutions } from "./resolver/apply-resolutions.js";
|
|
12
|
+
import { ConflictProgressEvent } from "./types/events.js";
|
|
13
|
+
import { resolveConflict } from "./resolver/resolve-conflict.js";
|
|
14
|
+
import { StepConfig, StepResult } from "./types/step.js";
|
|
15
|
+
import { resolveConflicts } from "./resolver/resolve-conflicts.js";
|
|
16
|
+
export { AIError, type AIErrorCode, type BlameOptions, type Conflict, type ConflictDiffOptions, ConflictError, type ConflictErrorCode, type ConflictGitOps, type ConflictGitPort, ConflictGitPortMissingOpError, type ConflictMarker, type ConflictModelMessage, type ConflictModelParams, type ConflictModelPort, type ConflictModelResult, type ConflictProgressEvent, type ConflictType, type GrepOptions, type LogOptions, type OpOptions, type Resolution, type ResolutionContext, type ResolutionMetrics, type ResolutionRefs, type ResolutionStrategy, type ResolvedChunk, type ResolverConfig, type ShowFileOptions, type StepConfig, type StepResult, type ThreeWayDiff, type ToolCall, type ToolDefinition, type ToolResult, type UnmergedEntry, applyResolutions, createResolution, extractConflict, resolveConflict, resolveConflicts, takeOurs, takeTheirs };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{AIError as e}from"./errors/ai.js";import{ConflictError as t}from"./errors/conflict.js";import{ConflictGitPortMissingOpError as n}from"./ports/git.js";import{extractConflict as r}from"./extraction/extract.js";import{createResolution as i}from"./resolution/create-resolution.js";import{takeOurs as a,takeTheirs as o}from"./resolution/take-strategy.js";import{applyResolutions as s}from"./resolver/apply-resolutions.js";import{resolveConflict as c}from"./resolver/resolve-conflict.js";import{resolveConflicts as l}from"./resolver/resolve-conflicts.js";export{e as AIError,t as ConflictError,n as ConflictGitPortMissingOpError,s as applyResolutions,i as createResolution,r as extractConflict,c as resolveConflict,l as resolveConflicts,a as takeOurs,o as takeTheirs};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { GitPort } from "@gitkraken/shared-tools";
|
|
2
|
+
|
|
3
|
+
//#region src/ports/git.d.ts
|
|
4
|
+
interface OpOptions {
|
|
5
|
+
signal?: AbortSignal;
|
|
6
|
+
}
|
|
7
|
+
interface ConflictDiffOptions extends OpOptions {
|
|
8
|
+
path?: string;
|
|
9
|
+
}
|
|
10
|
+
interface ShowFileOptions extends OpOptions {
|
|
11
|
+
startLine?: number;
|
|
12
|
+
endLine?: number;
|
|
13
|
+
}
|
|
14
|
+
interface BlameOptions extends OpOptions {
|
|
15
|
+
ref?: string;
|
|
16
|
+
startLine?: number;
|
|
17
|
+
endLine?: number;
|
|
18
|
+
}
|
|
19
|
+
interface GrepOptions extends OpOptions {
|
|
20
|
+
ref?: string;
|
|
21
|
+
maxResults?: number;
|
|
22
|
+
}
|
|
23
|
+
interface LogOptions extends OpOptions {
|
|
24
|
+
ref?: string;
|
|
25
|
+
path?: string;
|
|
26
|
+
maxCount?: number;
|
|
27
|
+
}
|
|
28
|
+
interface UnmergedEntry {
|
|
29
|
+
path: string;
|
|
30
|
+
reason: string;
|
|
31
|
+
}
|
|
32
|
+
interface ConflictGitOps {
|
|
33
|
+
readFile?: (path: string, opts?: OpOptions) => Promise<string>;
|
|
34
|
+
/**
|
|
35
|
+
* Returns the contents of `path` at `ref`. When `opts.startLine`/`endLine` are provided,
|
|
36
|
+
* implementations should return only that line range (1-indexed, inclusive). The library
|
|
37
|
+
* returns an actionable message instead of empty content when the requested range is fully
|
|
38
|
+
* out of bounds — applies to the exec-fallback path only.
|
|
39
|
+
*/
|
|
40
|
+
showFile?: (ref: string, path: string, opts?: ShowFileOptions) => Promise<string>;
|
|
41
|
+
unmergedEntries?: (opts?: OpOptions) => Promise<UnmergedEntry[]>;
|
|
42
|
+
mergeBase?: (a: string, b: string, opts?: OpOptions) => Promise<string>;
|
|
43
|
+
/**
|
|
44
|
+
* Returns line-by-line authorship for `path`. When `opts.startLine`/`endLine` are provided,
|
|
45
|
+
* implementations should restrict to that 1-indexed inclusive range. The library does not
|
|
46
|
+
* validate the range against actual file size — invalid ranges may surface as exceptions
|
|
47
|
+
* from the underlying git command.
|
|
48
|
+
*/
|
|
49
|
+
blame?: (path: string, opts?: BlameOptions) => Promise<string>;
|
|
50
|
+
/**
|
|
51
|
+
* Searches `pattern` across the working tree (or at `opts.ref`). When `opts.maxResults` is
|
|
52
|
+
* provided, implementations should return at most that many matches. Results are
|
|
53
|
+
* post-processed by the library against a hard cap of 100 matches.
|
|
54
|
+
*/
|
|
55
|
+
grep?: (pattern: string, opts?: GrepOptions) => Promise<string>;
|
|
56
|
+
diff?: (from: string, to: string, opts?: ConflictDiffOptions) => Promise<string>;
|
|
57
|
+
/**
|
|
58
|
+
* Returns commit history (typically one line per commit). When `opts.maxCount` is provided,
|
|
59
|
+
* implementations should respect it (the library clamps the requested value to a hard
|
|
60
|
+
* maximum of 100 before calling).
|
|
61
|
+
*/
|
|
62
|
+
log?: (opts?: LogOptions) => Promise<string>;
|
|
63
|
+
writeFile?: (path: string, content: string, opts?: OpOptions) => Promise<void>;
|
|
64
|
+
stageFiles?: (paths: string[], opts?: OpOptions) => Promise<void>;
|
|
65
|
+
}
|
|
66
|
+
type ConflictGitPort = ConflictGitOps & {
|
|
67
|
+
exec?: GitPort['exec'];
|
|
68
|
+
};
|
|
69
|
+
declare class ConflictGitPortMissingOpError extends Error {
|
|
70
|
+
readonly op: string;
|
|
71
|
+
readonly hint: string;
|
|
72
|
+
constructor(op: string, hint?: string);
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
75
|
+
export { BlameOptions, ConflictDiffOptions, ConflictGitOps, ConflictGitPort, ConflictGitPortMissingOpError, GrepOptions, LogOptions, OpOptions, ShowFileOptions, UnmergedEntry };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var e=class extends Error{op;hint;constructor(e,t){super(`ConflictGitPort is missing op '${e}'. `+`Provide either '${e}' directly, or provide 'exec' as a fallback. ${t??``}`.trim()),this.name=`ConflictGitPortMissingOpError`,this.op=e,this.hint=t??``}};export{e as ConflictGitPortMissingOpError};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ToolCall, ToolDefinition } from "../types/tool.js";
|
|
2
|
+
import { AiTokenUsage } from "@gitkraken/shared-tools";
|
|
3
|
+
|
|
4
|
+
//#region src/ports/model.d.ts
|
|
5
|
+
interface ConflictModelPort {
|
|
6
|
+
generate(params: ConflictModelParams): Promise<ConflictModelResult>;
|
|
7
|
+
}
|
|
8
|
+
interface ConflictModelParams {
|
|
9
|
+
system?: string;
|
|
10
|
+
messages: ConflictModelMessage[];
|
|
11
|
+
tools?: ToolDefinition[];
|
|
12
|
+
maxTokens?: number;
|
|
13
|
+
temperature?: number;
|
|
14
|
+
signal?: AbortSignal;
|
|
15
|
+
}
|
|
16
|
+
type ConflictModelMessage = {
|
|
17
|
+
role: 'user';
|
|
18
|
+
content: string;
|
|
19
|
+
} | {
|
|
20
|
+
role: 'assistant';
|
|
21
|
+
content?: string;
|
|
22
|
+
toolCalls?: ToolCall[];
|
|
23
|
+
} | {
|
|
24
|
+
role: 'tool';
|
|
25
|
+
toolCallId: string;
|
|
26
|
+
toolName: string;
|
|
27
|
+
content: string;
|
|
28
|
+
};
|
|
29
|
+
interface ConflictModelResult {
|
|
30
|
+
text?: string;
|
|
31
|
+
toolCalls?: ToolCall[];
|
|
32
|
+
usage?: AiTokenUsage;
|
|
33
|
+
}
|
|
34
|
+
interface ResolverConfig {
|
|
35
|
+
maxSteps?: number;
|
|
36
|
+
temperature?: number;
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
39
|
+
export { ConflictModelMessage, ConflictModelParams, ConflictModelPort, ConflictModelResult, ResolverConfig };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ConflictMarker } from "../types/conflict.js";
|
|
2
|
+
import { ResolvedChunk } from "../types/resolution.js";
|
|
3
|
+
|
|
4
|
+
//#region src/resolution/chunk-apply.d.ts
|
|
5
|
+
declare function applyChunks(rawContent: string, markers: ConflictMarker[], chunks: ResolvedChunk[]): string;
|
|
6
|
+
//#endregion
|
|
7
|
+
export { applyChunks };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
function e(e,n,r){let i=e.split(`
|
|
2
|
+
`),a=new Map;for(let e of r)a.set(e.markerIndex,e);let o=[],s=0;for(let e=0;e<n.length;e++){let r=n[e];if(!r)continue;let c=a.get(e);if(!c)continue;r.startLine>s&&o.push(i.slice(s,r.startLine).join(`
|
|
3
|
+
`));let l=t(c,r);l!==``&&o.push(l),s=r.endLine+1}return s<i.length&&o.push(i.slice(s).join(`
|
|
4
|
+
`)),o.join(`
|
|
5
|
+
`)}function t(e,t){switch(e.strategy){case`ours`:return t.ours;case`theirs`:return t.theirs;case`merged`:{let i=e.content.split(`
|
|
6
|
+
`);return n(i,t.contextBefore),r(i,t.contextAfter),i.join(`
|
|
7
|
+
`)}}}function n(e,t){if(t===``)return;let n=t.split(`
|
|
8
|
+
`),r=Math.min(n.length,e.length);for(let t=r;t>0;t--){let r=!0;for(let i=0;i<t;i++)if(n[n.length-t+i]!==e[i]){r=!1;break}if(r){e.splice(0,t);return}}}function r(e,t){if(t===``)return;let n=t.split(`
|
|
9
|
+
`),r=Math.min(n.length,e.length);for(let t=r;t>0;t--){let r=!0;for(let i=0;i<t;i++)if(n[i]!==e[e.length-t+i]){r=!1;break}if(r){e.splice(e.length-t,t);return}}}export{e as applyChunks};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Conflict } from "../types/conflict.js";
|
|
2
|
+
import { Resolution, ResolutionMetrics, ResolvedChunk } from "../types/resolution.js";
|
|
3
|
+
|
|
4
|
+
//#region src/resolution/create-resolution.d.ts
|
|
5
|
+
interface CreateResolutionOptions {
|
|
6
|
+
confidence: number;
|
|
7
|
+
description?: string;
|
|
8
|
+
metrics?: ResolutionMetrics;
|
|
9
|
+
}
|
|
10
|
+
declare function createResolution(conflict: Conflict, chunks: ResolvedChunk[], options: CreateResolutionOptions): Resolution;
|
|
11
|
+
//#endregion
|
|
12
|
+
export { CreateResolutionOptions, createResolution };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{applyChunks as e}from"./chunk-apply.js";function t(t,i,a){let o=a.description!==void 0&&a.description.trim().length>0?a.description:r(i);return{filePath:t.filePath,content:e(t.rawContent,t.markers,i),strategy:n(i),confidence:a.confidence,description:o,chunks:i,...a.metrics?{metrics:a.metrics}:{}}}function n(e){if(e.length===0)return`skipped`;let t=new Set(e.map(e=>e.strategy));if(t.size===1){if(t.has(`ours`))return`take-ours`;if(t.has(`theirs`))return`take-theirs`}return`ai`}function r(e){if(e.length===0)return`No markers resolved.`;let t=new Set(e.map(e=>e.strategy));if(t.size===1){if(t.has(`ours`))return`All markers resolved by taking ours.`;if(t.has(`theirs`))return`All markers resolved by taking theirs.`}return`Resolved ${e.length} marker(s) with mixed strategies.`}export{t as createResolution};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Conflict } from "../types/conflict.js";
|
|
2
|
+
import { Resolution } from "../types/resolution.js";
|
|
3
|
+
|
|
4
|
+
//#region src/resolution/take-strategy.d.ts
|
|
5
|
+
declare function takeOurs(conflict: Conflict): Resolution;
|
|
6
|
+
declare function takeTheirs(conflict: Conflict): Resolution;
|
|
7
|
+
//#endregion
|
|
8
|
+
export { takeOurs, takeTheirs };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{createResolution as e}from"./create-resolution.js";function t(t){return e(t,t.markers.map((e,t)=>({markerIndex:t,strategy:`ours`})),{confidence:1})}function n(t){return e(t,t.markers.map((e,t)=>({markerIndex:t,strategy:`theirs`})),{confidence:1})}export{t as takeOurs,n as takeTheirs};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ConflictGitPort } from "../ports/git.js";
|
|
2
|
+
import { Resolution } from "../types/resolution.js";
|
|
3
|
+
|
|
4
|
+
//#region src/resolver/apply-resolutions.d.ts
|
|
5
|
+
declare function applyResolutions(resolutions: Resolution[], deps: {
|
|
6
|
+
git: ConflictGitPort;
|
|
7
|
+
}): Promise<void>;
|
|
8
|
+
//#endregion
|
|
9
|
+
export { applyResolutions };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{stageFiles as e,writeFile as t}from"../git/port-dispatch.js";async function n(n,r){let i=n.filter(e=>e.strategy!==`skipped`);if(i.length!==0){for(let e of i)await t(r.git,e.filePath,e.content);await e(r.git,i.map(e=>e.filePath))}}export{n as applyResolutions};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ConflictGitPort } from "../ports/git.js";
|
|
2
|
+
import { ConflictModelPort, ResolverConfig } from "../ports/model.js";
|
|
3
|
+
import { ConflictProgressEvent } from "../types/events.js";
|
|
4
|
+
|
|
5
|
+
//#region src/resolver/deps.d.ts
|
|
6
|
+
interface ResolverDeps<C extends ResolverConfig = ResolverConfig> {
|
|
7
|
+
model: ConflictModelPort;
|
|
8
|
+
git: ConflictGitPort;
|
|
9
|
+
config?: C;
|
|
10
|
+
onProgress?: (event: ConflictProgressEvent) => void;
|
|
11
|
+
signal?: AbortSignal;
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
export { ResolverDeps };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Conflict } from "../types/conflict.js";
|
|
2
|
+
import { ResolutionContext, ResolutionMetrics, ResolvedChunk } from "../types/resolution.js";
|
|
3
|
+
import { ResolverDeps } from "./deps.js";
|
|
4
|
+
|
|
5
|
+
//#region src/resolver/loop.d.ts
|
|
6
|
+
interface ResolverLoopResult {
|
|
7
|
+
chunks: ResolvedChunk[];
|
|
8
|
+
confidence: number;
|
|
9
|
+
description: string;
|
|
10
|
+
metrics: ResolutionMetrics;
|
|
11
|
+
}
|
|
12
|
+
declare function runResolverLoop(conflict: Conflict, context: ResolutionContext, deps: ResolverDeps): Promise<ResolverLoopResult>;
|
|
13
|
+
//#endregion
|
|
14
|
+
export { ResolverLoopResult, runResolverLoop };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{AIError as e}from"../errors/ai.js";import{parseModelResponse as t}from"./parse-response.js";import{buildPrompt as n}from"./prompt.js";import{createResolverTools as r}from"./tools.js";import{validateChunks as i}from"./validate.js";async function a(a,s,c){let l=c.config?.maxSteps??15,u=c.config?.temperature??0,{definitions:d,execute:f}=r(c.git),{system:p,userMessage:m}=n(a,s),h=[{role:`user`,content:m}],g=c.signal?{signal:c.signal}:void 0,_=0,v=0,y=[];for(;_<l;){_++;let e=await c.model.generate({system:p,messages:[...h],tools:d,temperature:u,signal:c.signal});if(e.usage&&(y.push(e.usage),c.onProgress?.({type:`resolver:step-usage`,filePath:a.filePath,stepNumber:_,usage:e.usage})),e.toolCalls&&e.toolCalls.length>0){h.push({role:`assistant`,content:e.text,toolCalls:e.toolCalls});for(let t of e.toolCalls){c.onProgress?.({type:`resolver:tool-call`,filePath:a.filePath,tool:t.name,args:t.args,stepNumber:_}),v++;let e=await f(t,g);h.push({role:`tool`,toolCallId:e.toolCallId,toolName:t.name,content:e.content})}continue}let n=e.text??``;if(!n.trim()){h.push({role:`assistant`,content:``}),h.push({role:`user`,content:`Your response was empty. Please respond with a valid JSON object containing "confidence" (0-1), "description" (1-2 sentences), and "chunks" (array of { markerIndex, strategy, content? }).`});continue}let r;try{r=t(n,a.markers.length)}catch(e){let t=e instanceof Error?e.message:String(e);h.push({role:`assistant`,content:n}),h.push({role:`user`,content:`Failed to parse your response: ${t}. Please respond with a valid JSON object containing "confidence" (0-1), "description" (1-2 sentences), and "chunks" (array of { markerIndex, strategy, content? }).`});continue}let s=i(r.chunks,a);if(!s.valid){h.push({role:`assistant`,content:n}),h.push({role:`user`,content:`Validation failed: ${s.feedback}\nPlease fix the issues and respond with a corrected JSON object.`});continue}return{chunks:r.chunks,confidence:r.confidence,description:r.description,metrics:o(y,_,v)}}throw new e(`VALIDATION_EXHAUSTED`,`Resolver exhausted ${l} steps without producing a valid resolution for ${a.filePath}.`)}function o(e,t,n){let r={inputTokens:0,outputTokens:0};for(let t of e)r.inputTokens+=t.inputTokens,r.outputTokens+=t.outputTokens,t.cacheReadTokens&&(r.cacheReadTokens=(r.cacheReadTokens??0)+t.cacheReadTokens),t.cacheWriteTokens&&(r.cacheWriteTokens=(r.cacheWriteTokens??0)+t.cacheWriteTokens),t.reasoningTokens&&(r.reasoningTokens=(r.reasoningTokens??0)+t.reasoningTokens),t.cost&&(r.cost=(r.cost??0)+t.cost);return{...r,stepCount:t,toolCallCount:n}}export{a as runResolverLoop};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ResolvedChunk } from "../types/resolution.js";
|
|
2
|
+
|
|
3
|
+
//#region src/resolver/parse-response.d.ts
|
|
4
|
+
interface ParsedResolution {
|
|
5
|
+
confidence: number;
|
|
6
|
+
description: string;
|
|
7
|
+
chunks: ResolvedChunk[];
|
|
8
|
+
}
|
|
9
|
+
declare function parseModelResponse(text: string, markerCount: number): ParsedResolution;
|
|
10
|
+
//#endregion
|
|
11
|
+
export { ParsedResolution, parseModelResponse };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function e(e,n){let r=t(e);if(r===null)throw Error(`No valid JSON object found in model response.`);let i;try{i=JSON.parse(r)}catch{throw Error(`Failed to parse JSON from model response.`)}if(typeof i!=`object`||!i||Array.isArray(i))throw Error(`Model response must be a JSON object.`);let a=i,o=a.confidence;if(typeof o!=`number`||!Number.isFinite(o)||o<0||o>1)throw Error(`Field "confidence" must be a number in [0, 1].`);let s=a.description;if(typeof s!=`string`||s.trim().length===0)throw Error(`Field "description" must be a non-empty string.`);let c=a.chunks;if(!Array.isArray(c))throw Error(`Field "chunks" must be an array.`);let l=[];for(let e of c){if(typeof e!=`object`||!e)throw Error(`Each chunk must be an object.`);let t=e,r=t.markerIndex,i=t.strategy;if(typeof r!=`number`||!Number.isInteger(r))throw Error(`Each chunk must have an integer markerIndex.`);if(r<0||r>=n)throw Error(`markerIndex ${r} is out of range (0..${n-1}).`);if(i===`ours`||i===`theirs`)l.push({markerIndex:r,strategy:i});else if(i===`merged`){let e=t.content;if(typeof e!=`string`)throw Error(`Chunk at markerIndex ${r} with strategy "merged" must have a string "content".`);l.push({markerIndex:r,strategy:i,content:e})}else throw Error(`Unknown strategy "${String(i)}" at markerIndex ${r}.`)}return{confidence:o,description:s.trim(),chunks:l}}function t(e){let t=e.trim(),n=/```(?:json)?\s*\n([\s\S]*?)\n\s*```/.exec(t);if(n){let e=n[1];if(e!==void 0)return e.trim()}let r=/\{[\s\S]*\}/.exec(t);return r?r[0]:null}export{e as parseModelResponse};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Conflict } from "../types/conflict.js";
|
|
2
|
+
import { ResolutionContext } from "../types/resolution.js";
|
|
3
|
+
|
|
4
|
+
//#region src/resolver/prompt.d.ts
|
|
5
|
+
interface PromptPair {
|
|
6
|
+
system: string;
|
|
7
|
+
userMessage: string;
|
|
8
|
+
}
|
|
9
|
+
declare function buildPrompt(conflict: Conflict, context: ResolutionContext): PromptPair;
|
|
10
|
+
//#endregion
|
|
11
|
+
export { PromptPair, buildPrompt };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
function e(e,r){return{system:t(e),userMessage:n(e,r)}}function t(e){let t=e.markers.length-1;return`You are a git conflict resolver. Your task is to resolve merge conflicts by analyzing the conflicting changes and producing a correct resolution for each conflict marker.
|
|
2
|
+
|
|
3
|
+
You will receive a file with conflict markers. Each marker has an "ours" side (current branch) and a "theirs" side (incoming branch), and optionally a "base" (common ancestor).
|
|
4
|
+
|
|
5
|
+
When provided, the "Three-way diffs" section shows what each side changed relative to the common ancestor. Prefer resolving from this data and the conflict markers. Use the tools (show_file_at_ref, grep, blame, diff, log) only when the provided context is insufficient.
|
|
6
|
+
|
|
7
|
+
You have access to tools to inspect the repository: show_file_at_ref, grep, blame, diff, log. Use them to understand the intent behind each change before resolving.
|
|
8
|
+
|
|
9
|
+
Tool outputs are capped to keep context manageable. Prefer narrow queries: pass startLine/endLine when you need specific regions of a file, use path scope on grep/log, and use specific patterns over broad ones. If a tool response includes a 'capped' header, the underlying data is larger — re-run with a narrower range or filter to see other regions.
|
|
10
|
+
|
|
11
|
+
When you are ready to resolve, respond with a JSON object in this format:
|
|
12
|
+
|
|
13
|
+
\`\`\`json
|
|
14
|
+
{
|
|
15
|
+
"confidence": 0.85,
|
|
16
|
+
"description": "Combined both sides because they edited adjacent fields without conflicting intent.",
|
|
17
|
+
"chunks": [
|
|
18
|
+
{ "markerIndex": 0, "strategy": "ours" },
|
|
19
|
+
{ "markerIndex": 1, "strategy": "theirs" },
|
|
20
|
+
{ "markerIndex": 2, "strategy": "merged", "content": "resolved code here" }
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
\`\`\`
|
|
24
|
+
|
|
25
|
+
Top-level fields:
|
|
26
|
+
- "confidence": a number in [0, 1]. 1 = absolutely sure the resolution preserves both sides' intent. 0.5 = significant uncertainty. Lower if the conflict is ambiguous, semantics are unclear, or you had to guess.
|
|
27
|
+
- "description": one or two sentences in plain English explaining WHAT was resolved and WHY. Do not give a chunk-by-chunk play-by-play.
|
|
28
|
+
- "chunks": array of resolved chunks, one per conflict marker.
|
|
29
|
+
|
|
30
|
+
Rules:
|
|
31
|
+
- You must resolve ALL ${e.markers.length} marker(s). Every markerIndex from 0 to ${t} must appear exactly once in "chunks".
|
|
32
|
+
- Use "ours" to keep the current branch version.
|
|
33
|
+
- Use "theirs" to keep the incoming branch version.
|
|
34
|
+
- Use "merged" with "content" when you need to combine or rewrite both sides. The content replaces only the conflict region (between <<<<<<< and >>>>>>>), not the entire file.
|
|
35
|
+
- Do NOT include conflict markers (<<<<<<<, =======, >>>>>>>) in your resolved content.
|
|
36
|
+
- Respond ONLY with the JSON object when you are ready. Do not wrap it in markdown code blocks.`}function n(e,t){let n=[],r=t.threeWayDiff;if(n.push(`File: ${e.filePath}`),n.push(``),t.commitMessage&&(n.push(`Commit message: ${t.commitMessage}`),n.push(``)),t.prDescription&&(n.push(`PR description: ${t.prDescription}`),n.push(``)),t.fileNeighbors&&t.fileNeighbors.length>0&&(n.push(`Related files in this directory: ${t.fileNeighbors.join(`, `)}`),n.push(``)),t.previousResolutions&&t.previousResolutions.length>0){n.push(`Previously resolved conflicts in this session:`);for(let e of t.previousResolutions)n.push(` - ${e.filePath}: ${e.strategy} (${e.description})`);n.push(``)}if(r){let e=t.refs?.ours??`ours`,i=t.refs?.theirs??`theirs`;n.push(`Three-way diffs for this file:`),n.push(``),n.push(`--- ours changes (base → ${e}) ---`),n.push(r.oursDiff),n.push(``),n.push(`--- theirs changes (base → ${i}) ---`),n.push(r.theirsDiff),n.push(``)}n.push(`Conflict markers (${e.markers.length} total):`),n.push(``);for(let t=0;t<e.markers.length;t++){let r=e.markers[t];r&&(n.push(`--- Marker ${t} (lines ${r.startLine}-${r.endLine}) ---`),r.contextBefore&&n.push(`Context before:\n${r.contextBefore}`),n.push(`<<<<<<< ${r.oursLabel}`),n.push(r.ours),r.base!==null&&(n.push(`||||||| ${r.baseLabel??`base`}`),n.push(r.base)),n.push(`=======`),n.push(r.theirs),n.push(`>>>>>>> ${r.theirsLabel}`),r.contextAfter&&n.push(`Context after:\n${r.contextAfter}`),n.push(``))}return n.join(`
|
|
37
|
+
`)}export{e as buildPrompt};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Conflict } from "../types/conflict.js";
|
|
2
|
+
import { Resolution, ResolutionContext } from "../types/resolution.js";
|
|
3
|
+
import { ResolverDeps } from "./deps.js";
|
|
4
|
+
|
|
5
|
+
//#region src/resolver/resolve-conflict.d.ts
|
|
6
|
+
declare function resolveConflict(conflict: Conflict, context: ResolutionContext, deps: ResolverDeps): Promise<Resolution>;
|
|
7
|
+
//#endregion
|
|
8
|
+
export { resolveConflict };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{createResolution as e}from"../resolution/create-resolution.js";import{runResolverLoop as t}from"./loop.js";import{buildThreeWayDiff as n}from"./three-way-diff.js";async function r(r,i,a){let{chunks:o,confidence:s,description:c,metrics:l}=await t(r,i.threeWayDiff||!i.refs?i:{...i,threeWayDiff:await n(r.filePath,i.refs,{git:a.git,signal:a.signal})},{model:a.model,git:a.git,config:a.config,onProgress:a.onProgress,signal:a.signal}),u=e(r,o,{confidence:s,description:c,metrics:l});return a.onProgress?.({type:`resolver:completed`,filePath:r.filePath,stepCount:l.stepCount??0}),u}export{r as resolveConflict};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ResolutionContext } from "../types/resolution.js";
|
|
2
|
+
import { ResolverDeps } from "./deps.js";
|
|
3
|
+
import { StepConfig, StepResult } from "../types/step.js";
|
|
4
|
+
|
|
5
|
+
//#region src/resolver/resolve-conflicts.d.ts
|
|
6
|
+
type ResolveConflictsDeps = ResolverDeps<StepConfig>;
|
|
7
|
+
declare function resolveConflicts(deps: ResolveConflictsDeps, context?: ResolutionContext): Promise<StepResult>;
|
|
8
|
+
//#endregion
|
|
9
|
+
export { ResolveConflictsDeps, resolveConflicts };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{AIError as e}from"../errors/ai.js";import{unmergedEntries as t}from"../git/port-dispatch.js";import{extractConflict as n}from"../extraction/extract.js";import{takeOurs as r,takeTheirs as i}from"../resolution/take-strategy.js";import{resolveConflict as a}from"./resolve-conflict.js";import o from"picomatch";async function s(e,r){let i=e.config?.excludeFiles??[],a=i.length>0?o(i,{basename:!0}):()=>!1,s=(await t(e.git,{signal:e.signal})).filter(e=>!a(e.path)),u={...r??{}},d=[],f=[];for(let t of s){if(e.signal?.aborted)break;let r;try{r=await n(t.path,{git:e.git,signal:e.signal})}catch(e){f.push({filePath:t.path,error:l(e)});continue}if(!r)continue;e.onProgress?.({type:`conflict:found`,filePath:r.filePath,conflictType:r.type,markerCount:r.markers.length});let i=await c(r,u,e,f);i&&(u.previousResolutions=[...u.previousResolutions??[],i],d.push(i),e.onProgress?.({type:`resolution:applied`,filePath:i.filePath,strategy:i.strategy,confidence:i.confidence,description:i.description}))}return{resolutions:d,errors:f}}async function c(t,n,o,s){try{return await a(t,n,{model:o.model,git:o.git,config:o.config,onProgress:o.onProgress,signal:o.signal})}catch(n){let a=l(n),c=o.config?.fallbackStrategy;return c?(o.onProgress?.({type:`resolution:fallback`,filePath:t.filePath,error:a,cause:a instanceof e?a.code:void 0}),c===`take-ours`?r(t):i(t)):(s.push({filePath:t.filePath,error:a}),o.onProgress?.({type:`resolution:failed`,filePath:t.filePath,error:a}),null)}}function l(e){return e instanceof Error?e:Error(String(e))}export{s as resolveConflicts};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ConflictGitPort } from "../ports/git.js";
|
|
2
|
+
import { ResolutionRefs, ThreeWayDiff } from "../types/resolution.js";
|
|
3
|
+
|
|
4
|
+
//#region src/resolver/three-way-diff.d.ts
|
|
5
|
+
declare function buildThreeWayDiff(filePath: string, refs: ResolutionRefs, deps: {
|
|
6
|
+
git: ConflictGitPort;
|
|
7
|
+
signal?: AbortSignal;
|
|
8
|
+
}): Promise<ThreeWayDiff>;
|
|
9
|
+
//#endregion
|
|
10
|
+
export { buildThreeWayDiff };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{diff as e,mergeBase as t}from"../git/port-dispatch.js";async function n(n,r,i){let a=r.base??await t(i.git,r.ours,r.theirs,{signal:i.signal}),[o,s]=await Promise.all([e(i.git,a,r.ours,{path:n,signal:i.signal}),e(i.git,a,r.theirs,{path:n,signal:i.signal})]);return{oursDiff:o,theirsDiff:s}}export{n as buildThreeWayDiff};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ConflictGitPort, OpOptions } from "../ports/git.js";
|
|
2
|
+
import { ToolCall, ToolDefinition, ToolResult } from "../types/tool.js";
|
|
3
|
+
|
|
4
|
+
//#region src/resolver/tools.d.ts
|
|
5
|
+
interface ResolverTools {
|
|
6
|
+
definitions: ToolDefinition[];
|
|
7
|
+
execute: (call: ToolCall, opts?: OpOptions) => Promise<ToolResult>;
|
|
8
|
+
}
|
|
9
|
+
declare function createResolverTools(git: ConflictGitPort): ResolverTools;
|
|
10
|
+
//#endregion
|
|
11
|
+
export { createResolverTools };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{blame as e,diff as t,grep as n,log as r,showFile as i}from"../git/port-dispatch.js";function a(e,t){let n=e[t];if(!(typeof n!=`number`||!Number.isInteger(n)))return Math.max(1,n)}function o(o){let s={show_file_at_ref:async(e,t)=>{let n=String(e.ref??``),r=String(e.path??``),s=a(e,`startLine`),c=a(e,`endLine`);return i(o,n,r,{...t,startLine:s,endLine:c})},grep:async(e,t)=>{let r=String(e.pattern??``),i=e.ref==null?void 0:String(e.ref),s=a(e,`maxResults`);return n(o,r,{...t,ref:i,maxResults:s})},blame:async(t,n)=>{let r=String(t.path??``),i=t.ref==null?void 0:String(t.ref),s=a(t,`startLine`),c=a(t,`endLine`);return e(o,r,{...n,ref:i,startLine:s,endLine:c})},diff:async(e,n)=>{let r=String(e.from??``),i=String(e.to??``),a=e.path==null?void 0:String(e.path);return t(o,r,i,{...n,path:a})},log:async(e,t)=>{let n=e.ref==null?void 0:String(e.ref),i=e.path==null?void 0:String(e.path),s=a(e,`maxCount`);return r(o,{...t,ref:n,path:i,maxCount:s})}},c=[{name:`show_file_at_ref`,description:`Show the contents of a file at a specific git ref (commit, branch, tag). Output is capped at 1000 lines — pass startLine/endLine to read a specific region of a larger file.`,parameters:{type:`object`,properties:{ref:{type:`string`,description:`Git ref (commit SHA, branch name, tag).`},path:{type:`string`,description:`File path relative to the repository root.`},startLine:{type:`integer`,description:`Optional 1-indexed start line (inclusive). Use with endLine to read a region.`},endLine:{type:`integer`,description:`Optional 1-indexed end line (inclusive).`}},required:[`ref`,`path`]}},{name:`grep`,description:`Search for a pattern in the repository using git grep. Results are capped at 100 matches — narrow the search with a more specific pattern, a path scope, or a smaller maxResults.`,parameters:{type:`object`,properties:{pattern:{type:`string`,description:`Search pattern.`},ref:{type:`string`,description:`Optional git ref to search in.`},maxResults:{type:`integer`,description:`Optional cap on returned matches (default 100, hard maximum 100).`}},required:[`pattern`]}},{name:`blame`,description:`Show git blame (line-by-line authorship) for a file. Output is capped at 1000 lines — pass startLine/endLine to inspect a specific region.`,parameters:{type:`object`,properties:{path:{type:`string`,description:`File path relative to the repository root.`},ref:{type:`string`,description:`Optional git ref to blame at.`},startLine:{type:`integer`,description:`Optional 1-indexed start line (inclusive). Maps to git blame -L start,end.`},endLine:{type:`integer`,description:`Optional 1-indexed end line (inclusive).`}},required:[`path`]}},{name:`diff`,description:`Show the diff between two git refs.`,parameters:{type:`object`,properties:{from:{type:`string`,description:`Source git ref.`},to:{type:`string`,description:`Target git ref.`},path:{type:`string`,description:`Optional file path to scope the diff.`}},required:[`from`,`to`]}},{name:`log`,description:`Show commit history (one line per commit). Output is capped at 100 commits — pass a path to scope to a single file or refine the ref range to focus the view.`,parameters:{type:`object`,properties:{ref:{type:`string`,description:`Optional git ref to log from.`},path:{type:`string`,description:`Optional file path to scope the log.`},maxCount:{type:`integer`,description:`Optional maximum number of commits to return (default 100, hard maximum 100).`}},required:[]}}];async function l(e,t){let n=s[e.name];if(!n)return{toolCallId:e.id,content:`Unknown tool: ${e.name}`,isError:!0};try{let r=await n(e.args,t);return{toolCallId:e.id,content:r}}catch(t){let n=t instanceof Error?t.message:String(t);return{toolCallId:e.id,content:n,isError:!0}}}return{definitions:c,execute:l}}export{o as createResolverTools};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Conflict } from "../types/conflict.js";
|
|
2
|
+
import { ResolvedChunk } from "../types/resolution.js";
|
|
3
|
+
|
|
4
|
+
//#region src/resolver/validate.d.ts
|
|
5
|
+
interface ValidationResult {
|
|
6
|
+
valid: boolean;
|
|
7
|
+
feedback: string;
|
|
8
|
+
}
|
|
9
|
+
declare function validateChunks(chunks: ResolvedChunk[], conflict: Conflict): ValidationResult;
|
|
10
|
+
//#endregion
|
|
11
|
+
export { ValidationResult, validateChunks };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=[/^<{7}(\s|$)/,/^\|{7}(\s|$)/,/^={7}(\s|$)/,/^>{7}(\s|$)/];function t(t,n){let r=[],i=n.markers.length,a=new Set;for(let e of t)a.has(e.markerIndex)&&r.push(`Duplicate markerIndex ${e.markerIndex}.`),a.add(e.markerIndex);for(let e=0;e<i;e++)a.has(e)||r.push(`Missing resolution for marker ${e}.`);for(let n of t)if(n.strategy===`merged`){let t=n.content.split(`
|
|
2
|
+
`);for(let i of t)for(let t of e)t.test(i)&&r.push(`Marker ${n.markerIndex}: resolved content contains conflict marker "${i.trim()}".`)}return r.length===0?{valid:!0,feedback:``}:{valid:!1,feedback:r.join(` `)}}export{t as validateChunks};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//#region src/types/conflict.d.ts
|
|
2
|
+
type ConflictType = 'text';
|
|
3
|
+
interface ConflictMarker {
|
|
4
|
+
startLine: number;
|
|
5
|
+
endLine: number;
|
|
6
|
+
ours: string;
|
|
7
|
+
base: string | null;
|
|
8
|
+
theirs: string;
|
|
9
|
+
oursLabel: string;
|
|
10
|
+
theirsLabel: string;
|
|
11
|
+
baseLabel: string | null;
|
|
12
|
+
contextBefore: string;
|
|
13
|
+
contextAfter: string;
|
|
14
|
+
}
|
|
15
|
+
interface Conflict {
|
|
16
|
+
filePath: string;
|
|
17
|
+
markers: ConflictMarker[];
|
|
18
|
+
type: ConflictType;
|
|
19
|
+
rawContent: string;
|
|
20
|
+
}
|
|
21
|
+
//#endregion
|
|
22
|
+
export { Conflict, ConflictMarker, ConflictType };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ConflictType } from "./conflict.js";
|
|
2
|
+
import { ResolutionStrategy } from "./resolution.js";
|
|
3
|
+
import { AiTokenUsage } from "@gitkraken/shared-tools";
|
|
4
|
+
|
|
5
|
+
//#region src/types/events.d.ts
|
|
6
|
+
type ConflictProgressEvent = {
|
|
7
|
+
type: 'conflict:found';
|
|
8
|
+
filePath: string;
|
|
9
|
+
conflictType: ConflictType;
|
|
10
|
+
markerCount: number;
|
|
11
|
+
} | {
|
|
12
|
+
type: 'resolution:applied';
|
|
13
|
+
filePath: string;
|
|
14
|
+
strategy: ResolutionStrategy;
|
|
15
|
+
confidence?: number;
|
|
16
|
+
description?: string;
|
|
17
|
+
} | {
|
|
18
|
+
type: 'resolution:fallback';
|
|
19
|
+
filePath: string;
|
|
20
|
+
error: Error;
|
|
21
|
+
cause?: string;
|
|
22
|
+
} | {
|
|
23
|
+
type: 'resolution:failed';
|
|
24
|
+
filePath: string;
|
|
25
|
+
error: Error;
|
|
26
|
+
} | {
|
|
27
|
+
type: 'resolver:tool-call';
|
|
28
|
+
filePath: string;
|
|
29
|
+
tool: string;
|
|
30
|
+
args: Record<string, unknown>;
|
|
31
|
+
stepNumber: number;
|
|
32
|
+
} | {
|
|
33
|
+
type: 'resolver:step-usage';
|
|
34
|
+
filePath: string;
|
|
35
|
+
stepNumber: number;
|
|
36
|
+
usage: AiTokenUsage;
|
|
37
|
+
} | {
|
|
38
|
+
type: 'resolver:completed';
|
|
39
|
+
filePath: string;
|
|
40
|
+
stepCount: number;
|
|
41
|
+
};
|
|
42
|
+
//#endregion
|
|
43
|
+
export { ConflictProgressEvent };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { AiTokenUsage } from "@gitkraken/shared-tools";
|
|
2
|
+
|
|
3
|
+
//#region src/types/resolution.d.ts
|
|
4
|
+
type ResolutionStrategy = 'ai' | 'take-ours' | 'take-theirs' | 'skipped';
|
|
5
|
+
type ResolvedChunk = {
|
|
6
|
+
markerIndex: number;
|
|
7
|
+
strategy: 'ours' | 'theirs';
|
|
8
|
+
} | {
|
|
9
|
+
markerIndex: number;
|
|
10
|
+
strategy: 'merged';
|
|
11
|
+
content: string;
|
|
12
|
+
};
|
|
13
|
+
interface Resolution {
|
|
14
|
+
filePath: string;
|
|
15
|
+
content: string;
|
|
16
|
+
strategy: ResolutionStrategy;
|
|
17
|
+
confidence: number;
|
|
18
|
+
description: string;
|
|
19
|
+
note?: string;
|
|
20
|
+
chunks?: ResolvedChunk[];
|
|
21
|
+
metrics?: ResolutionMetrics;
|
|
22
|
+
}
|
|
23
|
+
interface ResolutionRefs {
|
|
24
|
+
ours: string;
|
|
25
|
+
theirs: string;
|
|
26
|
+
base?: string;
|
|
27
|
+
}
|
|
28
|
+
interface ThreeWayDiff {
|
|
29
|
+
oursDiff: string;
|
|
30
|
+
theirsDiff: string;
|
|
31
|
+
}
|
|
32
|
+
interface ResolutionContext {
|
|
33
|
+
commitMessage?: string;
|
|
34
|
+
prDescription?: string;
|
|
35
|
+
previousResolutions?: Resolution[];
|
|
36
|
+
fileNeighbors?: string[];
|
|
37
|
+
refs?: ResolutionRefs;
|
|
38
|
+
threeWayDiff?: ThreeWayDiff;
|
|
39
|
+
metadata?: Record<string, unknown>;
|
|
40
|
+
}
|
|
41
|
+
interface ResolutionMetrics extends AiTokenUsage {
|
|
42
|
+
stepCount?: number;
|
|
43
|
+
toolCallCount?: number;
|
|
44
|
+
durationMs?: number;
|
|
45
|
+
metadata?: Record<string, unknown>;
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
export { Resolution, ResolutionContext, ResolutionMetrics, ResolutionRefs, ResolutionStrategy, ResolvedChunk, ThreeWayDiff };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ResolverConfig } from "../ports/model.js";
|
|
2
|
+
import { Resolution } from "./resolution.js";
|
|
3
|
+
|
|
4
|
+
//#region src/types/step.d.ts
|
|
5
|
+
interface StepResult {
|
|
6
|
+
resolutions: Resolution[];
|
|
7
|
+
errors: {
|
|
8
|
+
filePath: string;
|
|
9
|
+
error: Error;
|
|
10
|
+
}[];
|
|
11
|
+
}
|
|
12
|
+
interface StepConfig extends ResolverConfig {
|
|
13
|
+
excludeFiles?: string[];
|
|
14
|
+
fallbackStrategy?: 'take-ours' | 'take-theirs';
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
export { StepConfig, StepResult };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region src/types/tool.d.ts
|
|
2
|
+
interface ToolDefinition {
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
parameters: Record<string, unknown>;
|
|
6
|
+
}
|
|
7
|
+
interface ToolCall {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
args: Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
interface ToolResult {
|
|
13
|
+
toolCallId: string;
|
|
14
|
+
content: string;
|
|
15
|
+
isError?: boolean;
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
export { ToolCall, ToolDefinition, ToolResult };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{};
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gitkraken/conflict-tools",
|
|
3
|
+
"version": "0.0.1-beta.1",
|
|
4
|
+
"description": "AI-powered git conflict resolution.",
|
|
5
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
6
|
+
"author": "Axosoft, LLC dba GitKraken",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">= 22"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"sideEffects": false,
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"CHANGELOG.md"
|
|
21
|
+
],
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"picomatch": "^4.0.4",
|
|
24
|
+
"@gitkraken/shared-tools": "^0.1.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "^22.0.0",
|
|
28
|
+
"@types/picomatch": "^4.0.3",
|
|
29
|
+
"typescript": "^6.0.2",
|
|
30
|
+
"vitest": "^4.1.1"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsdown --config-loader=unrun",
|
|
34
|
+
"build:dev": "tsdown --config tsdown.dev.config.ts --config-loader=unrun",
|
|
35
|
+
"typecheck": "tsc --noEmit",
|
|
36
|
+
"test": "vitest run --passWithNoTests"
|
|
37
|
+
}
|
|
38
|
+
}
|