@gitkraken/conflict-tools 0.2.4-beta.10 → 0.2.4-beta.12
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 +3 -0
- package/dist/ports/model.d.ts +6 -0
- package/dist/reference-integrity/detect.d.ts +17 -2
- package/dist/reference-integrity/detect.js +2 -2
- package/dist/resolver/fix-loop.d.ts +28 -0
- package/dist/resolver/fix-loop.js +0 -0
- package/dist/resolver/resolve-conflict.js +1 -1
- package/dist/resolver/resolve-conflicts.js +0 -0
- package/dist/types/events.d.ts +9 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
9
9
|
|
|
10
10
|
### Added
|
|
11
11
|
|
|
12
|
+
- Per-file fix loop (opt-in via `ResolverConfig.fix`, default `false`). When enabled, an AI-resolved (`strategy: 'ai'`) text file that carries error-severity per-file findings is routed back through refine: the findings become fix instructions, the file is re-analyzed after each edit, and the loop repeats until the file is clean, a round makes no progress, or `fixLoopMaxIterations` (default 3) is reached. Whatever it cannot fix stays in the findings, so the verdict reflects the post-fix state. The no-markers invariant holds (a fix that reintroduces markers is discarded). New progress events `fix:started` / `fix:completed`. Report-only (fix off) is unchanged. Deferred for the opt-in fix path: rebase per-step-ref/transient discipline, parser/validator call caching, cross-file fixes, and a full finding-lifecycle model (double-counting is handled here by fingerprint dedup when folding findings into the report).
|
|
13
|
+
- `collectFileFindings` — a per-file detection unit (structural + intra-file broken references + consumer validator issues, all `tier: 'file'`) over already-loaded parent snapshots; the fix loop re-runs it after each edit. `detect()` remains the authoritative whole-set report.
|
|
12
14
|
- Reference-integrity detection. `resolveConflicts` now attaches a `StepResult.detection` report listing broken references (symbols that resolved in a pre-merge parent but no longer resolve in the merged result, with provenance), duplicate definitions, and syntax errors in the resolved files. Detection is opt-in via a new `ParserPort` (`ResolverDeps.parser`) that supplies parsing/symbol resolution per surface (tree-sitter, language server); when it is absent, unavailable, or a file's language is unsupported, the report says so rather than implying a clean result. Report-only in this release — no automatic fixing.
|
|
13
15
|
- Auto-merged scope for reference-integrity detection: the detector discovers the merge's incoming/auto-merged set itself (new optional `changedFiles` git-port op; `git diff --name-only base..theirs`, base from `refs.base` or `mergeBase`) and includes it in scope, so dangling references in cleanly-merged files are caught. Scope is capped and reported via `DetectionReport.scope`; an optional `ResolutionContext.touchedFiles` overrides discovery. Adds a `dropped-side` finding when a whole-side take (take-ours/take-theirs/fallback) discards a substantial diff from the other side.
|
|
14
16
|
- `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.
|
|
@@ -23,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
23
25
|
|
|
24
26
|
### Fixed
|
|
25
27
|
|
|
28
|
+
- 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).
|
|
26
29
|
- A throwing consumer validator no longer destroys a good resolution. The post-refine `validator.validate()` call is now guarded: a validator error is reported via a new `validation:failed` progress event and the resolution is kept intact, instead of bubbling to `resolveOne` and degrading a successful AI resolve into a whole-file take-side fallback (confidence 0) — the exact dropped-side loss the tool guards against. The validator stays advisory.
|
|
27
30
|
- A user abort mid-resolve now cancels the operation instead of degrading into a take-side fallback. `resolveOne` rethrows when `signal.aborted` (covering aborts raised from the resolve loop, refine, or the validator), bypassing the emergency fallback rather than turning a cancellation into a whole-file take-one-side resolution.
|
|
28
31
|
- The refine pass no longer floods its bounded step budget when the model spirals. After a run of consecutive `edit_file_lines` apply errors (a model repeatedly proposing ops that never apply), the pass bails early — keeping whatever edits landed — and emits a new `refine:bailed` progress event, instead of executing failing tool calls until the step budget is exhausted.
|
package/dist/ports/model.d.ts
CHANGED
|
@@ -35,6 +35,12 @@ interface ResolverConfig {
|
|
|
35
35
|
maxSteps?: number;
|
|
36
36
|
temperature?: number;
|
|
37
37
|
refineMaxSteps?: number;
|
|
38
|
+
/** Opt in to the per-file fix loop (default `false` → report-only). When enabled, an AI-resolved file
|
|
39
|
+
* carrying error-severity per-file findings is routed back through refine to attempt a fix; what it
|
|
40
|
+
* cannot fix remains in the findings. Off by default so the report-only path is unchanged. */
|
|
41
|
+
fix?: boolean;
|
|
42
|
+
/** Max per-file fix iterations (refine → re-detect the file) when `fix` is on. Default 3. */
|
|
43
|
+
fixLoopMaxIterations?: number;
|
|
38
44
|
}
|
|
39
45
|
//#endregion
|
|
40
46
|
export { ConflictModelMessage, ConflictModelParams, ConflictModelPort, ConflictModelResult, ResolverConfig };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { ConflictGitPort } from "../ports/git.js";
|
|
2
|
-
import { DetectionReport } from "../types/findings.js";
|
|
2
|
+
import { DetectionReport, Finding } from "../types/findings.js";
|
|
3
3
|
import { Resolution, ResolutionContext } from "../types/resolution.js";
|
|
4
4
|
import { ParserPort } from "../ports/parser.js";
|
|
5
|
+
import { Validator } from "../ports/verify.js";
|
|
5
6
|
|
|
6
7
|
//#region src/reference-integrity/detect.d.ts
|
|
7
8
|
interface DetectDeps {
|
|
@@ -17,5 +18,19 @@ interface DetectDeps {
|
|
|
17
18
|
* parser is supplied, or refs are missing, the report says so instead of implying a clean result.
|
|
18
19
|
*/
|
|
19
20
|
declare function detect(resolutions: Resolution[], context: ResolutionContext, deps: DetectDeps): Promise<DetectionReport>;
|
|
21
|
+
interface FileFindingsDeps {
|
|
22
|
+
parser: ParserPort;
|
|
23
|
+
validator?: Validator;
|
|
24
|
+
signal?: AbortSignal;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Per-file findings for one file's current content, compared against its pre-merge parents: structural
|
|
28
|
+
* (syntax/duplicate) + intra-file broken references (a local definition/binding the merge dropped while
|
|
29
|
+
* a use of it remains in the same file) + the consumer validator's per-file issues. All `tier: 'file'`.
|
|
30
|
+
* Cross-file references need the full set and are not computed here — `detect()` remains the
|
|
31
|
+
* authoritative whole-set report. This is the lightweight unit the per-file fix loop re-runs after each
|
|
32
|
+
* edit, over already-loaded parent snapshots (no git reads, no incoming-set discovery).
|
|
33
|
+
*/
|
|
34
|
+
declare function collectFileFindings(file: string, oursContent: string | undefined, theirsContent: string | undefined, resultContent: string, deps: FileFindingsDeps): Promise<Finding[]>;
|
|
20
35
|
//#endregion
|
|
21
|
-
export { DetectDeps, detect };
|
|
36
|
+
export { DetectDeps, FileFindingsDeps, collectFileFindings, detect };
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{buildSnapshots as e}from"./snapshot.js";async function t(t,
|
|
1
|
+
import{buildSnapshots as e}from"./snapshot.js";async function t(t,n,i){let s=i.parser;if(!s)return{findings:[],checks:{referenceIntegrity:`skipped-no-parser`,structural:`skipped-no-parser`}};try{let l=n.detectionRefs??n.refs?await e(t,n,i):null;if(l){let[e,n,i]=await Promise.all([s.analyze(l.ours),s.analyze(l.theirs),s.analyze(l.result)]),u=l.analyzed>0&&l.readable===0?`skipped-unreadable-source`:c([e,n,i])?`partial-unsupported-language`:`ran`;return{findings:[...r(e,n,i),...a(i),...o(t,l)],checks:{referenceIntegrity:u,structural:c([i])?`partial-unsupported-language`:`ran`},scope:{analyzed:l.analyzed,readable:l.readable,truncated:l.truncated}}}let u=new Map(t.map(e=>[e.filePath,e.content])),d=await s.analyze(u);return{findings:a(d),checks:{referenceIntegrity:`skipped-no-refs`,structural:c([d])?`partial-unsupported-language`:`ran`}}}catch{return{findings:[],checks:{referenceIntegrity:`errored`,structural:`errored`}}}}async function n(e,t,n,i,o){let s=async t=>t==null?new Map:o.parser.analyze(new Map([[e,t]])),c=await s(t),l=await s(n),u=await o.parser.analyze(new Map([[e,i]])),d=[...r(c,l,u).filter(e=>e.tier===`file`),...a(u)];if(o.validator){let t=await o.validator.validate(new Map([[e,i]]));for(let e of t)d.push({category:e.category,severity:e.severity,tier:`file`,location:{file:e.file,line:e.line??1},message:e.message})}return d}function r(e,t,n){let r=new Map;i(e,`ours`,r),i(t,`theirs`,r);let a=[];for(let[e,t]of n)for(let n of t.references){if(n.resolved)continue;let t=r.get(l(e,n.name));t&&a.push({category:`broken-reference`,severity:`error`,tier:t.crossFile?`project`:`file`,location:{file:e,line:n.line,symbol:n.name},provenance:{side:t.side,line:t.line},message:`Reference "${n.name}" resolved before the merge (on ${t.side}) but no longer resolves.`})}return a}function i(e,t,n){let r=(e,r,i,a)=>{let o=l(e,r);n.has(o)||n.set(o,{side:t,line:i,crossFile:a})};for(let[t,n]of e){for(let e of n.definitions)r(t,e.name,e.line,!1);for(let e of n.bindings??[])r(t,e.name,e.line,!1)}for(let[t,n]of e)for(let e of n.references)e.resolved&&r(t,e.name,e.line,e.target?e.target.file!==t:!0)}function a(e){let t=[];for(let[n,r]of e){let e=new Map;for(let i of r.definitions){let r=e.get(i.name);r==null?e.set(i.name,i.line):t.push({category:`duplicate-definition`,severity:`error`,tier:`file`,location:{file:n,line:i.line,symbol:i.name},message:`Duplicate definition of "${i.name}" (also defined at line ${r}).`})}for(let e of r.syntaxErrors)t.push({category:`syntax-error`,severity:`error`,tier:`file`,location:{file:n,line:e.line},message:e.message})}return t}function o(e,t){let n=[];for(let r of e){let e=r.strategy===`take-ours`?`theirs`:r.strategy===`take-theirs`?`ours`:null;if(!e)continue;let i=(e===`ours`?t.ours:t.theirs).get(r.filePath);if(i==null)continue;let a=s(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 s(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 c(e){return e.some(e=>{for(let t of e.values())if(t.language===`unsupported`)return!0;return!1})}function l(e,t){return JSON.stringify([e,t])}export{n as collectFileFindings,t as detect};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Resolution } from "../types/resolution.js";
|
|
2
|
+
import { ConflictModelPort, ResolverConfig } from "../ports/model.js";
|
|
3
|
+
import { ParserPort } from "../ports/parser.js";
|
|
4
|
+
import { Validator } from "../ports/verify.js";
|
|
5
|
+
import { ConflictProgressEvent } from "../types/events.js";
|
|
6
|
+
|
|
7
|
+
//#region src/resolver/fix-loop.d.ts
|
|
8
|
+
interface FixLoopDeps {
|
|
9
|
+
model: ConflictModelPort;
|
|
10
|
+
parser: ParserPort;
|
|
11
|
+
validator?: Validator;
|
|
12
|
+
config?: ResolverConfig;
|
|
13
|
+
onProgress?: (event: ConflictProgressEvent) => void;
|
|
14
|
+
signal?: AbortSignal;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Per-file convergence loop (opt-in `config.fix`). Re-detects the file's per-file findings, feeds the
|
|
18
|
+
* error-severity ones back through refine as fix instructions, and repeats until the file is clean, a
|
|
19
|
+
* round makes no progress, or the iteration budget runs out. What it cannot fix stays in the returned
|
|
20
|
+
* `Resolution.findings` — the verdict is computed from that remainder, never from a stale pre-fix set.
|
|
21
|
+
*
|
|
22
|
+
* Only intra-file findings are addressed here (from {@link collectFileFindings}); cross-file references
|
|
23
|
+
* are a separate set-level stage. `dropped-side` is not produced per-file and so is never routed. The
|
|
24
|
+
* caller should invoke this only for AI-resolved (`strategy: 'ai'`) text files.
|
|
25
|
+
*/
|
|
26
|
+
declare function runFixLoop(resolution: Resolution, oursContent: string | undefined, theirsContent: string | undefined, deps: FixLoopDeps): Promise<Resolution>;
|
|
27
|
+
//#endregion
|
|
28
|
+
export { FixLoopDeps, runFixLoop };
|
|
Binary file
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{resolveChunkContent as e}from"../resolution/chunk-apply.js";import{createResolution as t}from"../resolution/create-resolution.js";import{checkResidualMarkers as n}from"../ports/verify.js";import{mergeUsages as r}from"../resolution/metrics.js";import{runResolverLoop as i}from"./loop.js";import{runRefinePass as a}from"./refine-pass.js";import{buildThreeWayDiff as o}from"./three-way-diff.js";const s={refineNoAction:.6,refineApplied:.9};async function c(c,l,u){let d=l.threeWayDiff||!l.refs?l:{...l,threeWayDiff:await o(c.filePath,l.refs,{git:u.git,signal:u.signal})},{chunks:f,strategy:p,confidence:m,description:h,metrics:g,followUpInstructions:_}=await i(c,d,{model:u.model,git:u.git,config:u.config,onProgress:u.onProgress,signal:u.signal}),v=t(c,f,{confidence:m,description:h,metrics:g,...p?{fileStrategy:p}:{},...d.threeWayDiff?{threeWayDiff:d.threeWayDiff}:{}}),y=_&&_.length>0;if(c.type===`text`&&y){u.onProgress?.({type:`refine:started`,filePath:c.filePath});let t=0;try{let i=await a(v.content,v.description,_,{model:u.model,signal:u.signal,temperature:u.config?.temperature,refineMaxSteps:u.config?.refineMaxSteps,onProgress:u.onProgress,filePath:c.filePath});if(i.edits.length>0){let e=n(c.filePath,i.content);if(e.valid)v={...v,content:i.content,edits:i.edits,strategy:`ai`},t=i.edits.length;else{let t=(e.issues??[]).map(e=>e.message).join(` `);u.onProgress?.({type:`refine:discarded`,filePath:c.filePath,reason:`Edits introduced conflict markers: ${t}`})}}if(t>0){v={...v,confidence:Math.min(v.confidence,s.refineApplied)};let t=[];for(let n=0;n<c.markers.length;n++){let r=c.markers[n];if(!r||r.userResolution==null)continue;let a=e({markerIndex:n,...r.userResolution},r);a!==``&&(i.content.includes(a)||t.push(n))}t.length>0&&(u.onProgress?.({type:`refine:touched-user-region`,filePath:c.filePath,markerIndexes:t}),v={...v,note:`Refine modified user-resolved region(s): marker ${t.join(`, `)}.`})}else i.edits.length===0&&i.verifies>0||(v={...v,confidence:Math.min(v.confidence,s.refineNoAction)});if(i.usage){let e=r(v.metrics??{inputTokens:0,outputTokens:0},i.usage);v={...v,metrics:{...v.metrics,...e}}}u.onProgress?.({type:`refine:completed`,filePath:c.filePath,appliedCount:t})}catch(e){let t=e instanceof Error?e.message:String(e);
|
|
1
|
+
import{resolveChunkContent as e}from"../resolution/chunk-apply.js";import{createResolution as t}from"../resolution/create-resolution.js";import{checkResidualMarkers as n}from"../ports/verify.js";import{mergeUsages as r}from"../resolution/metrics.js";import{runResolverLoop as i}from"./loop.js";import{runRefinePass as a}from"./refine-pass.js";import{buildThreeWayDiff as o}from"./three-way-diff.js";const s={refineNoAction:.6,refineApplied:.9};async function c(c,l,u){let d=l.threeWayDiff||!l.refs?l:{...l,threeWayDiff:await o(c.filePath,l.refs,{git:u.git,signal:u.signal})},{chunks:f,strategy:p,confidence:m,description:h,metrics:g,followUpInstructions:_}=await i(c,d,{model:u.model,git:u.git,config:u.config,onProgress:u.onProgress,signal:u.signal}),v=t(c,f,{confidence:m,description:h,metrics:g,...p?{fileStrategy:p}:{},...d.threeWayDiff?{threeWayDiff:d.threeWayDiff}:{}}),y=_&&_.length>0;if(c.type===`text`&&y){u.onProgress?.({type:`refine:started`,filePath:c.filePath});let t=0;try{let i=await a(v.content,v.description,_,{model:u.model,signal:u.signal,temperature:u.config?.temperature,refineMaxSteps:u.config?.refineMaxSteps,onProgress:u.onProgress,filePath:c.filePath});if(i.edits.length>0){let e=n(c.filePath,i.content);if(e.valid)v={...v,content:i.content,edits:i.edits,strategy:`ai`},t=i.edits.length;else{let t=(e.issues??[]).map(e=>e.message).join(` `);u.onProgress?.({type:`refine:discarded`,filePath:c.filePath,reason:`Edits introduced conflict markers: ${t}`})}}if(t>0){v={...v,confidence:Math.min(v.confidence,s.refineApplied)};let t=[];for(let n=0;n<c.markers.length;n++){let r=c.markers[n];if(!r||r.userResolution==null)continue;let a=e({markerIndex:n,...r.userResolution},r);a!==``&&(i.content.includes(a)||t.push(n))}t.length>0&&(u.onProgress?.({type:`refine:touched-user-region`,filePath:c.filePath,markerIndexes:t}),v={...v,note:`Refine modified user-resolved region(s): marker ${t.join(`, `)}.`})}else i.edits.length===0&&i.verifies>0||(v={...v,confidence:Math.min(v.confidence,s.refineNoAction)});if(i.usage){let e=r(v.metrics??{inputTokens:0,outputTokens:0},i.usage);v={...v,metrics:{...v.metrics,...e}}}u.onProgress?.({type:`refine:completed`,filePath:c.filePath,appliedCount:t})}catch(e){if(u.signal?.aborted)throw e;let t=e instanceof Error?e.message:String(e);u.onProgress?.({type:`refine:failed`,filePath:c.filePath,reason:t}),v={...v,confidence:Math.min(v.confidence,s.refineNoAction)}}}else u.onProgress?.({type:`refine:skipped`,filePath:c.filePath,reason:`no-follow-ups`});if(u.validator&&c.type===`text`)try{let e=await u.validator.validate(new Map([[c.filePath,v.content]]));if(e.length>0){let t=e.map(e=>({category:e.category,severity:e.severity,tier:`file`,location:{file:e.file,...e.line==null?{line:1}:{line:e.line}},message:e.message}));v={...v,findings:t}}}catch(e){if(u.signal?.aborted)throw e;let t=e instanceof Error?e.message:String(e);u.onProgress?.({type:`validation:failed`,filePath:c.filePath,reason:t})}return u.onProgress?.({type:`resolver:completed`,filePath:c.filePath,stepCount:g.stepCount??0}),v}export{c as resolveConflict};
|
|
Binary file
|
package/dist/types/events.d.ts
CHANGED
|
@@ -99,6 +99,15 @@ type ConflictProgressEvent = {
|
|
|
99
99
|
type: 'refine:touched-user-region';
|
|
100
100
|
filePath: string;
|
|
101
101
|
markerIndexes: number[];
|
|
102
|
+
} | {
|
|
103
|
+
type: 'fix:started';
|
|
104
|
+
filePath: string;
|
|
105
|
+
findingCount: number;
|
|
106
|
+
} | {
|
|
107
|
+
type: 'fix:completed';
|
|
108
|
+
filePath: string;
|
|
109
|
+
iterations: number;
|
|
110
|
+
remaining: number;
|
|
102
111
|
} | {
|
|
103
112
|
type: 'verification:issues';
|
|
104
113
|
filePath: string;
|