@gitkraken/conflict-tools 0.2.4-beta.9 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/CHANGELOG.md +32 -7
  2. package/README.md +262 -55
  3. package/dist/errors/conflict.d.ts +1 -1
  4. package/dist/git/port-dispatch.js +2 -3
  5. package/dist/index.d.ts +4 -4
  6. package/dist/index.js +1 -1
  7. package/dist/ports/git.d.ts +4 -0
  8. package/dist/ports/parser.d.ts +27 -6
  9. package/dist/ports/validator.d.ts +29 -0
  10. package/dist/ports/validator.js +1 -0
  11. package/dist/reference-integrity/detect.d.ts +39 -4
  12. package/dist/reference-integrity/detect.js +2 -2
  13. package/dist/reference-integrity/snapshot.d.ts +18 -5
  14. package/dist/reference-integrity/snapshot.js +1 -1
  15. package/dist/resolution/detect-code-loss.d.ts +23 -0
  16. package/dist/resolution/detect-code-loss.js +1 -0
  17. package/dist/resolver/deps.d.ts +1 -1
  18. package/dist/resolver/fix-loop.d.ts +67 -0
  19. package/dist/resolver/fix-loop.js +2 -0
  20. package/dist/resolver/model-budget.d.ts +26 -0
  21. package/dist/resolver/model-budget.js +1 -0
  22. package/dist/resolver/prompt.d.ts +8 -2
  23. package/dist/resolver/prompt.js +5 -3
  24. package/dist/resolver/refine-pass.d.ts +8 -0
  25. package/dist/resolver/refine-pass.js +1 -1
  26. package/dist/resolver/resolve-conflict.d.ts +7 -1
  27. package/dist/resolver/resolve-conflict.js +1 -1
  28. package/dist/resolver/resolve-conflicts.js +1 -1
  29. package/dist/resolver/summarize-operation.js +1 -1
  30. package/dist/resolver/validate.js +1 -1
  31. package/dist/types/events.d.ts +30 -3
  32. package/dist/types/findings.d.ts +25 -15
  33. package/dist/types/resolution.d.ts +11 -0
  34. package/dist/types/step.d.ts +27 -0
  35. package/dist/types/summary.d.ts +17 -10
  36. package/dist/validation/findings.d.ts +9 -0
  37. package/dist/validation/findings.js +1 -0
  38. package/dist/validation/markers.d.ts +11 -0
  39. package/dist/{ports/verify.js → validation/markers.js} +1 -1
  40. package/package.json +1 -1
  41. package/dist/ports/verify.d.ts +0 -30
