@gitkraken/conflict-tools 0.2.4-beta.24 → 0.2.4-beta.25

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 CHANGED
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
9
9
 
10
10
  ### Added
11
11
 
12
+ - `StepConfig.maxDetectionScopeFiles` — override the reference-integrity scope cap (default 400). Beyond the cap the scope is `truncated`, which holds the verdict off `clean`; raising it lets a large merge stay on the `--fix` auto-apply path. Threaded through `DetectDeps`/`SnapshotDeps` as `maxScopeFiles`.
12
13
  - `StepConfig.maxModelCalls` — an optional global model-call budget for the whole operation. Enforced per model call (the refine pass checks it before every step), not merely per pass. When set and the budget is spent, the opt-in fix phase stops attempting further fixes and leaves the remainder in the findings (so the verdict still reflects the true post-fix state), emitting a new `fix:budget-exhausted` progress event (`{ spent, limit }`). When unset, the fix phase is not budget-limited.
13
14
  - `fix:failed` progress event (`{ filePath?, reason }`) — emitted when the opt-in fix phase throws a non-abort error, so a consumer can observe the graceful degradation rather than only seeing the checks flip to `errored`. `filePath` is set when one file failed and the phase carried on without it; absent when the whole phase failed.
14
15
  - `ParserPort.capabilities` — a backend declares what it can resolve (currently `crossFileResolution`). A backend that cannot resolve references across files reports every import as unresolved on both parents *and* the result, so no cross-file break can ever surface; detection now reports `partial-no-cross-file-resolution` for that instead of letting the empty result pass as a clean cross-file check. Omitted means the conservative reading (no cross-file resolution).
@@ -42,6 +43,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
42
43
 
43
44
  ### Fixed
44
45
 
