@gitkraken/conflict-tools 0.7.1-beta.12 → 0.7.1-beta.13

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/README.md CHANGED
@@ -514,6 +514,7 @@ When enabled:
514
514
  - **Duplicate blocks** — a block of four or more significant lines that the merged file holds more often than either parent is reported as a `duplicate-block` warning whenever `refs` are set, with or without the fix phase. The fix phase still routes it through refine to keep one copy, and a leftover one gives `needs-review`, not `blocked`. This detection is language-agnostic, so it needs no `ParserPort`, but it needs at least one parent and does not see a block neither parent holds. Without a `ParserPort` the fix phase still runs the duplicate-block and validator checks, and cross-file routing is skipped.
515
515
  - **Per-file convergence** — an AI-resolved (`strategy: 'ai'`) text file carrying error-severity per-file findings is routed back through refine; the file is re-analyzed after each edit and the loop repeats until it is clean, a round makes no progress, or `fixLoopMaxIterations` (default 3) is reached.
516
516
  - **Cross-file routing** — a `tier: 'project'` broken reference (an import/cross-file use whose target stopped resolving) is routed to the refine of its *use-site* file, and the whole set is re-detected, up to `crossFileMaxIterations` (default 2) rounds. The refine is seeded with the defining file's current definitions (names, kind, line — a token-cheap list, not its text) so it can spot a renamed or moved symbol behind the broken use, even though the refine tool cannot read other files. A file is routed when it has a write channel: an AI resolution, or an AI-unanimous whole-side take (relabelled to `ai` when the fix edit lands). A deliberate rule/default take and an emergency fallback take are not routed, and a break landing on an untouched auto-merged file (no resolution to carry the edit) is reported but not auto-fixed.
517
+ - **Whole-set validator routing** — after the cross-file stage, the error findings of the validator's whole-set call (`scope: 'set'`) on files with a write channel (same rule as above) are routed to those files' refine and the set is validated again, up to `crossFileMaxIterations` (default 2) rounds. The stage ends when no routable error remains, a round lands no edit, the validator reports `not-run` or `errored`, the budget is spent, or the signal aborts; the last outcome is what the report carries, and findings on files without a write channel stay in it. When any round landed an edit, the parser report is refreshed with one more detection so `checks` and parser findings describe the final content.
517
518
 
518
519
  Whatever the fix phase cannot fix stays in the findings, so the verdict always reflects the post-fix state. Two invariants hold throughout: a fix that reintroduces conflict markers is discarded rather than written, and a round that leaves *more* error findings than it started with is discarded too — the better content is kept, so the phase can never ship a file worse than the one it was handed.
519
520
 
