@gitkraken/conflict-tools 0.2.4-beta.22 → 0.2.4-beta.24
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 +4 -2
- package/README.md +16 -7
- package/dist/errors/conflict.d.ts +1 -1
- package/dist/git/port-dispatch.js +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- package/dist/ports/git.d.ts +4 -0
- package/dist/reference-integrity/detect.js +2 -2
- package/dist/reference-integrity/snapshot.d.ts +2 -2
- package/dist/reference-integrity/snapshot.js +1 -1
- package/dist/resolution/detect-code-loss.d.ts +23 -0
- package/dist/resolution/detect-code-loss.js +1 -0
- package/dist/resolver/fix-loop.js +2 -2
- package/dist/resolver/model-budget.js +1 -1
- package/dist/resolver/resolve-conflicts.js +1 -1
- package/dist/resolver/summarize-operation.js +1 -1
- package/dist/types/step.d.ts +3 -0
- package/dist/types/summary.d.ts +7 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -21,9 +21,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
21
21
|
- `ResolutionContext.detectionRefs` — optional parent tips for reference-integrity detection's before/after comparison, separate from `refs` (which drives the resolver's three-way diff). Detection uses `detectionRefs ?? refs`. Needed when the resolver's refs are the in-flight per-step state (e.g. a rebase) while detection needs the two stable pre-merge branch tips; in a merge they coincide and it can be left unset.
|
|
22
22
|
- Binding-aware broken-reference detection. `ParsedFile` gains an optional `bindings` (names bound at any scope, module-level and nested), and reference-integrity now treats a name as "resolvable before the merge" if a parent resolved it as a reference **or** defined/bound it. This catches a reference broken by removing a nested binding (e.g. a `const` used by a closure elsewhere in the same file): the dangling use was never a resolved reference — because it was locally bound — so the before/after reference diff alone could not see the regression. Backends that only surface top-level `definitions` can omit `bindings`; they simply do not gain the nested-binding case.
|
|
23
23
|
- Operation-level assessment: `buildOperationDigest`/`summarizeOperation` now fold `StepResult.detection` into the rollup. A file with any finding is added to `needsReview` (`reason: 'finding'`) regardless of confidence — even a file with no resolution of its own; an error finding (broken-reference, syntax-error, dropped-side) drives `verdict: 'blocked'`, while a warning finding (duplicate-definition, consumer-validator warnings) drives `needs-review`; `OperationDigest.detectionComplete` reports whether every detection check ran (so empty findings on a skipped check are not read as clean); and a derived `OperationDigest.verdict` (`'clean' | 'needs-review' | 'blocked'`) gives consumers an honest gate signal. `ReviewItem` gains `reason` and optional `findingCategories`, and its `confidence` is now optional.
|
|
24
|
+
- `detectFallbackCodeLoss(conflict, strategy)` and the `CodeLossDetection` type (carrying `discardedSide`), exported from the package root.
|
|
25
|
+
- `StepConfig.allowLossyFallback?: boolean` escape hatch.
|
|
26
|
+
- `ConflictError` code `CODE_LOSS_RISK`.
|
|
24
27
|
|
|
25
28
|
### Changed
|
|
26
29
|
|
|
30
|
+
- **Breaking for `fallbackStrategy` users:** a wholesale `take-ours`/`take-theirs` fallback (taken when the AI resolver fails) is no longer applied to text conflicts. The file is recorded as a `CODE_LOSS_RISK` error with a `resolution:failed` event instead of completing silently, so the consumer stops for manual resolution. The fallback is applied with `git checkout --ours/--theirs`, which replaces the whole file with one stage and discards the other side plus anything git auto-merged outside the conflict markers; on rebase "ours" is everything integrated so far, so a blind take-theirs could wipe it (AGP-430). Only a delete-modify conflict resolved toward the side that keeps content still applies the fallback. Set `StepConfig.allowLossyFallback: true` to opt back into the previous behavior. Deliberate `rules` (e.g. `take-theirs` for lock files) are unaffected.
|
|
27
31
|
- Reference-integrity findings are now tiered by locality: an intra-file break — a local definition/binding the merge dropped while a use of it remains in the same file (e.g. a `const` used by a closure below) — is a `tier: 'file'` finding; a broken cross-file reference (an import whose target stopped resolving) stays `tier: 'project'`. Previously every `broken-reference` was `'project'`. Classification uses the reference's `target` file when the parser supplies it, else whether the name was a local definition/binding in the parent. This sets up the per-file vs cross-file fix loops.
|
|
28
32
|
- **Breaking**: consumer validation moved to a `Validator` port — `validate(sources: SourceOverlay): Promise<ValidatorFinding[]>` (replaces the per-file `ResolutionVerifier.verify`). It runs post-refine, non-blocking, over an overlay; its findings fold into the operation's detection findings and verdict. The overlay is currently always a single file — `SourceOverlay` is the parameter type so a whole-set call can be added without a further breaking change, but no such call site exists yet. `Resolution.findings` replaces `Resolution.issues`. `Finding.category` (and `ValidatorFinding.category`) is now `FindingCategory | (string & {})` — open to consumer categories while keeping the package's own values discoverable and autocompleting, matching the existing `UnmergedReason` precedent.
|
|
29
33
|
- **Breaking**: the resolver's in-loop blocking check is now **residual conflict markers only** (built-in, language-agnostic). Consumer validation (the `Validator` port, above) no longer gates the resolve loop or triggers re-resolution — it runs once **after** refine, **non-blocking**, and its findings are folded into the report (advisory). This stops the resolver from burning its step budget on non-marker validation (which was exhausting into a whole-file `take-theirs` fallback), and puts consumer validation in its intended post-refine, report-only role. `ResolverLoopResult` no longer carries `issues`.
|
|
@@ -35,7 +39,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
35
39
|
- **Breaking**: `ResolutionVerifier` — replaced by the `Validator` port (see Changed). `defaultVerifier` and `VerificationResult` are no longer exported either; `checkResidualMarkers` stays internal as the built-in marker gate.
|
|
36
40
|
- **Breaking**: the `Issue` type is no longer exported from the package root. It only ever described the internal marker-gate result, which is not part of the public surface now that `VerificationResult` is unexported.
|
|
37
41
|
- **Breaking**: the `verification:issues` progress event. Consumer validation no longer runs inside the resolve loop, so the event had no emitter; validator output arrives as `Finding`s on the detection report, and a throwing validator is reported via `validation:failed`.
|
|
38
|
-
- **Breaking**: `ResolverConfig.maxModelCalls` moved to `StepConfig` (see Added). The fix phase is what enforces the budget and the fix phase is batch-only, so on `resolveConflict` the field was silently inert.
|
|
39
42
|
|
|
40
43
|
### Fixed
|
|
41
44
|
|
|
@@ -55,7 +58,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
55
58
|
- Fix instructions are bounded and stripped of control characters. Finding messages come from a parser or a consumer validator and therefore carry repository-derived text; a multi-line message landed in a numbered instruction list as several forged instructions, and nothing capped how many findings or how much text reached the model on every iteration.
|
|
56
59
|
- Cross-side provenance no longer misclassifies an intra-file break as cross-file. Definitions and bindings were collected one side at a time, so a name that `ours` resolved via an import but `theirs` defined locally recorded `crossFile: true` and was tiered `project` — routed to the slower cross-file stage instead of the per-file loop. Both sides' definitions and bindings are now collected before either side's references.
|
|
57
60
|
- `fix: true` without `refs` is no longer a total no-op. The per-file stage runs on what needs no parent content (syntax errors, validator findings); only cross-file routing, which is driven by the whole-set before/after report, is skipped.
|
|
58
|
-
|
|
59
61
|
- The opt-in fix phase is now fault-isolated. A parser, validator, or model throw inside the fix phase no longer rejects the whole operation: the resolutions (possibly partially fixed) are kept, both detection checks are reported as `errored` (so the verdict is `needs-review`, never a false `clean`), and the throw is surfaced via `fix:failed`. A user abort still propagates and cancels the operation.
|
|
60
62
|
- `OperationDigest.verdict` can now reach `clean` on a real repository. `detectionComplete` treated `partial-unsupported-language` as "did not run", so any operation whose scope contained a non-code file (package.json, README, lockfiles — always present in a real merge) forced `verdict: 'needs-review'` even with zero findings and full confidence. Since `--fix` gates auto-apply on `verdict === 'clean'`, this made auto-apply effectively unreachable. A check now counts as complete when it ran, including when some scope files were an unsupported language (they carry no references to break); only a check that could not run at all — no parser, no refs, an unreadable parent snapshot, or a thrown parser — leaves detection incomplete.
|
|
61
63
|
- A non-abort error in the refine pass no longer destroys the pre-refine resolution. Previously the refine `catch` rethrew, bubbling to `resolveOne` and degrading a good resolve-stage result into a take-side fallback (confidence 0) — the same class of failure as a throwing validator, one stage later. The pre-refine resolution is now kept, its confidence capped (the enhancement did not complete), and the error reported via `refine:failed`. A user abort still cancels (rethrown past the fallback).
|
package/README.md
CHANGED
|
@@ -99,7 +99,7 @@ const parser: ParserPort = {
|
|
|
99
99
|
};
|
|
100
100
|
```
|
|
101
101
|
|
|
102
|
-
|
|
102
|
+
Four properties define the contract:
|
|
103
103
|
|
|
104
104
|
- **Content-in overlay, no FS access.** `analyze(sources)` sees exactly the `SourceOverlay` contents (a `ReadonlyMap<path, content>`) and never reads the filesystem itself. This is what lets the package hand it pre-merge parent snapshots *and* the post-merge result and diff the two. A tree-sitter backend sees only these files; a language server may resolve against its workspace with these contents overlaid on top.
|
|
105
105
|
- **Language-agnostic.** A file type the backend has no grammar for comes back `supported: false` with empty facts; the detector skips it (and reports `partial-unsupported-language`) rather than emitting false findings. A file it *does* handle but cannot parse is `supported: true` with the failure in `syntaxErrors` — that is a finding, not a coverage gap, and conflating the two would let a broken merge result read as clean.
|
|
@@ -279,7 +279,8 @@ if (digest.verdict === 'clean') {
|
|
|
279
279
|
- `rules` — array of `FileRule` objects. Each rule has a `match` (glob pattern or array of patterns) and a `strategy`. First matching rule wins. Patterns without `/` match on basename; patterns with `/` match on full path.
|
|
280
280
|
- `defaultStrategy` — strategy for files not matching any rule. Defaults to `'ai'`.
|
|
281
281
|
- `fix`, `fixLoopMaxIterations`, `crossFileMaxIterations`, `maxModelCalls` — the fix phase and its bounds. See [Fix phase](#fix-phase-opt-in). All four are batch-only because the fix phase is what enforces them.
|
|
282
|
-
- `fallbackStrategy` — when AI fails for a file, fall back to `'take-ours'` or `'take-theirs'` instead of recording an error.
|
|
282
|
+
- `fallbackStrategy` — when AI fails for a file, fall back to `'take-ours'` or `'take-theirs'` instead of recording an error. **Guarded against code loss, and the guard stops every text conflict.** The fallback is applied with `git checkout --ours/--theirs`, which replaces the whole file with one stage, so it discards the other side plus anything git auto-merged outside the conflict markers. On rebase "ours" is everything integrated so far, so a blind take-theirs can wipe it. The file is recorded as a `CODE_LOSS_RISK` error with a `resolution:failed` event instead of completing silently. The one case that still applies is a delete-modify conflict resolved toward the side that keeps content. Set `allowLossyFallback` to apply the fallback regardless.
|
|
283
|
+
- `allowLossyFallback` — opt back into the pre-guard behavior: apply `fallbackStrategy` wholesale even when it would discard integrated work. Off by default.
|
|
283
284
|
|
|
284
285
|
Available strategies for rules and `defaultStrategy`:
|
|
285
286
|
|
|
@@ -305,7 +306,7 @@ import { detect } from '@gitkraken/conflict-tools';
|
|
|
305
306
|
const report = await detect(step.resolutions, { refs }, { git, parser });
|
|
306
307
|
```
|
|
307
308
|
|
|
308
|
-
`DetectDeps` is `{ git; parser?; signal? }`. It flags:
|
|
309
|
+
`DetectDeps` is `{ git; parser?; signal?; excludePaths? }`. `excludePaths` (a `ReadonlySet<string>`) keeps paths out of scope — pass the files you skipped or failed to resolve, since those still carry conflict markers on disk and the parser would report the marker text as syntax errors. It flags:
|
|
309
310
|
|
|
310
311
|
- references that resolved in a pre-merge parent (ours/theirs) but no longer resolve in the merged result (`broken-reference`, with `provenance`),
|
|
311
312
|
- duplicate definitions and syntax errors across the whole analyzed scope — the resolved files *and* the auto-merged/incoming ones (`duplicate-definition`, `syntax-error`). Duplicates are diffed against the parents, so a legal same-name declaration that already existed (a TS overload, `interface`/`namespace` merging) is not reported; only a duplicate the merge introduced is,
|
|
@@ -406,7 +407,7 @@ Without `ResolutionContext.refs` (or `detectionRefs`) the per-file stage still r
|
|
|
406
407
|
| `crossFileMaxIterations` | `2` | Max cross-file fix rounds (route project-level breaks → re-detect the set) |
|
|
407
408
|
| `maxModelCalls` | unset | Global model-call budget for the whole operation. When set and the budget is spent, the fix phase stops attempting further fixes and leaves the remainder in the findings, emitting `fix:budget-exhausted`. When unset, the fix phase is not budget-limited |
|
|
408
409
|
|
|
409
|
-
The fix phase is **fault-isolated
|
|
410
|
+
The fix phase is **fault-isolated** at two levels. A throw on one file (parser, validator, or model) leaves that file at its pre-fix resolution, emits `fix:failed` with its `filePath`, and the phase moves on — detection still runs over the set. A throw that escapes the whole phase does not reject the operation either: the resolutions computed so far (possibly partially fixed, since the phase mutates them as it goes) are kept, both detection checks are reported as `errored`, and `fix:failed` is emitted without a `filePath`. A user abort always propagates and cancels the operation.
|
|
410
411
|
|
|
411
412
|
## Consumer examples
|
|
412
413
|
|
|
@@ -492,11 +493,11 @@ score(resolution, goldenFile);
|
|
|
492
493
|
| `refine:completed` | Refine finished successfully (`appliedCount`) |
|
|
493
494
|
| `refine:failed` | Refine threw before completing (`reason`) |
|
|
494
495
|
| `refine:bailed` | Refine bailed early after consecutive apply errors, keeping edits that landed (`reason`) |
|
|
495
|
-
| `refine:discarded` | A refine/fix edit batch was rejected
|
|
496
|
+
| `refine:discarded` | A refine/fix edit batch was rejected — it reintroduced conflict markers, or (in the fix phase) it introduced an error the file did not have (`reason` says which) |
|
|
496
497
|
| `refine:touched-user-region` | Refine modified a user-resolved marker region (`markerIndexes`) |
|
|
497
498
|
| `fix:started` | The fix loop began fixing a file (`findingCount`) |
|
|
498
499
|
| `fix:completed` | The fix loop finished evaluating a file (`iterations`, `remainingFindings`) — emitted even for a clean file (iterations 0, remainingFindings 0). `iterations: 0` means "evaluated, spent no model call" |
|
|
499
|
-
| `fix:failed` |
|
|
500
|
+
| `fix:failed` | A non-abort error in the opt-in fix phase (`filePath?`, `reason`). With `filePath`: that one file threw and was left at its pre-fix resolution while the phase carried on — detection still runs. Without `filePath`: the whole phase threw before it could report, so both checks are forced to `errored` |
|
|
500
501
|
| `fix:budget-exhausted` | The global `maxModelCalls` budget was spent (`spent`, `limit`); the fix phase stopped and left the remainder in findings |
|
|
501
502
|
| `validation:failed` | The consumer validator threw; reported and swallowed, resolution kept (`reason`) |
|
|
502
503
|
|
|
@@ -509,6 +510,7 @@ Operation-level events (rebase started, step completed, etc.) are the consumer's
|
|
|
509
510
|
| Error | Codes | When |
|
|
510
511
|
|---|---|---|
|
|
511
512
|
| `ConflictError` | `PARSE_FAILED` | Conflict extraction failed |
|
|
513
|
+
| `ConflictError` | `CODE_LOSS_RISK` | A wholesale `fallbackStrategy` would have discarded integrated work; the fallback was not applied |
|
|
512
514
|
| `AIError` | `VALIDATION_EXHAUSTED`, `INCOMPLETE_RESOLUTION` | Model could not produce a valid resolution after retries |
|
|
513
515
|
| `ConflictGitPortMissingOpError` | — | A required git op is missing and no `exec` fallback was provided |
|
|
514
516
|
|
|
@@ -526,7 +528,14 @@ try {
|
|
|
526
528
|
}
|
|
527
529
|
```
|
|
528
530
|
|
|
529
|
-
In the batch entry point, errors
|
|
531
|
+
In the batch entry point, errors do not throw; the loop continues with the next file. Where they land depends on the fallback:
|
|
532
|
+
|
|
533
|
+
- No `fallbackStrategy`: the error goes to `step.errors`.
|
|
534
|
+
- `fallbackStrategy` set, text conflict: the code-loss guard stops it, so a `CODE_LOSS_RISK` error goes to `step.errors` with a `resolution:failed` event. This is the common path, not the exception.
|
|
535
|
+
- `fallbackStrategy` set, delete-modify resolved toward the side that keeps content: downgraded to a `resolution:fallback` event, with the resolution in `step.resolutions`.
|
|
536
|
+
- `allowLossyFallback: true`: the guard is off, so every fallback takes the `resolution:fallback` path.
|
|
537
|
+
|
|
538
|
+
The advisory stages (refine, validator, detection, fix phase) are fault-isolated — a non-abort throw there is reported and swallowed rather than destroying a good resolution. A user abort (`signal.aborted`) always propagates and cancels the operation.
|
|
530
539
|
|
|
531
540
|
## Semver policy
|
|
532
541
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
//#region src/errors/conflict.d.ts
|
|
2
|
-
type ConflictErrorCode = 'PARSE_FAILED';
|
|
2
|
+
type ConflictErrorCode = 'PARSE_FAILED' | 'CODE_LOSS_RISK';
|
|
3
3
|
declare class ConflictError extends Error {
|
|
4
4
|
readonly code: ConflictErrorCode;
|
|
5
5
|
constructor(code: ConflictErrorCode, message: string, options?: {
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{ConflictGitPortMissingOpError as e}from"../ports/git.js";import{sliceLines as t,truncateLines as n,truncateMatches as r}from"./truncation.js";const i={DD:`both-deleted`,AU:`added-by-us`,UD:`deleted-by-them`,UA:`added-by-them`,DU:`deleted-by-us`,AA:`both-added`,UU:`both-modified`};function a(e){return i[e]??e}function o(e){return`Search capped at ${e} matches. There may be more — narrow with a more specific pattern or path.`}function s(t,n){throw new e(t,n)}function c(e,t){return e!=null&&t!=null?`startLine=${e}, endLine=${t}`:e==null?`endLine=${t}`:`startLine=${e}`}async function l(e,t,n){return e.readFile?e.readFile(t,n):s(`readFile`)}async function u(e,r,i,a){let o;if(e.showFile)o=await e.showFile(r,i,a);else if(e.exec){let n=t(await e.exec([`show`,`${r}:${i}`],{signal:a?.signal}),a?.startLine,a?.endLine);if(!n.ok)return n.reason===`invalid-range`?`[Invalid range: ${c(n.startLine,n.endLine)} (startLine must be <= endLine).]`:`[Range out of bounds: file has ${n.total} lines, requested ${c(n.startLine,n.endLine)}.]`;o=n.content}else return s(`showFile`);return n(o,1e3,`Output capped at 1000 lines. Use startLine/endLine to read other regions of the file.`)}async function d(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(`
|
|
1
|
+
import{ConflictGitPortMissingOpError as e}from"../ports/git.js";import{sliceLines as t,truncateLines as n,truncateMatches as r}from"./truncation.js";const i={DD:`both-deleted`,AU:`added-by-us`,UD:`deleted-by-them`,UA:`added-by-them`,DU:`deleted-by-us`,AA:`both-added`,UU:`both-modified`};function a(e){return i[e]??e}function o(e){return`Search capped at ${e} matches. There may be more — narrow with a more specific pattern or path.`}function s(t,n){throw new e(t,n)}function c(e,t){return e!=null&&t!=null?`startLine=${e}, endLine=${t}`:e==null?`endLine=${t}`:`startLine=${e}`}async function l(e,t,n){return e.readFile?e.readFile(t,n):s(`readFile`)}async function u(e,r,i,a){let o;if(e.showFile)o=await e.showFile(r,i,a);else if(e.exec){let n=t(await e.exec([`show`,`${r}:${i}`],{signal:a?.signal}),a?.startLine,a?.endLine);if(!n.ok)return n.reason===`invalid-range`?`[Invalid range: ${c(n.startLine,n.endLine)} (startLine must be <= endLine).]`:`[Range out of bounds: file has ${n.total} lines, requested ${c(n.startLine,n.endLine)}.]`;o=n.content}else return s(`showFile`);return a?.fullContent?o:n(o,1e3,`Output capped at 1000 lines. Use startLine/endLine to read other regions of the file.`)}async function d(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
2
|
`)){if(!e.startsWith(`u `))continue;let t=e.split(` `),n=t[1]??``,i=t.slice(10).join(` `);r.push({path:i,reason:a(n)})}return r}return s(`unmergedEntries`)}async function f(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():s(`mergeBase`)}async function p(e,t,r){let i=r?.startLine,a=r?.endLine,o=Number.isFinite(i),l=Number.isFinite(a);if(o&&l&&i>a)return`[Invalid range: ${c(i,a)} (startLine must be <= endLine).]`;let u;if(e.blame)u=await e.blame(t,r);else if(e.exec){let n=[`blame`,`--porcelain`],s=o?Math.max(1,i):void 0,c=l?Math.max(1,a):void 0;s!=null&&c!=null?n.push(`-L`,`${s},${c}`):s==null?c!=null&&n.push(`-L`,`1,${c}`):n.push(`-L`,`${s},`),r?.ref&&n.push(r.ref),n.push(`--`,t),u=await e.exec(n,{signal:r?.signal})}else return s(`blame`);return n(u,1e3,`Output capped at 1000 lines. Use startLine/endLine to inspect other regions.`)}async function m(e,t,n){let i=n?.maxResults,a=i!=null&&Number.isFinite(i)?Math.min(Math.max(1,i),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 s(`grep`);return r(c,a,o(a))}async function h(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 s(`diff`)}async function g(e,t,n,r){return e.changedFiles?e.changedFiles(t,n,r):e.exec?(await e.exec([`-c`,`core.quotepath=false`,`diff`,`--name-only`,`-z`,`--end-of-options`,t,n],{signal:r?.signal})).split(`\0`).filter(e=>e.length>0):s(`changedFiles`)}async function _(e,t){let r=t?.maxCount,i=r!=null&&Number.isFinite(r)?Math.min(Math.max(1,r),100):100,a;if(e.log)a=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),a=await e.exec(n,{signal:t?.signal})}else return s(`log`);return n(a,100,`Output capped at 100 commits. Use path to scope to a file or refine the ref range.`)}async function v(e,t,r){let i;if(e.show)i=await e.show(t,r);else if(e.exec){let n=[`show`,t];r?.path&&n.push(`--`,r.path),i=await e.exec(n,{signal:r?.signal})}else return s(`show`);return n(i,500,`Output capped at 500 lines. Use show_file_at_ref with startLine/endLine to read specific regions.`)}async function y(e,t,n,r){return e.writeFile?e.writeFile(t,n,r):s(`writeFile`)}async function b(e,t,n){if(e.stageFiles)return e.stageFiles(t,n);if(e.exec){await e.exec([`add`,`--`,...t],{signal:n?.signal});return}return s(`stageFiles`)}async function x(e,t,n,r){if(e.checkoutFile)return e.checkoutFile(t,n,r);if(e.exec){await e.exec([`checkout`,`--${n}`,`--`,t],{signal:r?.signal});return}return s(`checkoutFile`)}async function S(e,t,n){if(e.removeFile)return e.removeFile(t,n);if(e.exec){await e.exec([`rm`,`--`,t],{signal:n?.signal});return}return s(`removeFile`)}export{p as blame,g as changedFiles,x as checkoutFile,h as diff,m as grep,_ as log,f as mergeBase,l as readFile,S as removeFile,v as show,u as showFile,b as stageFiles,d as unmergedEntries,y as writeFile};
|
package/dist/index.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { ParsedFile, ParserCapabilities, ParserPort, SourceOverlay, SymbolDefini
|
|
|
13
13
|
import { Validator, ValidatorFinding } from "./ports/verify.js";
|
|
14
14
|
import { DetectDeps, detect } from "./reference-integrity/detect.js";
|
|
15
15
|
import { createResolution } from "./resolution/create-resolution.js";
|
|
16
|
+
import { CodeLossDetection, detectFallbackCodeLoss } from "./resolution/detect-code-loss.js";
|
|
16
17
|
import { takeOurs, takeTheirs } from "./resolution/take-strategy.js";
|
|
17
18
|
import { applyResolutions } from "./resolver/apply-resolutions.js";
|
|
18
19
|
import { ConflictProgressEvent } from "./types/events.js";
|
|
@@ -21,4 +22,4 @@ import { FileRule, StepConfig, StepResult } from "./types/step.js";
|
|
|
21
22
|
import { resolveConflicts } from "./resolver/resolve-conflicts.js";
|
|
22
23
|
import { OperationDigest, OperationSummary, OperationVerdict, ReviewItem } from "./types/summary.js";
|
|
23
24
|
import { SummarizeOperationOptions, buildOperationDigest, summarizeOperation } from "./resolver/summarize-operation.js";
|
|
24
|
-
export { AIError, type AIErrorCode, type BlameOptions, type CheckStatus, type Conflict, type ConflictDiffOptions, ConflictError, type ConflictErrorCode, type ConflictGitOps, type ConflictGitPort, ConflictGitPortMissingOpError, type ConflictMarker, type ConflictMarkerUserInput, type ConflictModelMessage, type ConflictModelParams, type ConflictModelPort, type ConflictModelResult, type ConflictProgressEvent, type ConflictType, type DetectDeps, type DetectionReport, type DiffStats, type FileRule, type Finding, type FindingCategory, type FindingTier, type GrepOptions, type IntraFileEditOp, type LogOptions, type OpOptions, type OperationDigest, type OperationSummary, type OperationVerdict, type ParsedFile, type ParserCapabilities, type ParserPort, type Resolution, type ResolutionContext, type ResolutionMetrics, type ResolutionRawSignals, type ResolutionRefs, type ResolutionStrategy, type ResolvedChunk, type ResolverConfig, type ReviewItem, type ShowFileOptions, type ShowOptions, type SourceOverlay, type StepConfig, type StepResult, type SummarizeOperationOptions, type SymbolDefinition, type SymbolReference, type SyntaxIssue, type ThreeWayDiff, type ToolCall, type ToolDefinition, type ToolResult, type UnmergedEntry, type UnmergedReason, type Validator, type ValidatorFinding, applyResolutions, buildOperationDigest, checkoutFile, createResolution, detect, extractConflict, removeFile, resolveConflict, resolveConflicts, show, summarizeOperation, takeOurs, takeTheirs };
|
|
25
|
+
export { AIError, type AIErrorCode, type BlameOptions, type CheckStatus, type CodeLossDetection, type Conflict, type ConflictDiffOptions, ConflictError, type ConflictErrorCode, type ConflictGitOps, type ConflictGitPort, ConflictGitPortMissingOpError, type ConflictMarker, type ConflictMarkerUserInput, type ConflictModelMessage, type ConflictModelParams, type ConflictModelPort, type ConflictModelResult, type ConflictProgressEvent, type ConflictType, type DetectDeps, type DetectionReport, type DiffStats, type FileRule, type Finding, type FindingCategory, type FindingTier, type GrepOptions, type IntraFileEditOp, type LogOptions, type OpOptions, type OperationDigest, type OperationSummary, type OperationVerdict, type ParsedFile, type ParserCapabilities, type ParserPort, type Resolution, type ResolutionContext, type ResolutionMetrics, type ResolutionRawSignals, type ResolutionRefs, type ResolutionStrategy, type ResolvedChunk, type ResolverConfig, type ReviewItem, type ShowFileOptions, type ShowOptions, type SourceOverlay, type StepConfig, type StepResult, type SummarizeOperationOptions, type SymbolDefinition, type SymbolReference, type SyntaxIssue, type ThreeWayDiff, type ToolCall, type ToolDefinition, type ToolResult, type UnmergedEntry, type UnmergedReason, type Validator, type ValidatorFinding, applyResolutions, buildOperationDigest, checkoutFile, createResolution, detect, detectFallbackCodeLoss, extractConflict, removeFile, resolveConflict, resolveConflicts, show, summarizeOperation, takeOurs, takeTheirs };
|
package/dist/index.js
CHANGED
|
@@ -1 +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{checkoutFile as r,removeFile as i,show as a}from"./git/port-dispatch.js";import{extractConflict as o}from"./extraction/extract.js";import{detect as s}from"./reference-integrity/detect.js";import{createResolution as c}from"./resolution/create-resolution.js";import{takeOurs as
|
|
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{checkoutFile as r,removeFile as i,show as a}from"./git/port-dispatch.js";import{extractConflict as o}from"./extraction/extract.js";import{detect as s}from"./reference-integrity/detect.js";import{createResolution as c}from"./resolution/create-resolution.js";import{detectFallbackCodeLoss as l}from"./resolution/detect-code-loss.js";import{takeOurs as u,takeTheirs as d}from"./resolution/take-strategy.js";import{applyResolutions as f}from"./resolver/apply-resolutions.js";import{resolveConflict as p}from"./resolver/resolve-conflict.js";import{resolveConflicts as m}from"./resolver/resolve-conflicts.js";import{buildOperationDigest as h,summarizeOperation as g}from"./resolver/summarize-operation.js";export{e as AIError,t as ConflictError,n as ConflictGitPortMissingOpError,f as applyResolutions,h as buildOperationDigest,r as checkoutFile,c as createResolution,s as detect,l as detectFallbackCodeLoss,o as extractConflict,i as removeFile,p as resolveConflict,m as resolveConflicts,a as show,g as summarizeOperation,u as takeOurs,d as takeTheirs};
|
package/dist/ports/git.d.ts
CHANGED
|
@@ -11,6 +11,10 @@ interface ConflictDiffOptions extends OpOptions {
|
|
|
11
11
|
interface ShowFileOptions extends OpOptions {
|
|
12
12
|
startLine?: number;
|
|
13
13
|
endLine?: number;
|
|
14
|
+
/** Skip the line cap the dispatcher applies by default. The cap keeps file content read for model
|
|
15
|
+
* prompts small; a caller feeding the content to a parser (not a prompt) needs the whole file, since
|
|
16
|
+
* a definition past the cap would silently drop out of the before/after comparison. */
|
|
17
|
+
fullContent?: boolean;
|
|
14
18
|
}
|
|
15
19
|
interface BlameOptions extends OpOptions {
|
|
16
20
|
ref?: string;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{toFileFinding as e}from"../ports/verify.js";import{buildSnapshots as t}from"./snapshot.js";async function n(e,n,i){let
|
|
1
|
+
import{toFileFinding as e}from"../ports/verify.js";import{buildSnapshots as t}from"./snapshot.js";async function n(e,n,i){let a=i.parser;if(!a)return{findings:[],checks:{referenceIntegrity:`skipped-no-parser`,structural:`skipped-no-parser`}};try{let s=n.detectionRefs??n.refs?await t(e,n,i):null;if(s){let t=await a.analyze(s.ours,i.signal),n=await a.analyze(s.theirs,i.signal),l=await a.analyze(s.result,i.signal);return{findings:[...o(t,n,l),...c(l,[t,n]),...u(e,s)],checks:{referenceIntegrity:r(s,a,[t,n,l]),structural:f(l)?`partial-unsupported-language`:`ran`},scope:{inScope:s.inScope,readable:s.readable,truncated:s.truncated,incomingDiscovered:s.incomingDiscovered}}}let l=new Map(e.map(e=>[e.filePath,e.content])),d=await a.analyze(l,i.signal);return{findings:c(d,[]),checks:{referenceIntegrity:`skipped-no-refs`,structural:f(d)?`partial-unsupported-language`:`ran`}}}catch(e){if(i.signal?.aborted)throw e;return{findings:[],checks:{referenceIntegrity:`errored`,structural:`errored`}}}}function r(e,t,n){return e.inScope>0&&e.readable===0?`skipped-unreadable-source`:t.capabilities?.crossFileResolution?n.some(f)?`partial-unsupported-language`:`ran`:`partial-no-cross-file-resolution`}async function i(t,n,r,i){let a=async e=>e==null?new Map:i.parser.analyze(new Map([[t,e]]),i.signal),s=await a(n.ours),l=await a(n.theirs),u=await i.parser.analyze(new Map([[t,r]]),i.signal),d=[...o(s,l,u).filter(e=>e.tier===`file`),...c(u,[s,l])],f=[];if(i.validator){let n=await i.validator.validate(new Map([[t,r]]),i.signal);for(let t of n)f.push(e(t))}return{parser:d,validator:f}}function a(e){let t=new Set;for(let n of e.definitions)t.add(n.name);for(let n of e.bindings??[])t.add(n.name);return t}function o(e,t,n){let r=s(e,t),i=[];for(let[e,t]of n){let n=a(t);for(let a of t.references){if(a.resolved||n.has(a.name))continue;let t=r.get(p(e,a.name));t&&i.push({category:`broken-reference`,severity:`error`,tier:t.crossFile?`project`:`file`,location:{file:e,line:a.line,symbol:a.name},provenance:{side:t.side,line:t.line},message:`Reference "${a.name}" resolved before the merge (on ${t.side}) but no longer resolves.`})}}return i}function s(e,t){let n=new Map,r=(e,t,r,i,a)=>{let o=p(t,r);n.has(o)||n.set(o,{side:e,line:i,crossFile:a})},i=[[`ours`,e],[`theirs`,t]];for(let[e,t]of i)for(let[n,i]of t){for(let t of i.definitions)r(e,n,t.name,t.line,!1);for(let t of i.bindings??[])r(e,n,t.name,t.line,!1)}for(let[e,t]of i)for(let[n,i]of t)for(let t of i.references)t.resolved&&r(e,n,t.name,t.line,t.target?t.target.file!==n:!0);return n}function c(e,t){let n=l(t),r=new Set;for(let e of t)for(let t of e.keys())r.add(t);let i=[];for(let[t,a]of e){let e=r.has(t),o=new Map;for(let r of a.definitions){let a=o.get(r.name);if(a==null){o.set(r.name,r.line);continue}!e||n.has(p(t,r.name))||i.push({category:`duplicate-definition`,severity:`warning`,tier:`file`,location:{file:t,line:r.line,symbol:r.name},message:`Duplicate definition of "${r.name}" (also defined at line ${a}).`})}for(let e of a.syntaxErrors)i.push({category:`syntax-error`,severity:`error`,tier:`file`,location:{file:t,line:e.line},message:e.message})}return i}function l(e){let t=new Set;for(let n of e)for(let[e,r]of n){let n=new Set;for(let i of r.definitions){let r=p(e,i.name);n.has(r)&&t.add(r),n.add(r)}}return t}function u(e,t){let n=[];for(let r of e){if(!r.fallback||r.strategy!==`take-ours`&&r.strategy!==`take-theirs`)continue;let e=r.strategy===`take-ours`?`theirs`:`ours`,i=(e===`ours`?t.ours:t.theirs).get(r.filePath);if(i==null)continue;let a=d(i,r.content);a<20||n.push({category:`dropped-side`,severity:`error`,tier:`file`,location:{file:r.filePath,line:1},message:`Resolution took "${r.strategy}" wholesale, discarding ~${a} line(s) of the ${e} side.`})}return n}function d(e,t){let n=new Set(t.split(`
|
|
2
2
|
`)),r=0;for(let t of e.split(`
|
|
3
|
-
`))t.trim()!==``&&!n.has(t)&&r++;return r}function
|
|
3
|
+
`))t.trim()!==``&&!n.has(t)&&r++;return r}function f(e){for(let t of e.values())if(!t.supported)return!0;return!1}function p(e,t){return`${e}\u0000${t}`}export{i as collectFileFindings,n as detect};
|
|
@@ -30,8 +30,8 @@ interface SnapshotDeps {
|
|
|
30
30
|
* objects — no working tree needed) and the resolved result (in-memory resolutions overlaid on the
|
|
31
31
|
* surrounding files). Scope is the resolved files plus the merge's incoming/auto-merged set, which
|
|
32
32
|
* the detector discovers itself via the git port (`changedFiles(base, theirs)`), unless the consumer
|
|
33
|
-
* supplies `context.touchedFiles` as an override. Returns `null` when `context.
|
|
34
|
-
* before/after comparison is impossible without the parent refs.
|
|
33
|
+
* supplies `context.touchedFiles` as an override. Returns `null` when neither `context.detectionRefs`
|
|
34
|
+
* nor `context.refs` is present — the before/after comparison is impossible without the parent refs.
|
|
35
35
|
*/
|
|
36
36
|
declare function buildSnapshots(resolutions: Resolution[], context: ResolutionContext, deps: SnapshotDeps): Promise<DetectionSnapshots | null>;
|
|
37
37
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{changedFiles as e,mergeBase as t,readFile as n,showFile as r}from"../git/port-dispatch.js";async function i(e,t,n){let r=t.detectionRefs??t.refs;if(!r)return null;let i=n.signal?{signal:n.signal}:void 0,c=new Map(e.map(e=>[e.filePath,e.content])),
|
|
1
|
+
import{changedFiles as e,mergeBase as t,readFile as n,showFile as r}from"../git/port-dispatch.js";async function i(e,t,n){let r=t.detectionRefs??t.refs;if(!r)return null;let i=n.signal?{signal:n.signal}:void 0,c={...i,fullContent:!0},l=new Map(e.map(e=>[e.filePath,e.content])),u=t.touchedFiles?{files:t.touchedFiles,discovered:!0}:await a(n.git,r,i),d=n.excludePaths,f=[...new Set([...l.keys(),...u.files,...t.fileNeighbors??[]])].filter(e=>!d?.has(e)),p=f.length>400,m=p?f.slice(0,400):f,h=new Map,g=new Map,_=new Map,v=0;for(let e of m){let t=await o(n.git,r.ours,e,c);t&&h.set(e,t);let a=await o(n.git,r.theirs,e,c);a&&g.set(e,a),(t||a)&&v++;let u=l.get(e);if(u!=null)_.set(e,u);else{let t=await s(n.git,e,i);t!=null&&_.set(e,t)}}return{ours:h,theirs:g,result:_,inScope:m.length,readable:v,truncated:p,incomingDiscovered:u.discovered}}async function a(n,r,i){try{return{files:await e(n,r.base??await t(n,r.ours,r.theirs,i),r.theirs,i),discovered:!0}}catch(e){return c(e,i),{files:[],discovered:!1}}}async function o(e,t,n,i){try{return await r(e,t,n,i)}catch(e){return c(e,i),null}}async function s(e,t,r){try{return await n(e,t,r)}catch(e){return c(e,r),null}}function c(e,t){if(t?.signal?.aborted)throw e}export{i as buildSnapshots};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Conflict } from "../types/conflict.js";
|
|
2
|
+
|
|
3
|
+
//#region src/resolution/detect-code-loss.d.ts
|
|
4
|
+
interface CodeLossDetection {
|
|
5
|
+
/** The side thrown away by the wholesale fallback. */
|
|
6
|
+
discardedSide: 'ours' | 'theirs';
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Reports whether a wholesale `take-ours`/`take-theirs` fallback would discard already-integrated
|
|
10
|
+
* work. On rebase "ours" is everything resolved so far, so taking the other side wholesale can wipe
|
|
11
|
+
* it (AGP-430).
|
|
12
|
+
*
|
|
13
|
+
* Any text conflict counts as loss. The fallback is applied with `git checkout --ours/--theirs`,
|
|
14
|
+
* which swaps the whole file for one stage, so whatever git auto-merged outside the conflict markers
|
|
15
|
+
* is discarded with it. Comparing marker regions cannot rule that out: it answers a narrower question
|
|
16
|
+
* than the write asks.
|
|
17
|
+
*
|
|
18
|
+
* Returns `null` only for the delete-modify direction that keeps content, the one case where the
|
|
19
|
+
* fallback is safe by construction.
|
|
20
|
+
*/
|
|
21
|
+
declare function detectFallbackCodeLoss(conflict: Conflict, strategy: 'take-ours' | 'take-theirs'): CodeLossDetection | null;
|
|
22
|
+
//#endregion
|
|
23
|
+
export { CodeLossDetection, detectFallbackCodeLoss };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function e(e,t){let n=t===`take-theirs`?`ours`:`theirs`;return e.type===`delete-modify`?t===`take-theirs`&&e.deletedBy===`theirs`||t===`take-ours`&&e.deletedBy===`ours`?{discardedSide:n}:null:e.markers.length>0?{discardedSide:n}:null}export{e as detectFallbackCodeLoss};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{checkResidualMarkers as e}from"../ports/verify.js";import{buildSnapshots as t}from"../reference-integrity/snapshot.js";import{collectFileFindings as n,detect as r}from"../reference-integrity/detect.js";import{runRefinePass as i}from"./refine-pass.js";import{CONFIDENCE_CAPS as a}from"./resolve-conflict.js";async function o(t,r,o){let d=o.config?.fixLoopMaxIterations??3,
|
|
2
|
-
`)}async function
|
|
1
|
+
import{checkResidualMarkers as e}from"../ports/verify.js";import{buildSnapshots as t}from"../reference-integrity/snapshot.js";import{collectFileFindings as n,detect as r}from"../reference-integrity/detect.js";import{runRefinePass as i}from"./refine-pass.js";import{CONFIDENCE_CAPS as a}from"./resolve-conflict.js";async function o(t,r,o){let d=o.config?.fixLoopMaxIterations??3,f={parser:o.parser,validator:o.validator,signal:o.signal},p={content:t.content,findings:await(e=>n(t.filePath,r,e,f))(t.content)},g=s(p.findings);if(g.length===0)return l(o,t.filePath,0,0),c(t,p.findings.validator);if(o.budget?.exhausted())return o.onBudgetExhausted?.(),l(o,t.filePath,0,g.length),c(t,p.findings.validator);o.onProgress?.({type:`fix:started`,filePath:t.filePath,findingCount:g.length});let _=[],v=0,y=m(g);for(;v<d&&g.length>0;){if(o.budget?.exhausted()){o.onBudgetExhausted?.();break}v++;let a=await i(p.content,t.description,u(g),{model:o.model,signal:o.signal,temperature:o.config?.temperature,refineMaxSteps:o.config?.refineMaxSteps,onProgress:o.onProgress,filePath:t.filePath,budget:o.budget});if(a.edits.length===0)break;if(!e(t.filePath,a.content).valid){o.onProgress?.({type:`refine:discarded`,filePath:t.filePath,reason:`Fix edits introduced conflict markers.`});break}let c=await n(t.filePath,r,a.content,f),l=s(c);if(h(g,l)){o.onProgress?.({type:`refine:discarded`,filePath:t.filePath,reason:`Fix edits introduced a new error (was ${g.length}, now ${l.length}).`});break}p={content:a.content,findings:c},_.push(...a.edits),g=l;let d=m(g);if(d===y)break;y=d}l(o,t.filePath,v,g.length);let b=c({...t,content:p.content},p.findings.validator);return _.length===0?b:{...b,strategy:`ai`,edits:[...t.edits??[],..._],confidence:Math.min(b.confidence,a.refineApplied)}}function s(e){return[...e.parser,...e.validator].filter(e=>e.severity===`error`)}function c(e,t){if(t.length===0){if(!e.findings)return e;let{findings:t,...n}=e;return n}return{...e,findings:t}}function l(e,t,n,r){e.onProgress?.({type:`fix:completed`,filePath:t,iterations:n,remainingFindings:r})}function u(e){return e.slice(0,20).map(d)}function d(e){let t=e.location.symbol?f(e.location.symbol):void 0,n=t?`"${t}" (line ${e.location.line})`:`line ${e.location.line}`;return`${f(e.category)} at ${n}: ${f(e.message)}`}function f(e){return e.replace(/[\p{Cc}\p{Cf}]/gu,` `).replace(/\s+/g,` `).trim().slice(0,300)}function p(e){return JSON.stringify([e.category,e.location.file,e.location.symbol??``])}function m(e){return e.map(p).sort().join(`
|
|
2
|
+
`)}function h(e,t){let n=new Set(e.map(p));return t.some(e=>!n.has(p(e)))}async function g(e,n,i){let a={git:i.git,parser:i.parser,signal:i.signal,excludePaths:i.excludePaths},o=await t(e,n,{git:i.git,signal:i.signal,excludePaths:i.excludePaths}),s=()=>i.onProgress?.({type:`fix:budget-exhausted`,spent:i.budget?.spent()??0,limit:i.budget?.limit()??0}),c={model:i.model,parser:i.parser,validator:i.validator,config:i.config,onProgress:i.onProgress,signal:i.signal,budget:i.budget,onBudgetExhausted:()=>i.budget?.notifyExhaustedOnce(s)};return await _(e,o,c),o?v(e,n,o,a,c,i):r(e,n,a)}async function _(e,t,n){for(let r=0;r<e.length;r++){let i=e[r];if(i.strategy===`ai`){if(n.signal?.aborted)return;try{e[r]=await o(i,y(t,i.filePath),n)}catch(e){if(n.signal?.aborted)throw e;n.onProgress?.({type:`fix:failed`,filePath:i.filePath,reason:e instanceof Error?e.message:String(e)})}}}}async function v(e,t,n,i,a,o){let s=o.config?.crossFileMaxIterations??2,c=await r(e,t,i);for(let l=0;l<s&&!o.signal?.aborted;l++){if(o.budget?.exhausted()){a.onBudgetExhausted?.();break}let s=x(c.findings,e);if(s.size===0)break;let l=!1;for(let[t,r]of s){if(o.budget?.exhausted()){a.onBudgetExhausted?.();break}let i=e.findIndex(e=>e.filePath===t);if(i<0)continue;let s=e[i],c=s.content;try{let o=await b(s,r,y(n,t),a);e[i]=o,o.content!==c&&(l=!0)}catch(e){if(o.signal?.aborted)throw e;o.onProgress?.({type:`fix:failed`,filePath:t,reason:e instanceof Error?e.message:String(e)})}}if(!l)break;c=await r(e,t,i)}return c}function y(e,t){return{ours:e?.ours.get(t),theirs:e?.theirs.get(t)}}async function b(t,r,o,l){let d={parser:l.parser,validator:l.validator,signal:l.signal},f=s(await n(t.filePath,o,t.content,d)),p=await i(t.content,t.description,u(r),{model:l.model,signal:l.signal,temperature:l.config?.temperature,refineMaxSteps:l.config?.refineMaxSteps,onProgress:l.onProgress,filePath:t.filePath,budget:l.budget});if(p.edits.length===0)return t;if(!e(t.filePath,p.content).valid)return l.onProgress?.({type:`refine:discarded`,filePath:t.filePath,reason:`Cross-file fix introduced conflict markers.`}),t;let m=await n(t.filePath,o,p.content,d);return h(f,s(m))?(l.onProgress?.({type:`refine:discarded`,filePath:t.filePath,reason:`Cross-file fix introduced a new error in the file.`}),t):c({...t,content:p.content,strategy:`ai`,edits:[...t.edits??[],...p.edits],confidence:Math.min(t.confidence,a.refineApplied)},m.validator)}function x(e,t){let n=new Set(t.filter(e=>e.strategy===`ai`).map(e=>e.filePath)),r=new Map;for(let t of e){if(t.tier!==`project`||t.severity!==`error`||!n.has(t.location.file))continue;let e=r.get(t.location.file)??[];e.push(t),r.set(t.location.file,e)}return r}export{o as runFixLoop,g as runFixPhase};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
function e(e){let t=0,n=!1,
|
|
1
|
+
function e(e){let t=Number.isFinite(e)?e:void 0,n=0,r=!1,i=()=>t===void 0?1/0:t-n;return{wrap(e){return{generate:t=>(n++,e.generate(t))}},spent:()=>n,limit:()=>t??0,remaining:i,exhausted:()=>i()<=0,notifyExhaustedOnce(e){r||(r=!0,e())}}}export{e as createModelBudget};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{AIError as e}from"../errors/ai.js";import{
|
|
1
|
+
import{AIError as e}from"../errors/ai.js";import{ConflictError as t}from"../errors/conflict.js";import{unmergedEntries as n}from"../git/port-dispatch.js";import{extractConflict as r}from"../extraction/extract.js";import{detect as i}from"../reference-integrity/detect.js";import{createResolution as a}from"../resolution/create-resolution.js";import{detectFallbackCodeLoss as o}from"../resolution/detect-code-loss.js";import{takeOurs as s,takeTheirs as c}from"../resolution/take-strategy.js";import{resolveConflict as l}from"./resolve-conflict.js";import{runFixPhase as u}from"./fix-loop.js";import{createModelBudget as d}from"./model-budget.js";import f from"picomatch";function p(e){let t=e.map(e=>{let t=(Array.isArray(e.match)?e.match:[e.match]).map(e=>f(e,{basename:!e.includes(`/`)}));return{isMatch:e=>t.some(t=>t(e)),strategy:e.strategy}});return e=>{for(let{isMatch:n,strategy:r}of t)if(n(e))return r}}async function m(e,t){let o=e.config?.rules??[],l=e.config?.defaultStrategy??`ai`,f=p(o),m=await n(e.git,{signal:e.signal}),v=d(e.config?.maxModelCalls),y=v.wrap(e.model),b={...t??{}},x=[],S=[],C=[];for(let t of m){if(e.signal?.aborted)break;let n=f(t.path)??l;if(n===`skip`){C.push({filePath:t.path,reason:`excluded`}),e.onProgress?.({type:`conflict:excluded`,filePath:t.path});continue}let i;try{i=await r(t.path,{git:e.git,signal:e.signal},t.reason)}catch(e){S.push({filePath:t.path,error:g(e)});continue}if(!i){C.push({filePath:t.path,reason:`no-markers`}),e.onProgress?.({type:`conflict:skipped`,filePath:t.path,reason:`no-markers`,entryReason:t.reason});continue}e.onProgress?.({type:`conflict:found`,filePath:i.filePath,conflictType:i.type,markerCount:i.markers.length});let o=null;o=n===`take-ours`?s(i):n===`take-theirs`?c(i):n===`deleted`?a(i,[],{confidence:1,fileStrategy:`deleted`}):await h(i,b,{...e,model:y},S),o&&(b.previousResolutions=[...b.previousResolutions??[],o],x.push(o),e.onProgress?.({type:`resolution:applied`,filePath:o.filePath,strategy:o.strategy,confidence:o.confidence,description:o.description}))}let w=new Set([...C.map(e=>e.filePath),...S.map(e=>e.filePath)]),T={git:e.git,parser:e.parser,signal:e.signal,excludePaths:w},E;if(e.config?.fix&&e.parser&&!e.signal?.aborted)try{E=await u(x,b,{model:y,git:e.git,parser:e.parser,validator:e.validator,config:e.config,onProgress:e.onProgress,signal:e.signal,budget:v,excludePaths:w})}catch(t){if(e.signal?.aborted)throw t;e.onProgress?.({type:`fix:failed`,reason:g(t).message}),E=await i(x,b,T),E={...E,checks:{referenceIntegrity:`errored`,structural:`errored`}}}else E=await i(x,b,T);let D=x.flatMap(e=>e.findings??[]);return{resolutions:x,errors:S,skipped:C,detection:{...E,findings:_([...E.findings,...D])}}}async function h(n,r,i,a){try{return await l(n,r,{model:i.model,git:i.git,config:i.config,onProgress:i.onProgress,signal:i.signal,validator:i.validator})}catch(r){let l=g(r);if(i.signal?.aborted)throw l;let u=i.config?.fallbackStrategy;if(!u)return a.push({filePath:n.filePath,error:l}),i.onProgress?.({type:`resolution:failed`,filePath:n.filePath,error:l}),null;let d=i.config?.allowLossyFallback?null:o(n,u);if(d){let e=new t(`CODE_LOSS_RISK`,`AI resolution failed and the '${u}' fallback would replace ${n.filePath} wholesale, discarding the ${d.discardedSide} side and anything git auto-merged outside the conflict markers. Stopping for manual resolution.`,{cause:l});return a.push({filePath:n.filePath,error:e}),i.onProgress?.({type:`resolution:failed`,filePath:n.filePath,error:e}),null}return i.onProgress?.({type:`resolution:fallback`,filePath:n.filePath,error:l,cause:l instanceof e?l.code:void 0}),{...u===`take-ours`?s(n):c(n),confidence:0,fallback:!0}}}function g(e){return e instanceof Error?e:Error(String(e))}function _(e){let t=new Set,n=[];for(let r of e){let{category:e,location:i}=r,a=`${e}\u0000${i.file}\u0000${i.line}\u0000${i.symbol??``}`;t.has(a)||(t.add(a),n.push(r))}return n}export{m as resolveConflicts};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{buildSummaryPrompt as e}from"./prompt.js";const t=.8;function n(e,n=t){let s={ai:0,"take-ours":0,"take-theirs":0,deleted:0,skipped:0},c=0,l=null,u=new Map;for(let t of e.resolutions)s[t.strategy]++,c+=t.chunks?.length??0,l=l===null?t.confidence:Math.min(l,t.confidence),t.confidence<n&&u.set(t.filePath,{filePath:t.filePath,confidence:t.confidence,reason:`low-confidence`});let d=e.detection,f=d?.findings??[],p=new Map,m=!1;for(let e of f){e.severity===`error`&&(m=!0);let t=p.get(e.location.file)??new Set;t.add(e.category),p.set(e.location.file,t)}for(let[e,t]of p){let n=u.get(e);u.set(e,{filePath:e,...n?.confidence==null?{}:{confidence:n.confidence},reason:`finding`,findingCategories:[...t].sort()})}let h=[...u.values()].sort(a),g=d!=null&&r(d.checks.referenceIntegrity)&&r(d.checks.structural)&&i(d.scope),_=o({hasErrorFinding:m,hasWarningFinding:f.some(e=>e.severity===`warning`),needsReview:h.length>0,detectionComplete:g,hasResolutionErrors:e.errors.length>0});return{files:e.resolutions.length,conflicts:c,byStrategy:s,minConfidence:l,needsReview:h,skipped:e.skipped?.length??0,errors:e.errors.length,detectionComplete:g,findingCount:f.length,verdict:_}}function r(e){return e===`ran`||e===`partial-unsupported-language`}function i(e){return e?!e.truncated&&e.incomingDiscovered:!0}function a(e,t){return e.reason===t.reason?(e.confidence??1)-(t.confidence??1):e.reason===`finding`?-1:1}function o(e){return e.hasErrorFinding?`blocked`:e.needsReview||!e.detectionComplete||e.hasResolutionErrors||e.hasWarningFinding?`needs-review`:`clean`}async function s(r,i={}){let a=n(r,i.reviewThreshold??t);if(!i.model)return{digest:a};let{system:o,userMessage:s}=e(r,a),c=await i.model.generate({system:o,messages:[{role:`user`,content:s}],temperature:i.temperature??0,signal:i.signal}),l=c.text?.trim();return{digest:a,...l?{prose:l}:{},...c.usage?{usage:c.usage}:{}}}export{n as buildOperationDigest,s as summarizeOperation};
|
|
1
|
+
import{buildSummaryPrompt as e}from"./prompt.js";const t=.8;function n(e,n=t){let s={ai:0,"take-ours":0,"take-theirs":0,deleted:0,skipped:0},c=0,l=null,u=new Map;for(let t of e.resolutions)s[t.strategy]++,c+=t.chunks?.length??0,l=l===null?t.confidence:Math.min(l,t.confidence),t.confidence<n&&u.set(t.filePath,{filePath:t.filePath,confidence:t.confidence,reason:`low-confidence`});let d=e.detection,f=d?.findings??[],p=new Map,m=!1;for(let e of f){e.severity===`error`&&(m=!0);let t=p.get(e.location.file)??new Set;t.add(e.category),p.set(e.location.file,t)}for(let[e,t]of p){let n=u.get(e);u.set(e,{filePath:e,...n?.confidence==null?{}:{confidence:n.confidence},reason:`finding`,findingCategories:[...t].sort()})}for(let t of e.skipped??[])t.reason!==`excluded`&&(u.has(t.filePath)||u.set(t.filePath,{filePath:t.filePath,reason:`unresolved`}));let h=[...u.values()].sort(a),g=d!=null&&r(d.checks.referenceIntegrity)&&r(d.checks.structural)&&i(d.scope),_=o({hasErrorFinding:m,hasWarningFinding:f.some(e=>e.severity===`warning`),needsReview:h.length>0,detectionComplete:g,hasResolutionErrors:e.errors.length>0});return{files:e.resolutions.length,conflicts:c,byStrategy:s,minConfidence:l,needsReview:h,skipped:e.skipped?.length??0,errors:e.errors.length,detectionComplete:g,findingCount:f.length,verdict:_}}function r(e){return e===`ran`||e===`partial-unsupported-language`}function i(e){return e?!e.truncated&&e.incomingDiscovered:!0}function a(e,t){return e.reason===t.reason?(e.confidence??1)-(t.confidence??1):e.reason===`finding`?-1:1}function o(e){return e.hasErrorFinding?`blocked`:e.needsReview||!e.detectionComplete||e.hasResolutionErrors||e.hasWarningFinding?`needs-review`:`clean`}async function s(r,i={}){let a=n(r,i.reviewThreshold??t);if(!i.model)return{digest:a};let{system:o,userMessage:s}=e(r,a),c=await i.model.generate({system:o,messages:[{role:`user`,content:s}],temperature:i.temperature??0,signal:i.signal}),l=c.text?.trim();return{digest:a,...l?{prose:l}:{},...c.usage?{usage:c.usage}:{}}}export{n as buildOperationDigest,s as summarizeOperation};
|
package/dist/types/step.d.ts
CHANGED
|
@@ -23,6 +23,9 @@ interface StepConfig extends ResolverConfig {
|
|
|
23
23
|
defaultStrategy?: 'ai' | 'take-ours' | 'take-theirs' | 'skip' | 'deleted';
|
|
24
24
|
rules?: FileRule[];
|
|
25
25
|
fallbackStrategy?: 'take-ours' | 'take-theirs';
|
|
26
|
+
/** Escape hatch: apply the wholesale `fallbackStrategy` even when it would discard already-integrated
|
|
27
|
+
* work. Off by default so a lossy fallback stops for manual resolution instead of completing silently. */
|
|
28
|
+
allowLossyFallback?: boolean;
|
|
26
29
|
/** Opt in to the fix phase (default `false` → report-only). When enabled, an AI-resolved file carrying
|
|
27
30
|
* error-severity per-file findings is routed back through refine to attempt a fix; what it cannot fix
|
|
28
31
|
* remains in the findings. Requires `ResolverDeps.parser` — without a parser the fix phase is a no-op
|
package/dist/types/summary.d.ts
CHANGED
|
@@ -2,13 +2,16 @@ import { ResolutionStrategy } from "./resolution.js";
|
|
|
2
2
|
import { AiTokenUsage } from "@gitkraken/shared-tools";
|
|
3
3
|
|
|
4
4
|
//#region src/types/summary.d.ts
|
|
5
|
-
/** A file flagged for human review. `reason` distinguishes a low file-confidence flag
|
|
6
|
-
*
|
|
7
|
-
* auto-merged file with no resolution of its own). */
|
|
5
|
+
/** A file flagged for human review. `reason` distinguishes a low file-confidence flag, a detection
|
|
6
|
+
* finding, and a file the tool left unmerged. `confidence` is absent for files flagged purely by a
|
|
7
|
+
* finding (e.g. an auto-merged file with no resolution of its own) or as unresolved. */
|
|
8
8
|
interface ReviewItem {
|
|
9
9
|
filePath: string;
|
|
10
10
|
confidence?: number;
|
|
11
|
-
|
|
11
|
+
/** `unresolved` — an unmerged entry the tool did not resolve (no conflict markers to act on: a binary
|
|
12
|
+
* add/add, an unparseable delete-modify). It is still unmerged on disk, so the operation is not
|
|
13
|
+
* clean; a `skip`-rule exclusion is a deliberate human choice and is not flagged. */
|
|
14
|
+
reason: 'low-confidence' | 'finding' | 'unresolved';
|
|
12
15
|
/** Categories of the findings on this file, of any severity (error and warning alike); present when
|
|
13
16
|
* `reason` is `'finding'`. */
|
|
14
17
|
findingCategories?: string[];
|