46
+ - An abort landing before the report-only detection no longer rejects the operation and discards the partial resolutions the resolve loop deliberately kept. The `else`/report-only path fed the already-aborted signal to `detect()`, which (per the parser-port contract, honored by any real adapter) rethrows on abort. A cancelled operation now returns its partial `StepResult` with an honest incomplete report (`checks: errored`), matching the fix-off loop-break intent; only the fake parser used in some tests, which ignores the signal, hid this.
47
+ - The fix phase's regression guard no longer accepts a round that adds a second same-category error. `introducedNewError` keyed on category/file/symbol (line and count excluded), so a round turning one `syntax-error` (or validator error, both symbol-less) into two shared a fingerprint and read as "no new kind" — the worse content was kept and confidence capped as if improved. The guard (now `regressed`) also rejects when the error count grows, at both the per-file and cross-file sites, restoring the "never ship a file worse than the one it was handed" invariant.
45
48
  - Files nobody resolved no longer poison the detection scope. A conflicted file excluded by a `skip` rule, or one that errored during extraction, has no resolution, so the detector read it from the working tree — conflict markers and all — and the parser reported the marker text as syntax errors. That forced `verdict: 'blocked'` for the whole operation, which made `clean` unreachable for anyone using skip rules (the README's own lockfile guidance). Skipped and errored paths are now excluded from scope outright.
46
49
  - `OperationDigest.detectionComplete` no longer ignores coverage gaps. Both checks could report `'ran'` while part of the scope was never examined — past the `MAX_SCOPE_FILES` cap (`scope.truncated`), or when the git port could not enumerate the incoming set — yielding `verdict: 'clean'` with hundreds of files unlooked-at. `detectionComplete` now also requires the scope to be fully covered, and `DetectionReport.scope` gained `incomingDiscovered` so "the other side changed nothing" is distinguishable from "we could not ask".
47
50
  - `duplicate-definition` no longer fires on legal same-name declarations. It ran over the whole result overlay — up to 400 auto-merged files — and reported every TS function/method overload, `interface`/`namespace`/`enum` declaration merging, and platform-conditional definition as a warning, which drives `needs-review` and made `clean` unreachable on ordinary TypeScript. Duplicates are now diffed against the parents like broken references already were: only a duplicate the merge introduced is reported. Without a parent baseline none can be attributed, so none are reported.
package/README.md CHANGED
@@ -381,7 +381,7 @@ const digest = buildOperationDigest(step);
381
381
 
382
382
  **Gate auto-apply on the verdict, not on confidence.** A refined or fixed file has its confidence capped at 0.9 — so a "confidence must be 1.0" gate would reject exactly the files the tool improved. `verdict === 'clean'` is the honest signal. And an *absent* verdict must NOT be read as clean: if you never ran detection (no parser) or never summarized, `detectionComplete` is `false` and empty findings mean "we didn't look," not "nothing's wrong." Only `verdict === 'clean'` is clean.
383
383
 
384
- A `ReviewItem` distinguishes a low-confidence flag from a finding: `reason: 'low-confidence' | 'finding'`, with `confidence?` (absent for a file flagged purely by a finding, e.g. an auto-merged file with no resolution of its own) and `findingCategories?` (present when `reason` is `'finding'`).
384
+ A `ReviewItem` distinguishes a low-confidence flag from a finding: `reason: 'low-confidence' | 'finding' | 'unresolved'` (`'unresolved'` marks an unmerged entry the tool left untouched — a no-markers skip like a binary add/add — which keeps the operation off `clean`), with `confidence?` (absent for a file flagged purely by a finding or as unresolved, e.g. an auto-merged file with no resolution of its own) and `findingCategories?` (present when `reason` is `'finding'`).
385
385
 
386
386
  ## Fix phase (opt-in)
387
387
 
@@ -13,6 +13,8 @@ interface DetectDeps {
13
13
  /** Paths to keep out of scope — the caller's skipped and failed files, which still carry conflict
14
14
  * markers on disk. See {@link import('./snapshot.js').SnapshotDeps.excludePaths}. */
15
15
  excludePaths?: ReadonlySet<string>;
16
+ /** Override the per-operation scope cap. See {@link import('./snapshot.js').SnapshotDeps.maxScopeFiles}. */
17
+ maxScopeFiles?: number;
16
18
  }
17
19
  /** Parent contents for one file, named so the two sides cannot be transposed at a call site. */
18
20
  interface ParentContents {
@@ -3,6 +3,10 @@ import { Resolution, ResolutionContext } from "../types/resolution.js";
3
3
  import { SourceOverlay } from "../ports/parser.js";
4
4
 
5
5
  //#region src/reference-integrity/snapshot.d.ts
6
+ /** Default backstop on how many files the detector will parse in one operation, overridable via
7
+ * `SnapshotDeps.maxScopeFiles`. The scope is ordered resolved-first, so a cap drops the least-important
8
+ * tail; truncation is reported, never silent. */
9
+ declare const DEFAULT_MAX_SCOPE_FILES = 400;
6
10
  interface DetectionSnapshots {
7
11
  ours: SourceOverlay;
8
12
  theirs: SourceOverlay;
@@ -24,6 +28,9 @@ interface SnapshotDeps {
24
28
  * still carry conflict markers in the working tree, so parsing them would report the marker text as
25
29
  * syntax errors and blame the merge for a file nobody resolved. */
26
30
  excludePaths?: ReadonlySet<string>;
31
+ /** Cap on files parsed per operation. Default {@link DEFAULT_MAX_SCOPE_FILES}. Exceeding it sets
32
+ * `truncated`, which keeps the verdict off `clean`; raise it so a large merge can still auto-apply. */
33
+ maxScopeFiles?: number;
27
34
  }
28
35
  /**
29
36
  * Builds the file-content overlays the detector compares: both pre-merge parents (read from git
@@ -35,4 +42,4 @@ interface SnapshotDeps {
35
42
  */
36
43
  declare function buildSnapshots(resolutions: Resolution[], context: ResolutionContext, deps: SnapshotDeps): Promise<DetectionSnapshots | null>;
37
44
  //#endregion
38
- export { DetectionSnapshots, SnapshotDeps, buildSnapshots };
45
+ export { DEFAULT_MAX_SCOPE_FILES, DetectionSnapshots, SnapshotDeps, buildSnapshots };
@@ -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={...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};
1
+ import{changedFiles as e,mergeBase as t,readFile as n,showFile as r}from"../git/port-dispatch.js";const i=400;async function a(e,t,n){let r=t.detectionRefs??t.refs;if(!r)return null;let i=n.signal?{signal:n.signal}:void 0,a={...i,fullContent:!0},l=new Map(e.map(e=>[e.filePath,e.content])),u=t.touchedFiles?{files:t.touchedFiles,discovered:!0}:await o(n.git,r,i),d=n.excludePaths,f=[...new Set([...l.keys(),...u.files,...t.fileNeighbors??[]])].filter(e=>!d?.has(e)),p=n.maxScopeFiles??400,m=f.length>p,h=m?f.slice(0,p):f,g=new Map,_=new Map,v=new Map,y=0;for(let e of h){let t=await s(n.git,r.ours,e,a);t&&g.set(e,t);let o=await s(n.git,r.theirs,e,a);o&&_.set(e,o),(t||o)&&y++;let u=l.get(e);if(u!=null)v.set(e,u);else{let t=await c(n.git,e,i);t!=null&&v.set(e,t)}}return{ours:g,theirs:_,result:v,inScope:h.length,readable:y,truncated:m,incomingDiscovered:u.discovered}}async function o(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 l(e,i),{files:[],discovered:!1}}}async function s(e,t,n,i){try{return await r(e,t,n,i)}catch(e){return l(e,i),null}}async function c(e,t,r){try{return await n(e,t,r)}catch(e){return l(e,r),null}}function l(e,t){if(t?.signal?.aborted)throw e}export{i as DEFAULT_MAX_SCOPE_FILES,a as buildSnapshots};
@@ -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,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
+ 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 regressed the file (was ${g.length} error(s), 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){if(t.length>e.length)return!0;let n=new Set(e.map(p));return t.some(e=>!n.has(p(e)))}async function g(e,n,i){let a=i.config?.maxDetectionScopeFiles,o={git:i.git,parser:i.parser,signal:i.signal,excludePaths:i.excludePaths,maxScopeFiles:a},s=await t(e,n,{git:i.git,signal:i.signal,excludePaths:i.excludePaths,maxScopeFiles:a}),c=()=>i.onProgress?.({type:`fix:budget-exhausted`,spent:i.budget?.spent()??0,limit:i.budget?.limit()??0}),l={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(c)};return await _(e,s,l),s?v(e,n,s,o,l,i):r(e,n,o)}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 left the file with a new or additional error.`}),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
- 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
+ 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,maxScopeFiles:e.config?.maxDetectionScopeFiles},E;if(e.signal?.aborted)E={findings:[],checks:{referenceIntegrity:`errored`,structural:`errored`}};else if(e.config?.fix&&e.parser)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};
@@ -39,6 +39,9 @@ interface StepConfig extends ResolverConfig {
39
39
  /** Max cross-file fix rounds (route project-level breaks to their use-site file → re-detect the whole
40
40
  * set) when `fix` is on. Default 2. */
41
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;
42
45
  /** Global cap on `model.generate` calls for the whole operation (resolve + refine + fix). Default
43
46
  * `undefined` = unlimited. Primarily a backstop for the fix phase's multiplicative worst case
44
47
  * (files × fixLoopMaxIterations + crossFileMaxIterations × routable calls). When spent, the fix
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitkraken/conflict-tools",
3
- "version": "0.2.4-beta.24",
3
+ "version": "0.2.4-beta.25",
4
4
  "description": "AI-powered git conflict resolution.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Axosoft, LLC dba GitKraken",