@@ -626,6 +627,7 @@ score(resolution, goldenFile);
626
627
  | `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" |
627
628
  | `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` |
628
629
  | `fix:budget-exhausted` | The global `maxModelCalls` budget was spent (`spent`, `limit`); the fix phase stopped and left the remainder in findings |
630
+ | `fix:set-validation-round` | A round of whole-set validator routing started (`round`, 1-based; `findingCount` of the set call; `routedFiles`) — emitted once per round that routes at least one file |
629
631
  | `validation:failed` | The consumer validator threw; reported and swallowed, resolution kept (`scope`: `file` or `set`, `filePath?` — absent for the set call, `reason`) |
630
632
  | `validation:not-run` | The consumer validator answered `not-run` (`scope`, `filePath?` — absent for the set call, `reason`) |
631
633
 
@@ -1,2 +1,2 @@
1
1
  import{buildSnapshots as e}from"../reference-integrity/snapshot.js";import{collectFileFindings as t,detect as n}from"../reference-integrity/detect.js";import{checkResidualMarkers as r}from"../validation/markers.js";import{matchFileGuidance as i}from"./file-guidance.js";import{runRefinePass as a}from"./refine-pass.js";import{CONFIDENCE_CAPS as o}from"./resolve-conflict.js";import{validateResolvedSet as s,withSetValidation as c}from"../reference-integrity/validate-set.js";async function l(e,n,s){let c=s.config?.fixLoopMaxIterations??3,l={parser:s.parser,validator:s.validator,signal:s.signal,onProgress:s.onProgress},g={content:e.content,findings:await(r=>t(e.filePath,n,r,l))(e.content)};g.findings.preExisting.length>0&&s.onProgress?.({type:`findings:pre-existing`,filePath:e.filePath,findings:g.findings.preExisting});let _=f(e,g.findings),v=d(g.findings);if(v.length===0)return m(s,e.filePath,0,0),p(e,_,g.findings);if(s.budget?.exhausted())return s.onBudgetExhausted?.(),m(s,e.filePath,0,v.length),u(p(e,_,g.findings),v);s.onProgress?.({type:`fix:started`,filePath:e.filePath,findingCount:v.length});let x=[],S=0,C=y(v);for(;S<c&&v.length>0;){if(s.budget?.exhausted()){s.onBudgetExhausted?.();break}S++;let o=await a(g.content,e.description,h(v),{model:s.model,signal:s.signal,temperature:s.config?.temperature,refineMaxSteps:s.config?.refineMaxSteps,modelCallTimeoutMs:s.config?.modelCallTimeoutMs,onProgress:s.onProgress,filePath:e.filePath,fileGuidance:i(s.fileGuidance,[e.filePath,...e.retiredPaths??[]]),budget:s.budget});if(o.edits.length===0)break;if(!r(e.filePath,o.content).valid){s.onProgress?.({type:`refine:discarded`,filePath:e.filePath,reason:`Fix edits introduced conflict markers.`});break}let c=await t(e.filePath,n,o.content,l),u=d(c);if(b(g.findings,c)){s.onProgress?.({type:`refine:discarded`,filePath:e.filePath,reason:`Fix edits regressed the file (was ${v.length} error(s), now ${u.length}).`});break}g={content:o.content,findings:c},x.push(...o.edits),v=u;let f=y(v);if(f===C)break;C=f}m(s,e.filePath,S,v.length);let w=p({...e,content:g.content},_,g.findings),T=d(g.findings);return x.length===0?u(w,T):u({...w,strategy:`ai`,edits:[...e.edits??[],...x],confidence:Math.min(w.confidence,o.refineApplied)},T)}function u(e,t){let n=t.filter(e=>e.severity===`error`);if(n.length===0)return e;let r=[...n].sort((e,t)=>e.location.line-t.location.line).slice(0,3).map(e=>`${e.category} at line ${e.location.line}`).join(`, `),i=n.length>3?`, and ${n.length-3} more`:``;return{...e,confidence:Math.min(e.confidence,o.errorsRemain),description:`Unresolved: ${n.length} error(s) remain after the fix pass (${r}${i}); fix them by hand before committing. ${e.description}`}}function d(e){return[...e.parser,...e.validator,...e.text].filter(e=>e.severity===`error`||e.category===`duplicate-block`)}function f(e,t){let n=(e,t)=>e.category===t.category&&e.location.file===t.location.file&&e.location.line===t.location.line&&e.location.symbol===t.location.symbol&&e.message===t.message;return(e.findings??[]).filter(e=>e.category!==`duplicate-block`&&![...t.validator,...t.preExisting].some(t=>n(e,t)))}function p(e,t,n){let r=[...t,...n.text,...n.validator];if(r.length===0){if(!e.findings)return e;let{findings:t,...n}=e;return n}return{...e,findings:r}}function m(e,t,n,r){e.onProgress?.({type:`fix:completed`,filePath:t,iterations:n,remainingFindings:r})}function h(e){return e.slice(0,20).map(g)}function g(e){let t=e.location.symbol?_(e.location.symbol):void 0,n=t?`"${t}" (line ${e.location.line})`:`line ${e.location.line}`;return`${_(e.category)} at ${n}: ${_(e.message)}`}function _(e){return e.replace(/[\p{Cc}\p{Cf}]/gu,` `).replace(/\s+/g,` `).trim().slice(0,300)}function v(e){return JSON.stringify([e.category,e.location.file,e.location.symbol??``])}function y(e){return e.map(v).sort().join(`