@@ -1 +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{detect as r}from"../reference-integrity/detect.js";import{createResolution as i}from"../resolution/create-resolution.js";import{takeOurs as a,takeTheirs as o}from"../resolution/take-strategy.js";import{resolveConflict as s}from"./resolve-conflict.js";import c from"picomatch";function l(e){let t=e.map(e=>{let t=(Array.isArray(e.match)?e.match:[e.match]).map(e=>c(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 u(e,s){let c=e.config?.rules??[],u=e.config?.defaultStrategy??`ai`,p=l(c),m=await t(e.git,{signal:e.signal}),h={...s??{}},g=[],_=[],v=[];for(let t of m){if(e.signal?.aborted)break;let r=p(t.path)??u;if(r===`skip`){v.push({filePath:t.path,reason:`excluded`}),e.onProgress?.({type:`conflict:excluded`,filePath:t.path});continue}let s;try{s=await n(t.path,{git:e.git,signal:e.signal},t.reason)}catch(e){_.push({filePath:t.path,error:f(e)});continue}if(!s){v.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:s.filePath,conflictType:s.type,markerCount:s.markers.length});let c=null;c=r===`take-ours`?a(s):r===`take-theirs`?o(s):r===`deleted`?i(s,[],{confidence:1,fileStrategy:`deleted`}):await d(s,h,e,_),c&&(h.previousResolutions=[...h.previousResolutions??[],c],g.push(c),e.onProgress?.({type:`resolution:applied`,filePath:c.filePath,strategy:c.strategy,confidence:c.confidence,description:c.description}))}let y=await r(g,h,{git:e.git,parser:e.parser,signal:e.signal}),b=g.flatMap(e=>e.findings??[]);return{resolutions:g,errors:_,skipped:v,detection:b.length>0?{...y,findings:[...y.findings,...b]}:y}}async function d(t,n,r,i){try{return await s(t,n,{model:r.model,git:r.git,config:r.config,onProgress:r.onProgress,signal:r.signal,validator:r.validator})}catch(n){let s=f(n),c=r.config?.fallbackStrategy;return c?(r.onProgress?.({type:`resolution:fallback`,filePath:t.filePath,error:s,cause:s instanceof e?s.code:void 0}),{...c===`take-ours`?a(t):o(t),confidence:0}):(i.push({filePath:t.filePath,error:s}),r.onProgress?.({type:`resolution:failed`,filePath:t.filePath,error:s}),null)}}function f(e){return e instanceof Error?e:Error(String(e))}export{u as resolveConflicts};
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 i=e.config?.rules??[],o=e.config?.defaultStrategy??`ai`,l=p(i),u=await n(e.git,{signal:e.signal}),f=d(e.config?.maxModelCalls),m=f.wrap(e.model),v={...t??{}},y=[],b=[],x=[];for(let t of u){if(e.signal?.aborted)break;let n=l(t.path)??o;if(n===`skip`){x.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){b.push({filePath:t.path,error:_(e)});continue}if(!i){x.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 u=null;u=n===`take-ours`?{...s(i),deliberateTake:!0}:n===`take-theirs`?{...c(i),deliberateTake:!0}:n===`deleted`?a(i,[],{confidence:1,fileStrategy:`deleted`}):await g(i,v,{...e,model:m},b),u&&(v.previousResolutions=[...v.previousResolutions??[],u],y.push(u),e.onProgress?.({type:`resolution:applied`,filePath:u.filePath,strategy:u.strategy,confidence:u.confidence,description:u.description}))}return{resolutions:y,errors:b,skipped:x,detection:await h(y,v,x,b,f,{...e,model:m})}}async function h(e,t,n,r,a,o){let s=new Set([...n.map(e=>e.filePath),...r.map(e=>e.filePath)]),c={git:o.git,parser:o.parser,signal:o.signal,excludePaths:s,maxScopeFiles:o.config?.maxDetectionScopeFiles},l;if(o.signal?.aborted)l={findings:[],checks:{referenceIntegrity:`errored`,structural:`errored`}};else if(o.config?.fix&&o.parser)try{l=await u(e,t,{model:o.model,git:o.git,parser:o.parser,validator:o.validator,config:o.config,onProgress:o.onProgress,signal:o.signal,budget:a,excludePaths:s})}catch(n){if(o.signal?.aborted)throw n;o.onProgress?.({type:`fix:failed`,reason:_(n).message}),l=await i(e,t,c),l={...l,checks:{referenceIntegrity:`errored`,structural:`errored`}}}else l=await i(e,t,c);let d=e.flatMap(e=>e.findings??[]);return{...l,findings:v([...l.findings,...d])}}async function g(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=_(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 _(e){return e instanceof Error?e:Error(String(e))}function v(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 a={ai:0,"take-ours":0,"take-theirs":0,deleted:0,skipped:0},o=0,s=null,c=new Map;for(let t of e.resolutions)a[t.strategy]++,o+=t.chunks?.length??0,s=s===null?t.confidence:Math.min(s,t.confidence),t.confidence<n&&c.set(t.filePath,{filePath:t.filePath,confidence:t.confidence,reason:`low-confidence`});let l=e.detection,u=l?.findings??[],d=new Map;for(let e of u){if(e.severity!==`error`)continue;let t=d.get(e.location.file)??new Set;t.add(e.category),d.set(e.location.file,t)}for(let[e,t]of d){let n=c.get(e);c.set(e,{filePath:e,...n?.confidence==null?{}:{confidence:n.confidence},reason:`finding`,findingCategories:[...t].sort()})}let f=[...c.values()].sort(r),p=l!=null&&l.checks.referenceIntegrity===`ran`&&l.checks.structural===`ran`,m=i({hasErrorFinding:d.size>0,hasWarningFinding:u.some(e=>e.severity===`warning`),needsReview:f.length>0,detectionComplete:p,hasErrors:e.errors.length>0});return{files:e.resolutions.length,conflicts:o,byStrategy:a,minConfidence:s,needsReview:f,skipped:e.skipped?.length??0,errors:e.errors.length,detectionComplete:p,findingCount:u.length,verdict:m}}function r(e,t){return e.reason===t.reason?(e.confidence??1)-(t.confidence??1):e.reason===`finding`?-1:1}function i(e){return e.hasErrorFinding?`blocked`:e.needsReview||!e.detectionComplete||e.hasErrors||e.hasWarningFinding?`needs-review`:`clean`}async function a(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,a 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};
@@ -1,2 +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++)n.markers[e]?.userResolution??(a.has(e)||r.push(`Missing resolution for marker ${e}.`));for(let n of t)if(n.strategy===`merged`){let t=n.content.split(`
1
+ import{CONFLICT_MARKER_PATTERNS as e}from"../validation/markers.js";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++)n.markers[e]?.userResolution??(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
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}:{valid:!1,issues:r.map(e=>({severity:`error`,message:e}))}}export{t as validateChunks};
@@ -1,6 +1,5 @@
1
1
  import { ResolutionStrategy, ResolvedChunk } from "./resolution.js";
2
2
  import { ConflictType } from "./conflict.js";
3
- import { Issue } from "./issue.js";
4
3
  import { AiTokenUsage } from "@gitkraken/shared-tools";
5
4
 
6
5
  //#region src/types/events.d.ts
@@ -91,14 +90,42 @@ type ConflictProgressEvent = {
91
90
  type: 'refine:failed';
92
91
  filePath: string;
93
92
  reason: string;
93
+ } | {
94
+ type: 'refine:bailed';
95
+ filePath: string;
96
+ reason: string;
94
97
  } | {
95
98
  type: 'refine:touched-user-region';
96
99
  filePath: string;
97
100
  markerIndexes: number[];
98
101
  } | {
99
- type: 'verification:issues';
102
+ type: 'fix:started';
103
+ filePath: string;
104
+ findingCount: number;
105
+ }
106
+ /** `iterations` counts refine rounds actually run for this file (0 = evaluated, nothing spent).
107
+ * `remainingFindings` is the error findings still standing — named apart from the model-call budget's
108
+ * `remaining`, which the sibling `fix:budget-exhausted` reports as `spent`/`limit`. */
109
+ | {
110
+ type: 'fix:completed';
111
+ filePath: string;
112
+ iterations: number;
113
+ remainingFindings: number;
114
+ }
115
+ /** `filePath` is set when one file's fix threw and the phase carried on without it; absent when the
116
+ * whole phase failed. */
117
+ | {
118
+ type: 'fix:failed';
119
+ filePath?: string;
120
+ reason: string;
121
+ } | {
122
+ type: 'fix:budget-exhausted';
123
+ spent: number;
124
+ limit: number;
125
+ } | {
126
+ type: 'validation:failed';
100
127
  filePath: string;
101
- issues: Issue[];
128
+ reason: string;
102
129
  } | {
103
130
  type: 'resolver:tool-result';
104
131
  filePath: string;
@@ -1,13 +1,13 @@
1
1
  //#region src/types/findings.d.ts
2
- /** The package's own detection categories. `Finding.category` is an open `string` so consumer
3
- * validators can contribute their own kinds (e.g. `'type-error'`, `'lint:no-console'`); these are the
4
- * well-known values the package itself emits. */
2
+ /** The package's own detection categories. `Finding.category` stays open so consumer validators can
3
+ * contribute their own kinds (e.g. `'type-error'`, `'lint:no-console'`); these are the well-known
4
+ * values the package itself emits. */
5
5
  type FindingCategory = 'broken-reference' | 'duplicate-definition' | 'syntax-error' | 'dropped-side';
6
6
  type FindingTier = 'file' | 'project';
7
7
  interface Finding {
8
- /** Finding kind. Open string — the package emits {@link FindingCategory} values; a consumer
9
- * validator may use its own category strings. */
10
- category: string;
8
+ /** Finding kind. The package emits {@link FindingCategory} values; a consumer validator may use its
9
+ * own category strings, which is what the open half of the union allows. */
10
+ category: FindingCategory | (string & {});
11
11
  severity: 'error' | 'warning';
12
12
  tier: FindingTier;
13
13
  location: {
@@ -20,27 +20,37 @@ interface Finding {
20
20
  side: 'ours' | 'theirs';
21
21
  line: number;
22
22
  };
23
+ /** Present for a cross-file `broken-reference` when the parser reported the pre-merge resolution
24
+ * target: the file whose definition the use site expected. Lets the cross-file fix pass show the
25
+ * refining model what that file currently defines (a rename/move is otherwise invisible from the use
26
+ * site alone). Absent when the backend does not fill reference targets. */
27
+ definitionFile?: string;
23
28
  message: string;
24
29
  }
25
30
  /** Why a check produced its result — lets a consumer tell "ran, clean" apart from "did not run".
26
31
  * `errored` means the parser was present but threw; `partial-unsupported-language` means some files
27
- * in scope could not be parsed; `skipped-unreadable-source` means the parent snapshots came back
28
- * empty (unreadable source), so the before/after comparison had nothing to compare against. */
29
- type CheckStatus = 'ran' | 'skipped-no-parser' | 'skipped-no-refs' | 'skipped-unreadable-source' | 'partial-unsupported-language' | 'errored';
32
+ * in scope could not be parsed; `partial-no-cross-file-resolution` means the parser backend declared
33
+ * it does not resolve references across files, so cross-file breaks are structurally out of reach;
34
+ * `skipped-unreadable-source` means the parent snapshots came back empty (unreadable source), so the
35
+ * before/after comparison had nothing to compare against. */
36
+ type CheckStatus = 'ran' | 'skipped-no-parser' | 'skipped-no-refs' | 'skipped-unreadable-source' | 'partial-unsupported-language' | 'partial-no-cross-file-resolution' | 'errored';
30
37
  interface DetectionReport {
31
38
  findings: Finding[];
32
39
  checks: {
33
40
  referenceIntegrity: CheckStatus;
34
41
  structural: CheckStatus;
35
42
  };
36
- /** Honest coverage signal so empty findings are not read as "clean": `analyzed` is the number of
37
- * files in scope, `readable` how many had loadable parent content (0 means the before/after
38
- * comparison had nothing to compare), and `truncated` whether the scope was capped. Present only
39
- * when the reference check ran (refs available). */
43
+ /** Honest coverage signal so empty findings are not read as "clean". Present only when the reference
44
+ * check ran (refs available). */
40
45
  scope?: {
41
- analyzed: number;
42
- readable: number;
46
+ /** How many files the detector took into scope, after the cap. */inScope: number;
47
+ /** How many scope files had loadable parent content. 0 means the before/after comparison had
48
+ * nothing to compare. */
49
+ readable: number; /** Whether the scope exceeded the cap and its tail was dropped. */
43
50
  truncated: boolean;
51
+ /** Whether the merge's incoming/auto-merged set could be discovered. `false` means the git port
52
+ * could not answer, so scope fell back to the conflict set — a coverage gap, not an empty diff. */
53
+ incomingDiscovered: boolean;
44
54
  };
45
55
  }
46
56
  //#endregion
@@ -26,6 +26,17 @@ interface Resolution {
26
26
  confidence: number;
27
27
  description: string;
28
28
  note?: string;
29
+ /** Set when this resolution is the resolver's emergency fallback — a whole-side take after the AI
30
+ * path failed, not a decision. Distinguishes it from an identical-looking rule or default take, and
31
+ * from a genuine AI resolution that happened to pick one side; reference-integrity uses it to decide
32
+ * whether discarding the other side was intentional. */
33
+ fallback?: true;
34
+ /** Set when this resolution is a deliberate whole-side discard chosen by a file rule or the default
35
+ * strategy — the caller asked to take one side wholesale, so the fix phase must not refine it back
36
+ * toward the discarded side. Distinct from `fallback` (an emergency take after the AI path failed)
37
+ * and from an AI resolution that merely happened to pick one side unanimously (which the fix phase
38
+ * may still refine). */
39
+ deliberateTake?: true;
29
40
  /** Non-blocking per-file findings from the consumer's validator port (post-refine). Folded into
30
41
  * the operation's detection findings, hence into the verdict. */
31
42
  findings?: Finding[];
@@ -23,6 +23,33 @@ 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;
29
+ /** Opt in to the fix phase (default `false` → report-only). When enabled, an AI-resolved file carrying
30
+ * error-severity per-file findings is routed back through refine to attempt a fix; what it cannot fix
31
+ * remains in the findings. Requires `ResolverDeps.parser` — without a parser the fix phase is a no-op
32
+ * and the report-only detection path runs instead. Without `ResolutionContext.refs` (or
33
+ * `detectionRefs`) the per-file stage still runs on what needs no parent content — syntax errors and
34
+ * validator findings — but cross-file routing is skipped, since it is driven by the whole-set
35
+ * before/after report. Off by default so the report-only path is unchanged. */
36
+ fix?: boolean;
37
+ /** Max per-file fix iterations (refine → re-detect the file) when `fix` is on. Default 3. */
38
+ fixLoopMaxIterations?: number;
39
+ /** Max cross-file fix rounds (route project-level breaks to their use-site file → re-detect the whole
40
+ * set) when `fix` is on. Default 2. */
41
+ crossFileMaxIterations?: number;
42
+ /** Override the reference-integrity scope cap (default 400 files). Beyond the cap the scope is
43
+ * `truncated`, which holds the verdict off `clean`; raise it so a large merge stays auto-applyable. */
44
+ maxDetectionScopeFiles?: number;
45
+ /** Global cap on `model.generate` calls for the whole operation (resolve + refine + fix). Default
46
+ * `undefined` = unlimited. Primarily a backstop for the fix phase's multiplicative worst case
47
+ * (files × fixLoopMaxIterations + crossFileMaxIterations × routable calls). When spent, the fix
48
+ * phase stops attempting further fixes and leaves the remainder in findings — it never aborts the
49
+ * operation. The resolve loop is not gated by this (it is already bounded by `maxSteps`); its calls
50
+ * still count toward the cap. Batch-only: the fix phase is what enforces it, so it has no effect on
51
+ * the single-file `resolveConflict`. */
52
+ maxModelCalls?: number;
26
53
  }
27
54
  //#endregion
28
55
  export { FileRule, StepConfig, StepResult };
@@ -2,14 +2,18 @@ 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 from a
6
- * detection finding; `confidence` is absent for files flagged purely by a finding (e.g. an
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
- reason: 'low-confidence' | 'finding';
12
- /** Categories of the error-severity findings on this file; present when `reason` is `'finding'`. */
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';
15
+ /** Categories of the findings on this file, of any severity (error and warning alike); present when
16
+ * `reason` is `'finding'`. */
13
17
  findingCategories?: string[];
14
18
  }
15
19
  /** Suggested honest verdict for the operation. `blocked` when any error-severity finding exists;
@@ -27,16 +31,19 @@ interface OperationDigest {
27
31
  byStrategy: Record<ResolutionStrategy, number>;
28
32
  /** Lowest file confidence across all resolutions; null when there are none. */
29
33
  minConfidence: number | null;
30
- /** Files needing review — low-confidence resolutions and files with error findings, findings
31
- * first, then lowest confidence first. */
34
+ /** Files needing review — low-confidence resolutions and files carrying any finding (error or
35
+ * warning), findings first, then lowest confidence first. */
32
36
  needsReview: ReviewItem[];
33
37
  /** Files skipped by rule or because they had no markers. */
34
38
  skipped: number;
35
39
  /** Files that errored during resolution. */
36
40
  errors: number;
37
- /** Whether reference-integrity detection ran to completion (every check `ran`). `false` when no
38
- * detection was attached or any check was skipped/errored — so empty findings must NOT be read
39
- * as "clean". */
41
+ /** Whether detection achieved full coverage: both checks ran (a check that ran with some
42
+ * unsupported-language files in scope still counts — non-code files carry no references to break)
43
+ * *and* nothing was left unexamined (scope not truncated, incoming set discoverable). `false` when
44
+ * no detection was attached, a check was skipped/errored, the backend cannot resolve cross-file
45
+ * references, or part of the scope was never looked at — so empty findings must NOT be read as
46
+ * "clean". */
40
47
  detectionComplete: boolean;
41
48
  /** Total detection findings across the operation. */
42
49
  findingCount: number;
@@ -0,0 +1,9 @@
1
+ import { Finding } from "../types/findings.js";
2
+ import { ValidatorFinding } from "../ports/validator.js";
3
+
4
+ //#region src/validation/findings.d.ts
5
+ /** Lifts a consumer finding into the package's `Finding` shape. Validators run per file, so everything
6
+ * they report is `tier: 'file'`. */
7
+ declare function toFileFinding(finding: ValidatorFinding): Finding;
8
+ //#endregion
9
+ export { toFileFinding };
@@ -0,0 +1 @@
1
+ function e(e){return{category:e.category,severity:e.severity,tier:`file`,location:{file:e.file,line:e.line??1},message:e.message}}export{e as toFileFinding};
@@ -0,0 +1,11 @@
1
+ import { Issue } from "../types/issue.js";
2
+
3
+ //#region src/validation/markers.d.ts
4
+ interface VerificationResult {
5
+ valid: boolean;
6
+ issues?: Issue[];
7
+ }
8
+ declare const CONFLICT_MARKER_PATTERNS: readonly [RegExp, RegExp, RegExp, RegExp];
9
+ declare function checkResidualMarkers(_filePath: string, content: string): VerificationResult;
10
+ //#endregion
11
+ export { CONFLICT_MARKER_PATTERNS, VerificationResult, checkResidualMarkers };
@@ -1,2 +1,2 @@
1
1
  const e=[/^<{7}(\s|$)/,/^\|{7}(\s|$)/,/^={7}(\s|$)/,/^>{7}(\s|$)/];function t(t,n){let r=n.split(`
2
- `),i=[];for(let t=0;t<r.length;t++){let n=r[t]??``;for(let r of e)if(r.test(n)){i.push(t+1);break}}return i.length===0?{valid:!0}:{valid:!1,issues:[{severity:`error`,message:`Residual conflict markers found at line(s) ${i.join(`, `)}.`}]}}export{t as checkResidualMarkers};
2
+ `),i=[];for(let t=0;t<r.length;t++){let n=r[t]??``;for(let r of e)if(r.test(n)){i.push(t+1);break}}return i.length===0?{valid:!0}:{valid:!1,issues:[{severity:`error`,message:`Residual conflict markers found at line(s) ${i.join(`, `)}.`}]}}export{e as CONFLICT_MARKER_PATTERNS,t as checkResidualMarkers};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitkraken/conflict-tools",
3
- "version": "0.2.4-beta.9",
3
+ "version": "0.3.0",
4
4
  "description": "AI-powered git conflict resolution.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Axosoft, LLC dba GitKraken",
@@ -1,30 +0,0 @@
1
- import { SourceOverlay } from "./parser.js";
2
- import { Issue } from "../types/issue.js";
3
-
4
- //#region src/ports/verify.d.ts
5
- interface VerificationResult {
6
- valid: boolean;
7
- issues?: Issue[];
8
- }
9
- interface ValidatorFinding {
10
- /** Open category string set by the consumer (e.g. `'type-error'`, `'lint:no-console'`). */
11
- category: string;
12
- severity: 'error' | 'warning';
13
- file: string;
14
- /** 1-indexed. */
15
- line?: number;
16
- message: string;
17
- }
18
- /**
19
- * The consumer's validation port (RFC stages 5/7). Plug in any checks — syntax, real import
20
- * resolution, types, lint, project rules. **Non-blocking**: runs post-refine over an overlay (one file
21
- * for per-file validation, or the whole touched set for cross-file); its findings are advisory (folded
22
- * into the operation's findings and verdict), never a re-resolve. The only blocking gate is residual
23
- * conflict markers, checked built-in by the resolver ({@link checkResidualMarkers}) — not this port.
24
- */
25
- interface Validator {
26
- validate(sources: SourceOverlay): Promise<ValidatorFinding[]>;
27
- }
28
- declare function checkResidualMarkers(_filePath: string, content: string): VerificationResult;
29
- //#endregion
30
- export { Validator, ValidatorFinding, VerificationResult, checkResidualMarkers };