2
- `)}function b(e,t){let n=e=>[...e.parser,...e.validator,...e.text].filter(e=>e.severity===`error`||e.category===`syntax-error`||e.category===`duplicate-block`),r=n(e),i=n(t);if(i.length>r.length)return!0;let a=new Set(r.map(v));return i.some(e=>!a.has(v(e)))}async function x(t,r,i){let a=i.config?.maxDetectionScopeFiles,o={git:i.git,parser:i.parser,signal:i.signal,excludePaths:i.excludePaths,maxScopeFiles:a},l=await e(t,r,{git:i.git,signal:i.signal,excludePaths:i.excludePaths,maxScopeFiles:a}),u=()=>i.onProgress?.({type:`fix:budget-exhausted`,spent:i.budget?.spent()??0,limit:i.budget?.limit()??0}),d={model:i.model,fileGuidance:r.fileGuidance,parser:i.parser,validator:i.validator,config:i.config,onProgress:i.onProgress,signal:i.signal,budget:i.budget,onBudgetExhausted:()=>i.budget?.notifyExhaustedOnce(u)};return await C(t,l,d),c(l?await w(t,r,l,o,d,i):await n(t,r,o),await s(t,l,{validator:i.validator,signal:i.signal,onProgress:i.onProgress}))}function S(e){return e.strategy===`ai`?!0:e.strategy===`take-ours`||e.strategy===`take-theirs`?!e.fallback&&!e.deliberateTake:!1}async function C(e,t,n){for(let r=0;r<e.length;r++){let i=e[r];if(S(i)){if(n.signal?.aborted)return;try{e[r]=await l(i,T(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 w(e,t,r,i,a,o){let s=o.config?.crossFileMaxIterations??2,c=await n(e,t,i);for(let l=0;l<s&&!o.signal?.aborted;l++){if(o.budget?.exhausted()){a.onBudgetExhausted?.();break}let s=D(c.findings,e);if(s.size===0)break;let l=!1;for(let[t,n]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 E(s,n,T(r,t),r.result,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 n(e,t,i)}return c}function T(e,t){return{ours:e?.ours.get(t),theirs:e?.theirs.get(t)}}async function E(e,n,s,c,l){let u={parser:l.parser,validator:l.validator,signal:l.signal,onProgress:l.onProgress},d=await t(e.filePath,s,e.content,u),m=l.parser?await O(n,c,l.parser,l.signal):void 0,g=await a(e.content,e.description,h(n),{model:l.model,signal:l.signal,temperature:l.config?.temperature,refineMaxSteps:l.config?.refineMaxSteps,modelCallTimeoutMs:l.config?.modelCallTimeoutMs,onProgress:l.onProgress,filePath:e.filePath,fileGuidance:i(l.fileGuidance,[e.filePath,...e.retiredPaths??[]]),budget:l.budget,relatedDefinitions:m});if(g.edits.length===0)return e;if(!r(e.filePath,g.content).valid)return l.onProgress?.({type:`refine:discarded`,filePath:e.filePath,reason:`Cross-file fix introduced conflict markers.`}),e;let _=await t(e.filePath,s,g.content,u);return b(d,_)?(l.onProgress?.({type:`refine:discarded`,filePath:e.filePath,reason:`Cross-file fix left the file with a new or additional error.`}),e):p({...e,content:g.content,strategy:`ai`,edits:[...e.edits??[],...g.edits],confidence:Math.min(e.confidence,o.refineApplied)},f(e,d),_)}function D(e,t){let n=new Set(t.filter(S).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}async function O(e,t,n,r){let i=new Set;for(let n of e){let e=n.definitionFile;e&&t.has(e)&&i.add(e)}if(i.size===0)return;let a=[];for(let e of i){let i=t.get(e);if(i==null)continue;let o=(await n.analyze(new Map([[e,i]]),r)).get(e);if(!o||o.definitions.length===0)continue;let s=o.definitions.slice(0,50).map(e=>{let t=_(e.name),n=e.kind?_(e.kind):void 0;return n?`${t} (${n}, line ${e.line})`:`${t} (line ${e.line})`});a.push({file:_(e),symbols:s})}return a.length>0?a:void 0}export{S as isFixable,T as parentsOf,l as runFixLoop,x as runFixPhase};
2
+ `)}function b(e,t){let n=e=>[...e.parser,...e.validator,...e.text].filter(e=>e.severity===`error`||e.category===`syntax-error`||e.category===`duplicate-block`),r=n(e),i=n(t);if(i.length>r.length)return!0;let a=new Set(r.map(v));return i.some(e=>!a.has(v(e)))}async function x(t,r,i){let a=i.config?.maxDetectionScopeFiles,o={git:i.git,parser:i.parser,signal:i.signal,excludePaths:i.excludePaths,maxScopeFiles:a},s=await e(t,r,{git:i.git,signal:i.signal,excludePaths:i.excludePaths,maxScopeFiles:a}),l=()=>i.onProgress?.({type:`fix:budget-exhausted`,spent:i.budget?.spent()??0,limit:i.budget?.limit()??0}),u={model:i.model,fileGuidance:r.fileGuidance,parser:i.parser,validator:i.validator,config:i.config,onProgress:i.onProgress,signal:i.signal,budget:i.budget,onBudgetExhausted:()=>i.budget?.notifyExhaustedOnce(l)};await C(t,s,u);let d=s?await w(t,r,s,o,u,i):await n(t,r,o),{set:f,changed:p}=await T(t,s,u,i);return p&&(d=await n(t,r,o)),c(d,f)}function S(e){return e.strategy===`ai`?!0:e.strategy===`take-ours`||e.strategy===`take-theirs`?!e.fallback&&!e.deliberateTake:!1}async function C(e,t,n){for(let r=0;r<e.length;r++){let i=e[r];if(S(i)){if(n.signal?.aborted)return;try{e[r]=await l(i,E(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 w(e,t,r,i,a,o){let s=o.config?.crossFileMaxIterations??2,c=await n(e,t,i);for(let l=0;l<s&&!o.signal?.aborted;l++){if(o.budget?.exhausted()){a.onBudgetExhausted?.();break}let s=O(c.findings,e,`project`);if(s.size===0)break;let l=!1;for(let[t,n]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 D(s,n,E(r,t),r.result,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 n(e,t,i)}return c}async function T(e,t,n,r){let i={validator:r.validator,signal:r.signal,onProgress:r.onProgress},a=r.config?.crossFileMaxIterations??2,o=await s(e,t,i),c=!1;for(let l=0;l<a&&!(o.status!==`ran`||r.signal?.aborted);l++){if(r.budget?.exhausted()){n.onBudgetExhausted?.();break}let a=O(o.findings,e);if(a.size===0)break;r.onProgress?.({type:`fix:set-validation-round`,round:l+1,findingCount:o.findings.length,routedFiles:a.size});let u=!1;for(let[i,o]of a){if(r.budget?.exhausted()){n.onBudgetExhausted?.();break}let a=e.findIndex(e=>e.filePath===i);if(a<0)continue;let s=e[a];try{let r=await D(s,o,E(t,i),t?.result??new Map,n);e[a]=r,r.content!==s.content&&(u=!0)}catch(e){if(r.signal?.aborted)throw e;r.onProgress?.({type:`fix:failed`,filePath:i,reason:e instanceof Error?e.message:String(e)})}}if(!u)break;c=!0,o=await s(e,t,i)}return{set:o,changed:c}}function E(e,t){return{ours:e?.ours.get(t),theirs:e?.theirs.get(t)}}async function D(e,n,s,c,l){let u={parser:l.parser,validator:l.validator,signal:l.signal,onProgress:l.onProgress},d=await t(e.filePath,s,e.content,u),m=l.parser?await k(n,c,l.parser,l.signal):void 0,g=await a(e.content,e.description,h(n),{model:l.model,signal:l.signal,temperature:l.config?.temperature,refineMaxSteps:l.config?.refineMaxSteps,modelCallTimeoutMs:l.config?.modelCallTimeoutMs,onProgress:l.onProgress,filePath:e.filePath,fileGuidance:i(l.fileGuidance,[e.filePath,...e.retiredPaths??[]]),budget:l.budget,relatedDefinitions:m});if(g.edits.length===0)return e;if(!r(e.filePath,g.content).valid)return l.onProgress?.({type:`refine:discarded`,filePath:e.filePath,reason:`Cross-file fix introduced conflict markers.`}),e;let _=await t(e.filePath,s,g.content,u);return b(d,_)?(l.onProgress?.({type:`refine:discarded`,filePath:e.filePath,reason:`Cross-file fix left the file with a new or additional error.`}),e):p({...e,content:g.content,strategy:`ai`,edits:[...e.edits??[],...g.edits],confidence:Math.min(e.confidence,o.refineApplied)},f(e,d),_)}function O(e,t,n){let r=new Set(t.filter(S).map(e=>e.filePath)),i=new Map;for(let t of e){if(n&&t.tier!==n||t.severity!==`error`||!r.has(t.location.file))continue;let e=i.get(t.location.file)??[];e.push(t),i.set(t.location.file,e)}return i}async function k(e,t,n,r){let i=new Set;for(let n of e){let e=n.definitionFile;e&&t.has(e)&&i.add(e)}if(i.size===0)return;let a=[];for(let e of i){let i=t.get(e);if(i==null)continue;let o=(await n.analyze(new Map([[e,i]]),r)).get(e);if(!o||o.definitions.length===0)continue;let s=o.definitions.slice(0,50).map(e=>{let t=_(e.name),n=e.kind?_(e.kind):void 0;return n?`${t} (${n}, line ${e.line})`:`${t} (line ${e.line})`});a.push({file:_(e),symbols:s})}return a.length>0?a:void 0}export{S as isFixable,E as parentsOf,l as runFixLoop,x as runFixPhase};
@@ -154,6 +154,11 @@ type ConflictProgressEvent = {
154
154
  type: 'fix:budget-exhausted';
155
155
  spent: number;
156
156
  limit: number;
157
+ } | {
158
+ type: 'fix:set-validation-round';
159
+ round: number;
160
+ findingCount: number;
161
+ routedFiles: number;
157
162
  } | {
158
163
  type: 'validation:failed';
159
164
  filePath?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitkraken/conflict-tools",
3
- "version": "0.7.1-beta.12",
3
+ "version": "0.7.1-beta.13",
4
4
  "description": "AI-powered git conflict resolution.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Axosoft, LLC dba GitKraken",