@gitkraken/conflict-tools 0.2.4-beta.1 → 0.2.4-beta.10

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
@@ -10,6 +10,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
10
10
  ### Added
11
11
 
12
12
  - 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
+ - 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
+ - `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.
15
+ - Binding-aware broken-reference detection. `ParsedFile` gains an optional `bindings` (names bound at any scope, module-level and nested), and reference-integrity now treats a name as "resolvable before the merge" if a parent resolved it as a reference **or** defined/bound it. This catches a reference broken by removing a nested binding (e.g. a `const` used by a closure elsewhere in the same file): the dangling use was never a resolved reference — because it was locally bound — so the before/after reference diff alone could not see the regression. Backends that only surface top-level `definitions` can omit `bindings`; they simply do not gain the nested-binding case.
16
+ - Operation-level assessment: `buildOperationDigest`/`summarizeOperation` now fold `StepResult.detection` into the rollup. A file with an error-severity finding (broken-reference, dropped-side, syntax-error, duplicate-definition) is added to `needsReview` (`reason: 'finding'`) regardless of confidence — even a file with no resolution of its own; `OperationDigest.detectionComplete` reports whether every detection check ran (so empty findings on a skipped check are not read as clean); and a derived `OperationDigest.verdict` (`'clean' | 'needs-review' | 'blocked'`) gives consumers an honest gate signal. `ReviewItem` gains `reason` and optional `findingCategories`, and its `confidence` is now optional.
17
+
18
+ ### Changed
19
+
20
+ - Reference-integrity findings are now tiered by locality: an intra-file break — a local definition/binding the merge dropped while a use of it remains in the same file (e.g. a `const` used by a closure below) — is a `tier: 'file'` finding; a broken cross-file reference (an import whose target stopped resolving) stays `tier: 'project'`. Previously every `broken-reference` was `'project'`. Classification uses the reference's `target` file when the parser supplies it, else whether the name was a local definition/binding in the parent. This sets up the per-file vs cross-file fix loops.
21
+ - **Breaking**: consumer validation moved to a `Validator` port — `validate(sources: SourceOverlay): Promise<ValidatorFinding[]>` (replaces the per-file `ResolutionVerifier.verify`). It runs post-refine, non-blocking, over an overlay (one file for per-file validation, or the touched set for cross-file); its findings fold into the operation's detection findings and verdict. `Resolution.findings` replaces `Resolution.issues`. `Finding.category` is now an open `string` so a consumer validator can contribute its own categories — the package still emits the well-known `FindingCategory` values. `defaultVerifier` / `VerificationResult` are no longer exported (`checkResidualMarkers` stays internal, the built-in marker gate).
22
+ - **Breaking**: the resolver's in-loop blocking check is now **residual conflict markers only** (built-in, language-agnostic). The `ResolverDeps.verifier` port no longer gates the resolve loop or triggers re-resolution — it runs once **after** refine, **non-blocking**, and its issues are attached to `Resolution.issues` (advisory) and reported. This stops the resolver from burning its step budget on non-marker validation (which was exhausting into a whole-file `take-theirs` fallback), and puts consumer validation in its intended post-refine, report-only role. `ResolverLoopResult` no longer carries `issues`.
23
+
24
+ ### Fixed
25
+
26
+ - 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
+ - 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
+ - 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.
29
+ - Reference-integrity detection no longer reads as "clean" when the parent snapshots could not be loaded. A git port that returns empty content for an unreadable parent (e.g. a `showFile` that swallows read errors into `""` instead of throwing) previously produced empty findings with `referenceIntegrity: 'ran'`, masking a total read failure as a clean result. `buildSnapshots` now ignores empty parent content; `DetectionReport.scope` reports `readable` (parents actually loaded) alongside `analyzed` (files in scope); and `referenceIntegrity` reports a new `'skipped-unreadable-source'` status when nothing in scope could be read — which flips `OperationDigest.detectionComplete` to `false` and the verdict to `needs-review` instead of `clean`.
13
30
 
14
31
  ## [0.2.3] - 2026-07-06
15
32
 
@@ -8,6 +8,7 @@ declare function mergeBase(git: ConflictGitPort, a: string, b: string, opts?: Op
8
8
  declare function blame(git: ConflictGitPort, path: string, opts?: BlameOptions): Promise<string>;
9
9
  declare function grep(git: ConflictGitPort, pattern: string, opts?: GrepOptions): Promise<string>;
10
10
  declare function diff(git: ConflictGitPort, from: string, to: string, opts?: ConflictDiffOptions): Promise<string>;
11
+ declare function changedFiles(git: ConflictGitPort, from: string, to: string, opts?: OpOptions): Promise<string[]>;
11
12
  declare function log(git: ConflictGitPort, opts?: LogOptions): Promise<string>;
12
13
  declare function show(git: ConflictGitPort, sha: string, opts?: ShowOptions): Promise<string>;
13
14
  declare function writeFile(git: ConflictGitPort, path: string, content: string, opts?: OpOptions): Promise<void>;
@@ -15,4 +16,4 @@ declare function stageFiles(git: ConflictGitPort, paths: string[], opts?: OpOpti
15
16
  declare function checkoutFile(git: ConflictGitPort, path: string, side: 'ours' | 'theirs', opts?: OpOptions): Promise<void>;
16
17
  declare function removeFile(git: ConflictGitPort, path: string, opts?: OpOptions): Promise<void>;
17
18
  //#endregion
18
- export { blame, checkoutFile, diff, grep, log, mergeBase, readFile, removeFile, show, showFile, stageFiles, unmergedEntries, writeFile };
19
+ export { blame, changedFiles, checkoutFile, diff, grep, log, mergeBase, readFile, removeFile, show, showFile, stageFiles, unmergedEntries, writeFile };
@@ -1,2 +1,3 @@
1
1
  import{ConflictGitPortMissingOpError as e}from"../ports/git.js";import{sliceLines as t,truncateLines as n,truncateMatches as r}from"./truncation.js";const i={DD:`both-deleted`,AU:`added-by-us`,UD:`deleted-by-them`,UA:`added-by-them`,DU:`deleted-by-us`,AA:`both-added`,UU:`both-modified`};function a(e){return i[e]??e}function o(e){return`Search capped at ${e} matches. There may be more — narrow with a more specific pattern or path.`}function s(t,n){throw new e(t,n)}function c(e,t){return e!=null&&t!=null?`startLine=${e}, endLine=${t}`:e==null?`endLine=${t}`:`startLine=${e}`}async function l(e,t,n){return e.readFile?e.readFile(t,n):s(`readFile`)}async function u(e,r,i,a){let o;if(e.showFile)o=await e.showFile(r,i,a);else if(e.exec){let n=t(await e.exec([`show`,`${r}:${i}`],{signal:a?.signal}),a?.startLine,a?.endLine);if(!n.ok)return n.reason===`invalid-range`?`[Invalid range: ${c(n.startLine,n.endLine)} (startLine must be <= endLine).]`:`[Range out of bounds: file has ${n.total} lines, requested ${c(n.startLine,n.endLine)}.]`;o=n.content}else return s(`showFile`);return n(o,1e3,`Output capped at 1000 lines. Use startLine/endLine to read other regions of the file.`)}async function d(e,t){if(e.unmergedEntries)return e.unmergedEntries(t);if(e.exec){let n=await e.exec([`status`,`--porcelain=v2`],{signal:t?.signal}),r=[];for(let e of n.split(`
2
- `)){if(!e.startsWith(`u `))continue;let t=e.split(` `),n=t[1]??``,i=t.slice(10).join(` `);r.push({path:i,reason:a(n)})}return r}return s(`unmergedEntries`)}async function f(e,t,n,r){return e.mergeBase?e.mergeBase(t,n,r):e.exec?(await e.exec([`merge-base`,`--`,t,n],{signal:r?.signal})).trim():s(`mergeBase`)}async function p(e,t,r){let i=r?.startLine,a=r?.endLine,o=Number.isFinite(i),l=Number.isFinite(a);if(o&&l&&i>a)return`[Invalid range: ${c(i,a)} (startLine must be <= endLine).]`;let u;if(e.blame)u=await e.blame(t,r);else if(e.exec){let n=[`blame`,`--porcelain`],s=o?Math.max(1,i):void 0,c=l?Math.max(1,a):void 0;s!=null&&c!=null?n.push(`-L`,`${s},${c}`):s==null?c!=null&&n.push(`-L`,`1,${c}`):n.push(`-L`,`${s},`),r?.ref&&n.push(r.ref),n.push(`--`,t),u=await e.exec(n,{signal:r?.signal})}else return s(`blame`);return n(u,1e3,`Output capped at 1000 lines. Use startLine/endLine to inspect other regions.`)}async function m(e,t,n){let i=n?.maxResults,a=i!=null&&Number.isFinite(i)?Math.min(Math.max(1,i),100):100,c;if(e.grep)c=await e.grep(t,n);else if(e.exec){let r=[`grep`,`-n`,`--`,t];n?.ref&&r.push(n.ref),c=await e.exec(r,{signal:n?.signal})}else return s(`grep`);return r(c,a,o(a))}async function h(e,t,n,r){if(e.diff)return e.diff(t,n,r);if(e.exec){let i=[`diff`,t,n];return r?.path&&i.push(`--`,r.path),e.exec(i,{signal:r?.signal})}return s(`diff`)}async function g(e,t){let r=t?.maxCount,i=r!=null&&Number.isFinite(r)?Math.min(Math.max(1,r),100):100,a;if(e.log)a=await e.log({...t,maxCount:i});else if(e.exec){let n=[`log`,`--oneline`,`-n`,String(i)];t?.ref&&n.push(t.ref),t?.path&&n.push(`--`,t.path),a=await e.exec(n,{signal:t?.signal})}else return s(`log`);return n(a,100,`Output capped at 100 commits. Use path to scope to a file or refine the ref range.`)}async function _(e,t,r){let i;if(e.show)i=await e.show(t,r);else if(e.exec){let n=[`show`,t];r?.path&&n.push(`--`,r.path),i=await e.exec(n,{signal:r?.signal})}else return s(`show`);return n(i,500,`Output capped at 500 lines. Use show_file_at_ref with startLine/endLine to read specific regions.`)}async function v(e,t,n,r){return e.writeFile?e.writeFile(t,n,r):s(`writeFile`)}async function y(e,t,n){if(e.stageFiles)return e.stageFiles(t,n);if(e.exec){await e.exec([`add`,`--`,...t],{signal:n?.signal});return}return s(`stageFiles`)}async function b(e,t,n,r){if(e.checkoutFile)return e.checkoutFile(t,n,r);if(e.exec){await e.exec([`checkout`,`--${n}`,`--`,t],{signal:r?.signal});return}return s(`checkoutFile`)}async function x(e,t,n){if(e.removeFile)return e.removeFile(t,n);if(e.exec){await e.exec([`rm`,`--`,t],{signal:n?.signal});return}return s(`removeFile`)}export{p as blame,b as checkoutFile,h as diff,m as grep,g as log,f as mergeBase,l as readFile,x as removeFile,_ as show,u as showFile,y as stageFiles,d as unmergedEntries,v as writeFile};
2
+ `)){if(!e.startsWith(`u `))continue;let t=e.split(` `),n=t[1]??``,i=t.slice(10).join(` `);r.push({path:i,reason:a(n)})}return r}return s(`unmergedEntries`)}async function f(e,t,n,r){return e.mergeBase?e.mergeBase(t,n,r):e.exec?(await e.exec([`merge-base`,`--`,t,n],{signal:r?.signal})).trim():s(`mergeBase`)}async function p(e,t,r){let i=r?.startLine,a=r?.endLine,o=Number.isFinite(i),l=Number.isFinite(a);if(o&&l&&i>a)return`[Invalid range: ${c(i,a)} (startLine must be <= endLine).]`;let u;if(e.blame)u=await e.blame(t,r);else if(e.exec){let n=[`blame`,`--porcelain`],s=o?Math.max(1,i):void 0,c=l?Math.max(1,a):void 0;s!=null&&c!=null?n.push(`-L`,`${s},${c}`):s==null?c!=null&&n.push(`-L`,`1,${c}`):n.push(`-L`,`${s},`),r?.ref&&n.push(r.ref),n.push(`--`,t),u=await e.exec(n,{signal:r?.signal})}else return s(`blame`);return n(u,1e3,`Output capped at 1000 lines. Use startLine/endLine to inspect other regions.`)}async function m(e,t,n){let i=n?.maxResults,a=i!=null&&Number.isFinite(i)?Math.min(Math.max(1,i),100):100,c;if(e.grep)c=await e.grep(t,n);else if(e.exec){let r=[`grep`,`-n`,`--`,t];n?.ref&&r.push(n.ref),c=await e.exec(r,{signal:n?.signal})}else return s(`grep`);return r(c,a,o(a))}async function h(e,t,n,r){if(e.diff)return e.diff(t,n,r);if(e.exec){let i=[`diff`,t,n];return r?.path&&i.push(`--`,r.path),e.exec(i,{signal:r?.signal})}return s(`diff`)}async function g(e,t,n,r){return e.changedFiles?e.changedFiles(t,n,r):e.exec?(await e.exec([`diff`,`--name-only`,t,n],{signal:r?.signal})).split(`
3
+ `).map(e=>e.trim()).filter(e=>e.length>0):s(`changedFiles`)}async function _(e,t){let r=t?.maxCount,i=r!=null&&Number.isFinite(r)?Math.min(Math.max(1,r),100):100,a;if(e.log)a=await e.log({...t,maxCount:i});else if(e.exec){let n=[`log`,`--oneline`,`-n`,String(i)];t?.ref&&n.push(t.ref),t?.path&&n.push(`--`,t.path),a=await e.exec(n,{signal:t?.signal})}else return s(`log`);return n(a,100,`Output capped at 100 commits. Use path to scope to a file or refine the ref range.`)}async function v(e,t,r){let i;if(e.show)i=await e.show(t,r);else if(e.exec){let n=[`show`,t];r?.path&&n.push(`--`,r.path),i=await e.exec(n,{signal:r?.signal})}else return s(`show`);return n(i,500,`Output capped at 500 lines. Use show_file_at_ref with startLine/endLine to read specific regions.`)}async function y(e,t,n,r){return e.writeFile?e.writeFile(t,n,r):s(`writeFile`)}async function b(e,t,n){if(e.stageFiles)return e.stageFiles(t,n);if(e.exec){await e.exec([`add`,`--`,...t],{signal:n?.signal});return}return s(`stageFiles`)}async function x(e,t,n,r){if(e.checkoutFile)return e.checkoutFile(t,n,r);if(e.exec){await e.exec([`checkout`,`--${n}`,`--`,t],{signal:r?.signal});return}return s(`checkoutFile`)}async function S(e,t,n){if(e.removeFile)return e.removeFile(t,n);if(e.exec){await e.exec([`rm`,`--`,t],{signal:n?.signal});return}return s(`removeFile`)}export{p as blame,g as changedFiles,x as checkoutFile,h as diff,m as grep,_ as log,f as mergeBase,l as readFile,S as removeFile,v as show,u as showFile,b as stageFiles,d as unmergedEntries,y as writeFile};
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { AIError, AIErrorCode } from "./errors/ai.js";
2
2
  import { ConflictError, ConflictErrorCode } from "./errors/conflict.js";
3
3
  import { BlameOptions, ConflictDiffOptions, ConflictGitOps, ConflictGitPort, ConflictGitPortMissingOpError, GrepOptions, LogOptions, OpOptions, ShowFileOptions, ShowOptions, UnmergedEntry, UnmergedReason } from "./ports/git.js";
4
+ import { CheckStatus, DetectionReport, Finding, FindingCategory, FindingTier } from "./types/findings.js";
4
5
  import { IntraFileEditOp } from "./types/hints.js";
5
- import { Issue } from "./types/issue.js";
6
6
  import { DiffStats, Resolution, ResolutionContext, ResolutionMetrics, ResolutionRawSignals, ResolutionRefs, ResolutionStrategy, ResolvedChunk, ThreeWayDiff } from "./types/resolution.js";
7
7
  import { Conflict, ConflictMarker, ConflictMarkerUserInput, ConflictType } from "./types/conflict.js";
8
8
  import { extractConflict } from "./extraction/extract.js";
@@ -10,8 +10,8 @@ import { checkoutFile, removeFile, show } from "./git/port-dispatch.js";
10
10
  import { ToolCall, ToolDefinition, ToolResult } from "./types/tool.js";
11
11
  import { ConflictModelMessage, ConflictModelParams, ConflictModelPort, ConflictModelResult, ResolverConfig } from "./ports/model.js";
12
12
  import { ParsedFile, ParserPort, SourceOverlay, SymbolDefinition, SymbolReference, SyntaxIssue } from "./ports/parser.js";
13
- import { ResolutionVerifier, VerificationResult, defaultVerifier } from "./ports/verify.js";
14
- import { CheckStatus, DetectionReport, Finding, FindingCategory, FindingTier } from "./types/findings.js";
13
+ import { Issue } from "./types/issue.js";
14
+ import { Validator, ValidatorFinding } from "./ports/verify.js";
15
15
  import { DetectDeps, detect } from "./reference-integrity/detect.js";
16
16
  import { createResolution } from "./resolution/create-resolution.js";
17
17
  import { takeOurs, takeTheirs } from "./resolution/take-strategy.js";
@@ -20,6 +20,6 @@ import { ConflictProgressEvent } from "./types/events.js";
20
20
  import { resolveConflict } from "./resolver/resolve-conflict.js";
21
21
  import { FileRule, StepConfig, StepResult } from "./types/step.js";
22
22
  import { resolveConflicts } from "./resolver/resolve-conflicts.js";
23
- import { OperationDigest, OperationSummary, ReviewItem } from "./types/summary.js";
23
+ import { OperationDigest, OperationSummary, OperationVerdict, ReviewItem } from "./types/summary.js";
24
24
  import { SummarizeOperationOptions, buildOperationDigest, summarizeOperation } from "./resolver/summarize-operation.js";
25
- export { AIError, type AIErrorCode, type BlameOptions, type CheckStatus, type Conflict, type ConflictDiffOptions, ConflictError, type ConflictErrorCode, type ConflictGitOps, type ConflictGitPort, ConflictGitPortMissingOpError, type ConflictMarker, type ConflictMarkerUserInput, type ConflictModelMessage, type ConflictModelParams, type ConflictModelPort, type ConflictModelResult, type ConflictProgressEvent, type ConflictType, type DetectDeps, type DetectionReport, type DiffStats, type FileRule, type Finding, type FindingCategory, type FindingTier, type GrepOptions, type IntraFileEditOp, type Issue, type LogOptions, type OpOptions, type OperationDigest, type OperationSummary, type ParsedFile, type ParserPort, type Resolution, type ResolutionContext, type ResolutionMetrics, type ResolutionRawSignals, type ResolutionRefs, type ResolutionStrategy, type ResolutionVerifier, type ResolvedChunk, type ResolverConfig, type ReviewItem, type ShowFileOptions, type ShowOptions, type SourceOverlay, type StepConfig, type StepResult, type SummarizeOperationOptions, type SymbolDefinition, type SymbolReference, type SyntaxIssue, type ThreeWayDiff, type ToolCall, type ToolDefinition, type ToolResult, type UnmergedEntry, type UnmergedReason, type VerificationResult, applyResolutions, buildOperationDigest, checkoutFile, createResolution, defaultVerifier, detect, extractConflict, removeFile, resolveConflict, resolveConflicts, show, summarizeOperation, takeOurs, takeTheirs };
25
+ export { AIError, type AIErrorCode, type BlameOptions, type CheckStatus, type Conflict, type ConflictDiffOptions, ConflictError, type ConflictErrorCode, type ConflictGitOps, type ConflictGitPort, ConflictGitPortMissingOpError, type ConflictMarker, type ConflictMarkerUserInput, type ConflictModelMessage, type ConflictModelParams, type ConflictModelPort, type ConflictModelResult, type ConflictProgressEvent, type ConflictType, type DetectDeps, type DetectionReport, type DiffStats, type FileRule, type Finding, type FindingCategory, type FindingTier, type GrepOptions, type IntraFileEditOp, type Issue, type LogOptions, type OpOptions, type OperationDigest, type OperationSummary, type OperationVerdict, type ParsedFile, type ParserPort, type Resolution, type ResolutionContext, type ResolutionMetrics, type ResolutionRawSignals, type ResolutionRefs, type ResolutionStrategy, type ResolvedChunk, type ResolverConfig, type ReviewItem, type ShowFileOptions, type ShowOptions, type SourceOverlay, type StepConfig, type StepResult, type SummarizeOperationOptions, type SymbolDefinition, type SymbolReference, type SyntaxIssue, type ThreeWayDiff, type ToolCall, type ToolDefinition, type ToolResult, type UnmergedEntry, type UnmergedReason, type Validator, type ValidatorFinding, applyResolutions, buildOperationDigest, checkoutFile, createResolution, detect, extractConflict, removeFile, resolveConflict, resolveConflicts, show, summarizeOperation, takeOurs, takeTheirs };
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import{AIError as e}from"./errors/ai.js";import{ConflictError as t}from"./errors/conflict.js";import{ConflictGitPortMissingOpError as n}from"./ports/git.js";import{checkoutFile as r,removeFile as i,show as a}from"./git/port-dispatch.js";import{extractConflict as o}from"./extraction/extract.js";import{defaultVerifier as s}from"./ports/verify.js";import{detect as c}from"./reference-integrity/detect.js";import{createResolution as l}from"./resolution/create-resolution.js";import{takeOurs as u,takeTheirs as d}from"./resolution/take-strategy.js";import{applyResolutions as f}from"./resolver/apply-resolutions.js";import{resolveConflict as p}from"./resolver/resolve-conflict.js";import{resolveConflicts as m}from"./resolver/resolve-conflicts.js";import{buildOperationDigest as h,summarizeOperation as g}from"./resolver/summarize-operation.js";export{e as AIError,t as ConflictError,n as ConflictGitPortMissingOpError,f as applyResolutions,h as buildOperationDigest,r as checkoutFile,l as createResolution,s as defaultVerifier,c as detect,o as extractConflict,i as removeFile,p as resolveConflict,m as resolveConflicts,a as show,g as summarizeOperation,u as takeOurs,d as takeTheirs};
1
+ import{AIError as e}from"./errors/ai.js";import{ConflictError as t}from"./errors/conflict.js";import{ConflictGitPortMissingOpError as n}from"./ports/git.js";import{checkoutFile as r,removeFile as i,show as a}from"./git/port-dispatch.js";import{extractConflict as o}from"./extraction/extract.js";import{detect as s}from"./reference-integrity/detect.js";import{createResolution as c}from"./resolution/create-resolution.js";import{takeOurs as l,takeTheirs as u}from"./resolution/take-strategy.js";import{applyResolutions as d}from"./resolver/apply-resolutions.js";import{resolveConflict as f}from"./resolver/resolve-conflict.js";import{resolveConflicts as p}from"./resolver/resolve-conflicts.js";import{buildOperationDigest as m,summarizeOperation as h}from"./resolver/summarize-operation.js";export{e as AIError,t as ConflictError,n as ConflictGitPortMissingOpError,d as applyResolutions,m as buildOperationDigest,r as checkoutFile,c as createResolution,s as detect,o as extractConflict,i as removeFile,f as resolveConflict,p as resolveConflicts,a as show,h as summarizeOperation,l as takeOurs,u as takeTheirs};
@@ -45,6 +45,11 @@ interface ConflictGitOps {
45
45
  showFile?: (ref: string, path: string, opts?: ShowFileOptions) => Promise<string>;
46
46
  unmergedEntries?: (opts?: OpOptions) => Promise<UnmergedEntry[]>;
47
47
  mergeBase?: (a: string, b: string, opts?: OpOptions) => Promise<string>;
48
+ /**
49
+ * Returns the paths changed between two refs (name-only). Used to discover the merge's
50
+ * incoming/auto-merged set for reference-integrity detection.
51
+ */
52
+ changedFiles?: (from: string, to: string, opts?: OpOptions) => Promise<string[]>;
48
53
  /**
49
54
  * Returns line-by-line authorship for `path`. When `opts.startLine`/`endLine` are provided,
50
55
  * implementations should restrict to that 1-indexed inclusive range. The library does not
@@ -32,6 +32,12 @@ interface ParsedFile {
32
32
  language: string;
33
33
  definitions: SymbolDefinition[];
34
34
  references: SymbolReference[];
35
+ /** Names bound anywhere in the file, at any scope (module-level and nested). Optional: a backend that
36
+ * only surfaces top-level `definitions` may omit it. Populating it lets the detector catch a
37
+ * reference broken by removing a *nested* binding — a use satisfied by a local binding is never
38
+ * itself reported as a resolved reference, so without the binding as its "before" the before/after
39
+ * diff has no transition to flag. */
40
+ bindings?: SymbolDefinition[];
35
41
  syntaxErrors: SyntaxIssue[];
36
42
  }
37
43
  /**
@@ -1,3 +1,4 @@
1
+ import { SourceOverlay } from "./parser.js";
1
2
  import { Issue } from "../types/issue.js";
2
3
 
3
4
  //#region src/ports/verify.d.ts
@@ -5,10 +6,25 @@ interface VerificationResult {
5
6
  valid: boolean;
6
7
  issues?: Issue[];
7
8
  }
8
- interface ResolutionVerifier {
9
- verify(filePath: string, content: string): Promise<VerificationResult>;
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[]>;
10
27
  }
11
28
  declare function checkResidualMarkers(_filePath: string, content: string): VerificationResult;
12
- declare const defaultVerifier: ResolutionVerifier;
13
29
  //#endregion
14
- export { ResolutionVerifier, VerificationResult, checkResidualMarkers, defaultVerifier };
30
+ export { Validator, ValidatorFinding, 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(`, `)}.`}]}}const n={async verify(e,n){return t(e,n)}};export{t as checkResidualMarkers,n as defaultVerifier};
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};
@@ -1,7 +1,7 @@
1
1
  import { ConflictGitPort } from "../ports/git.js";
2
+ import { DetectionReport } from "../types/findings.js";
2
3
  import { Resolution, ResolutionContext } from "../types/resolution.js";
3
4
  import { ParserPort } from "../ports/parser.js";
4
- import { DetectionReport } from "../types/findings.js";
5
5
 
6
6
  //#region src/reference-integrity/detect.d.ts
7
7
  interface DetectDeps {
@@ -1 +1,3 @@
1
- import{buildSnapshots as e}from"./snapshot.js";async function t(t,r,o){let s=o.parser;if(!s)return{findings:[],checks:{referenceIntegrity:`skipped-no-parser`,structural:`skipped-no-parser`}};try{let c=r.refs?await e(t,r,o):null;if(c){let[e,t,r]=await Promise.all([s.analyze(c.ours),s.analyze(c.theirs),s.analyze(c.result)]);return{findings:[...n(e,t,r),...i(r)],checks:{referenceIntegrity:a([e,t,r])?`partial-unsupported-language`:`ran`,structural:a([r])?`partial-unsupported-language`:`ran`}}}let l=new Map(t.map(e=>[e.filePath,e.content])),u=await s.analyze(l);return{findings:i(u),checks:{referenceIntegrity:`skipped-no-refs`,structural:a([u])?`partial-unsupported-language`:`ran`}}}catch{return{findings:[],checks:{referenceIntegrity:`errored`,structural:`errored`}}}}function n(e,t,n){let i=new Map;r(e,`ours`,i),r(t,`theirs`,i);let a=[];for(let[e,t]of n)for(let n of t.references){if(n.resolved)continue;let t=i.get(o(e,n.name));t&&a.push({category:`broken-reference`,severity:`error`,tier:`project`,location:{file:e,line:n.line,symbol:n.name},provenance:t,message:`Reference "${n.name}" resolved before the merge (on ${t.side}) but no longer resolves.`})}return a}function r(e,t,n){for(let[r,i]of e)for(let e of i.references){if(!e.resolved)continue;let i=o(r,e.name);n.has(i)||n.set(i,{side:t,line:e.line})}}function i(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 a(e){return e.some(e=>{for(let t of e.values())if(t.language===`unsupported`)return!0;return!1})}function o(e,t){return JSON.stringify([e,t])}export{t as detect};
1
+ import{buildSnapshots as e}from"./snapshot.js";async function t(t,r,o){let c=o.parser;if(!c)return{findings:[],checks:{referenceIntegrity:`skipped-no-parser`,structural:`skipped-no-parser`}};try{let l=r.detectionRefs??r.refs?await e(t,r,o):null;if(l){let[e,r,o]=await Promise.all([c.analyze(l.ours),c.analyze(l.theirs),c.analyze(l.result)]),u=l.analyzed>0&&l.readable===0?`skipped-unreadable-source`:s([e,r,o])?`partial-unsupported-language`:`ran`;return{findings:[...n(e,r,o),...i(o),...a(t,l)],checks:{referenceIntegrity:u,structural:s([o])?`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 c.analyze(u);return{findings:i(d),checks:{referenceIntegrity:`skipped-no-refs`,structural:s([d])?`partial-unsupported-language`:`ran`}}}catch{return{findings:[],checks:{referenceIntegrity:`errored`,structural:`errored`}}}}function n(e,t,n){let i=new Map;r(e,`ours`,i),r(t,`theirs`,i);let a=[];for(let[e,t]of n)for(let n of t.references){if(n.resolved)continue;let t=i.get(c(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 r(e,t,n){let r=(e,r,i,a)=>{let o=c(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 i(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 a(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=o(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 o(e,t){let n=new Set(t.split(`
2
+ `)),r=0;for(let t of e.split(`
3
+ `))t.trim()!==``&&!n.has(t)&&r++;return r}function s(e){return e.some(e=>{for(let t of e.values())if(t.language===`unsupported`)return!0;return!1})}function c(e,t){return JSON.stringify([e,t])}export{t as detect};
@@ -7,6 +7,13 @@ interface DetectionSnapshots {
7
7
  ours: SourceOverlay;
8
8
  theirs: SourceOverlay;
9
9
  result: SourceOverlay;
10
+ /** Number of files in scope (after the cap). */
11
+ analyzed: number;
12
+ /** How many scope files had loadable parent content on `ours` or `theirs`. When this is 0 the
13
+ * before/after comparison had nothing to read — a total read failure, not a clean result. */
14
+ readable: number;
15
+ /** Whether the scope exceeded the cap and was truncated. */
16
+ truncated: boolean;
10
17
  }
11
18
  interface SnapshotDeps {
12
19
  git: ConflictGitPort;
@@ -15,9 +22,10 @@ interface SnapshotDeps {
15
22
  /**
16
23
  * Builds the file-content overlays the detector compares: both pre-merge parents (read from git
17
24
  * objects — no working tree needed) and the resolved result (in-memory resolutions overlaid on the
18
- * surrounding files). Scope is the resolved files plus `context.fileNeighbors`. Returns `null` when
19
- * `context.refs` is absent — the before/after comparison is impossible without the parent refs. A
20
- * file missing at a parent (e.g. added by one side) is simply absent from that overlay.
25
+ * surrounding files). Scope is the resolved files plus the merge's incoming/auto-merged set, which
26
+ * the detector discovers itself via the git port (`changedFiles(base, theirs)`), unless the consumer
27
+ * supplies `context.touchedFiles` as an override. Returns `null` when `context.refs` is absent — the
28
+ * before/after comparison is impossible without the parent refs.
21
29
  */
22
30
  declare function buildSnapshots(resolutions: Resolution[], context: ResolutionContext, deps: SnapshotDeps): Promise<DetectionSnapshots | null>;
23
31
  //#endregion
@@ -1 +1 @@
1
- import{readFile as e,showFile as t}from"../git/port-dispatch.js";async function n(e,t,n){let a=t.refs;if(!a)return null;let o=new Map(e.map(e=>[e.filePath,e.content])),s=new Set([...o.keys(),...t.fileNeighbors??[]]),c=n.signal?{signal:n.signal}:void 0,l=new Map,u=new Map,d=new Map;for(let e of s){let t=await r(n.git,a.ours,e,c);t!=null&&l.set(e,t);let s=await r(n.git,a.theirs,e,c);s!=null&&u.set(e,s);let f=o.get(e);if(f!=null)d.set(e,f);else{let t=await i(n.git,e,c);t!=null&&d.set(e,t)}}return{ours:l,theirs:u,result:d}}async function r(e,n,r,i){try{return await t(e,n,r,i)}catch{return null}}async function i(t,n,r){try{return await e(t,n,r)}catch{return null}}export{n as buildSnapshots};
1
+ import{changedFiles as e,mergeBase as t,readFile as n,showFile as r}from"../git/port-dispatch.js";async function i(e,t,n){let r=t.detectionRefs??t.refs;if(!r)return null;let i=n.signal?{signal:n.signal}:void 0,c=new Map(e.map(e=>[e.filePath,e.content])),l=t.touchedFiles??await a(n.git,r,i),u=[],d=new Set;for(let e of[...c.keys(),...l,...t.fileNeighbors??[]])d.has(e)||(d.add(e),u.push(e));let f=u.length>400,p=f?u.slice(0,400):u,m=new Map,h=new Map,g=new Map,_=0;for(let e of p){let t=await o(n.git,r.ours,e,i);t&&m.set(e,t);let a=await o(n.git,r.theirs,e,i);a&&h.set(e,a),(t||a)&&_++;let l=c.get(e);if(l!=null)g.set(e,l);else{let t=await s(n.git,e,i);t!=null&&g.set(e,t)}}return{ours:m,theirs:h,result:g,analyzed:p.length,readable:_,truncated:f}}async function a(n,r,i){try{return await e(n,r.base??await t(n,r.ours,r.theirs,i),r.theirs,i)}catch{return[]}}async function o(e,t,n,i){try{return await r(e,t,n,i)}catch{return null}}async function s(e,t,r){try{return await n(e,t,r)}catch{return null}}export{i as buildSnapshots};
@@ -1,7 +1,7 @@
1
1
  import { ConflictGitPort } from "../ports/git.js";
2
2
  import { ConflictModelPort, ResolverConfig } from "../ports/model.js";
3
3
  import { ParserPort } from "../ports/parser.js";
4
- import { ResolutionVerifier } from "../ports/verify.js";
4
+ import { Validator } from "../ports/verify.js";
5
5
  import { ConflictProgressEvent } from "../types/events.js";
6
6
 
7
7
  //#region src/resolver/deps.d.ts
@@ -11,7 +11,7 @@ interface ResolverDeps<C extends ResolverConfig = ResolverConfig> {
11
11
  config?: C;
12
12
  onProgress?: (event: ConflictProgressEvent) => void;
13
13
  signal?: AbortSignal;
14
- verifier?: ResolutionVerifier;
14
+ validator?: Validator;
15
15
  parser?: ParserPort;
16
16
  }
17
17
  //#endregion
@@ -1,4 +1,3 @@
1
- import { Issue } from "../types/issue.js";
2
1
  import { ResolutionContext, ResolutionMetrics, ResolvedChunk } from "../types/resolution.js";
3
2
  import { Conflict } from "../types/conflict.js";
4
3
  import { ResolverDeps } from "./deps.js";
@@ -11,7 +10,6 @@ interface ResolverLoopResult {
11
10
  description: string;
12
11
  metrics: ResolutionMetrics;
13
12
  followUpInstructions?: string[];
14
- issues?: Issue[];
15
13
  }
16
14
  declare function runResolverLoop(conflict: Conflict, context: ResolutionContext, deps: ResolverDeps): Promise<ResolverLoopResult>;
17
15
  //#endregion
@@ -1 +1 @@
1
- import{AIError as e}from"../errors/ai.js";import{applyChunks as t}from"../resolution/chunk-apply.js";import{sumUsages as n}from"../resolution/metrics.js";import{parseModelResponse as r}from"./parse-response.js";import{buildPrompt as i}from"./prompt.js";import{createResolverTools as a}from"./tools.js";import{validateChunks as o}from"./validate.js";async function s(n,s,d){let f=d.config?.maxSteps??15,p=d.config?.temperature??0,{definitions:m,execute:h}=a(d.git),{system:g,userMessage:_}=i(n,s,{maxSteps:f}),v=[{role:`user`,content:_}],y=d.signal?{signal:d.signal}:void 0,b=0,x=0,S=0,C=[],w=n.markers.some(e=>e.userResolution!=null),T;for(;b<f;){b++;let e=await d.model.generate({system:g,messages:[...v],tools:m,temperature:p,signal:d.signal});if(e.usage&&(C.push(e.usage),d.onProgress?.({type:`resolver:step-usage`,filePath:n.filePath,stepNumber:b,usage:e.usage})),e.toolCalls&&e.toolCalls.length>0){v.push({role:`assistant`,content:e.text,toolCalls:e.toolCalls});for(let t of e.toolCalls){let e=typeof t.args.reason==`string`?t.args.reason:void 0;d.onProgress?.({type:`resolver:tool-call`,filePath:n.filePath,tool:t.name,args:t.args,stepNumber:b,reason:e}),x++;let r=await h(t,y);d.onProgress?.({type:`resolver:tool-result`,filePath:n.filePath,tool:t.name,stepNumber:b,content:r.content}),v.push({role:`tool`,toolCallId:r.toolCallId,toolName:t.name,content:r.content})}continue}let i=e.text??``;if(!i.trim()){S++,v.push({role:`assistant`,content:``}),v.push({role:`user`,content:`[Step ${b}/${f}] Your response was empty. Please respond with a valid JSON object containing "description" (1-2 sentences) and either "strategy" ("ours", "theirs", or "deleted") with a top-level "confidence" (0-1) for a file-level decision, or "chunks" (array of { markerIndex, strategy, confidence, content?, reason? }).`});continue}let a;try{a=r(i,n.markers.length)}catch(e){S++;let t=e instanceof Error?e.message:String(e);v.push({role:`assistant`,content:i}),v.push({role:`user`,content:`[Step ${b}/${f}] Failed to parse your response: ${t}. Please respond with a valid JSON object containing "description" (1-2 sentences) and either "strategy" ("ours", "theirs", or "deleted") with a top-level "confidence" (0-1) for a file-level decision, or "chunks" (array of { markerIndex, strategy, confidence, content?, reason? }).`});continue}if(a.strategy===`deleted`&&n.type!==`delete-modify`){S++,v.push({role:`assistant`,content:i}),v.push({role:`user`,content:`[Step ${b}/${f}] Strategy "deleted" is only valid for delete-modify conflicts. This is a text conflict — respond with "chunks" or a file-level "ours"/"theirs" strategy.`});continue}if(a.strategy){let e=a.strategy;if((e===`ours`||e===`theirs`)&&w)a.chunks=n.markers.map((t,n)=>t.userResolution==null?{markerIndex:n,strategy:e}:null).filter(e=>e!==null),a.strategy=void 0;else return d.onProgress?.({type:`resolver:response`,filePath:n.filePath,chunks:a.chunks,strategy:e,confidence:a.confidence,description:a.description,...a.followUpInstructions?{followUpInstructions:a.followUpInstructions}:{}}),{chunks:a.chunks,strategy:e,confidence:a.confidence,description:a.description,metrics:u(C,b,x,S),...a.followUpInstructions?{followUpInstructions:a.followUpInstructions}:{}}}if(n.markers.length===0&&!a.strategy){S++,v.push({role:`assistant`,content:i}),v.push({role:`user`,content:`[Step ${b}/${f}] Delete-modify conflicts require a file-level strategy field ("ours", "theirs", or "deleted"). Please respond with a JSON object containing "strategy".`});continue}let s=a.chunks.filter(e=>n.markers[e.markerIndex]?.userResolution==null),_=o(s,n);if(!_.valid){S++;let e=(_.issues??[]).map(e=>e.message).join(` `);v.push({role:`assistant`,content:i}),v.push({role:`user`,content:`[Step ${b}/${f}] Validation failed: ${e}\nPlease fix the issues and respond with a corrected JSON object. Make sure every marker index appears exactly once, or use a top-level "strategy" field.`});continue}let E=l(s,n);if(d.verifier&&n.type===`text`){let e=t(n.rawContent,n.markers,E),r=await d.verifier.verify(n.filePath,e);if(!r.valid){S++;let e=(r.issues??[]).filter(e=>e.severity===`error`).map(e=>e.message).join(` `)||`Content did not pass verification.`;v.push({role:`assistant`,content:i}),v.push({role:`user`,content:`[Step ${b}/${f}] Verification failed: ${e}\nPlease fix and respond with corrected JSON.`});continue}let a=(r.issues??[]).filter(e=>e.severity===`warning`);a.length>0&&(T=a,d.onProgress?.({type:`verification:issues`,filePath:n.filePath,issues:a}))}let D=c(s,a.confidence);return d.onProgress?.({type:`resolver:response`,filePath:n.filePath,chunks:a.chunks,confidence:D,description:a.description,...a.followUpInstructions?{followUpInstructions:a.followUpInstructions}:{}}),{chunks:E,confidence:D,description:a.description,metrics:u(C,b,x,S),...a.followUpInstructions?{followUpInstructions:a.followUpInstructions}:{},...T&&T.length>0?{issues:T}:{}}}throw new e(`VALIDATION_EXHAUSTED`,`Resolver exhausted ${f} steps without producing a valid resolution for ${n.filePath}.`)}function c(e,t){let n=e.map(e=>e.confidence).filter(e=>e!==void 0);return n.length>0?Math.min(...n):e.length>0?t:1}function l(e,t){let n=[...e];for(let e=0;e<t.markers.length;e++){let r=t.markers[e]?.userResolution;r!=null&&n.push({markerIndex:e,...r})}return n.sort((e,t)=>e.markerIndex-t.markerIndex),n}function u(e,t,r,i){return{...n(e)??{inputTokens:0,outputTokens:0},stepCount:t,toolCallCount:r,retryCount:i}}export{s as runResolverLoop};
1
+ import{AIError as e}from"../errors/ai.js";import{sumUsages as t}from"../resolution/metrics.js";import{parseModelResponse as n}from"./parse-response.js";import{buildPrompt as r}from"./prompt.js";import{createResolverTools as i}from"./tools.js";import{validateChunks as a}from"./validate.js";async function o(t,o,u){let d=u.config?.maxSteps??15,f=u.config?.temperature??0,{definitions:p,execute:m}=i(u.git),{system:h,userMessage:g}=r(t,o,{maxSteps:d}),_=[{role:`user`,content:g}],v=u.signal?{signal:u.signal}:void 0,y=0,b=0,x=0,S=[],C=t.markers.some(e=>e.userResolution!=null);for(;y<d;){y++;let e=await u.model.generate({system:h,messages:[..._],tools:p,temperature:f,signal:u.signal});if(e.usage&&(S.push(e.usage),u.onProgress?.({type:`resolver:step-usage`,filePath:t.filePath,stepNumber:y,usage:e.usage})),e.toolCalls&&e.toolCalls.length>0){_.push({role:`assistant`,content:e.text,toolCalls:e.toolCalls});for(let n of e.toolCalls){let e=typeof n.args.reason==`string`?n.args.reason:void 0;u.onProgress?.({type:`resolver:tool-call`,filePath:t.filePath,tool:n.name,args:n.args,stepNumber:y,reason:e}),b++;let r=await m(n,v);u.onProgress?.({type:`resolver:tool-result`,filePath:t.filePath,tool:n.name,stepNumber:y,content:r.content}),_.push({role:`tool`,toolCallId:r.toolCallId,toolName:n.name,content:r.content})}continue}let r=e.text??``;if(!r.trim()){x++,_.push({role:`assistant`,content:``}),_.push({role:`user`,content:`[Step ${y}/${d}] Your response was empty. Please respond with a valid JSON object containing "description" (1-2 sentences) and either "strategy" ("ours", "theirs", or "deleted") with a top-level "confidence" (0-1) for a file-level decision, or "chunks" (array of { markerIndex, strategy, confidence, content?, reason? }).`});continue}let i;try{i=n(r,t.markers.length)}catch(e){x++;let t=e instanceof Error?e.message:String(e);_.push({role:`assistant`,content:r}),_.push({role:`user`,content:`[Step ${y}/${d}] Failed to parse your response: ${t}. Please respond with a valid JSON object containing "description" (1-2 sentences) and either "strategy" ("ours", "theirs", or "deleted") with a top-level "confidence" (0-1) for a file-level decision, or "chunks" (array of { markerIndex, strategy, confidence, content?, reason? }).`});continue}if(i.strategy===`deleted`&&t.type!==`delete-modify`){x++,_.push({role:`assistant`,content:r}),_.push({role:`user`,content:`[Step ${y}/${d}] Strategy "deleted" is only valid for delete-modify conflicts. This is a text conflict — respond with "chunks" or a file-level "ours"/"theirs" strategy.`});continue}if(i.strategy){let e=i.strategy;if((e===`ours`||e===`theirs`)&&C)i.chunks=t.markers.map((t,n)=>t.userResolution==null?{markerIndex:n,strategy:e}:null).filter(e=>e!==null),i.strategy=void 0;else return u.onProgress?.({type:`resolver:response`,filePath:t.filePath,chunks:i.chunks,strategy:e,confidence:i.confidence,description:i.description,...i.followUpInstructions?{followUpInstructions:i.followUpInstructions}:{}}),{chunks:i.chunks,strategy:e,confidence:i.confidence,description:i.description,metrics:l(S,y,b,x),...i.followUpInstructions?{followUpInstructions:i.followUpInstructions}:{}}}if(t.markers.length===0&&!i.strategy){x++,_.push({role:`assistant`,content:r}),_.push({role:`user`,content:`[Step ${y}/${d}] Delete-modify conflicts require a file-level strategy field ("ours", "theirs", or "deleted"). Please respond with a JSON object containing "strategy".`});continue}let o=i.chunks.filter(e=>t.markers[e.markerIndex]?.userResolution==null),g=a(o,t);if(!g.valid){x++;let e=(g.issues??[]).map(e=>e.message).join(` `);_.push({role:`assistant`,content:r}),_.push({role:`user`,content:`[Step ${y}/${d}] Validation failed: ${e}\nPlease fix the issues and respond with a corrected JSON object. Make sure every marker index appears exactly once, or use a top-level "strategy" field.`});continue}let w=c(o,t),T=s(o,i.confidence);return u.onProgress?.({type:`resolver:response`,filePath:t.filePath,chunks:i.chunks,confidence:T,description:i.description,...i.followUpInstructions?{followUpInstructions:i.followUpInstructions}:{}}),{chunks:w,confidence:T,description:i.description,metrics:l(S,y,b,x),...i.followUpInstructions?{followUpInstructions:i.followUpInstructions}:{}}}throw new e(`VALIDATION_EXHAUSTED`,`Resolver exhausted ${d} steps without producing a valid resolution for ${t.filePath}.`)}function s(e,t){let n=e.map(e=>e.confidence).filter(e=>e!==void 0);return n.length>0?Math.min(...n):e.length>0?t:1}function c(e,t){let n=[...e];for(let e=0;e<t.markers.length;e++){let r=t.markers[e]?.userResolution;r!=null&&n.push({markerIndex:e,...r})}return n.sort((e,t)=>e.markerIndex-t.markerIndex),n}function l(e,n,r,i){return{...t(e)??{inputTokens:0,outputTokens:0},stepCount:n,toolCallCount:r,retryCount:i}}export{o as runResolverLoop};
@@ -1 +1 @@
1
- import{sumUsages as e}from"../resolution/metrics.js";import{buildRefinePrompt as t}from"./prompt.js";import{createRefineTools as n}from"./refine-tools.js";async function r(r,i,a,o){let s=o.refineMaxSteps??5,{system:c,userMessage:l}=t(r,i,a),{definitions:u,execute:d}=n(),f=[{role:`user`,content:l}],p=r,m=[],h=0,g=[];for(let t=0;t<s;t++){let n=t+1,r=await o.model.generate({system:c,messages:[...f],tools:u,temperature:o.temperature??0,signal:o.signal});if(r.usage&&g.push(r.usage),r.toolCalls&&r.toolCalls.length>0){f.push({role:`assistant`,content:r.text,toolCalls:r.toolCalls});for(let e of r.toolCalls){let t=e.args.ops,r=Array.isArray(t)?t:[];o.onProgress?.({type:`refine:tool-call`,filePath:o.filePath,stepNumber:n,ops:r});let{content:i,edits:a,verifiedCount:s,feedback:c,isError:l}=d(e,p);l||(p=i,m.push(...a),h+=s),o.onProgress?.({type:`refine:tool-result`,filePath:o.filePath,stepNumber:n,appliedCount:a.length,isError:l,feedback:c}),f.push({role:`tool`,toolCallId:e.id,toolName:e.name,content:c})}continue}let i=(r.text??``).trim();return o.onProgress?.({type:`refine:final-text`,filePath:o.filePath,text:i}),{edits:m,verifies:h,content:p,description:i,usage:e(g)}}return{edits:m,verifies:h,content:p,description:``,usage:e(g)}}export{r as runRefinePass};
1
+ import{sumUsages as e}from"../resolution/metrics.js";import{buildRefinePrompt as t}from"./prompt.js";import{createRefineTools as n}from"./refine-tools.js";async function r(r,i,a,o){let s=o.refineMaxSteps??5,{system:c,userMessage:l}=t(r,i,a),{definitions:u,execute:d}=n(),f=[{role:`user`,content:l}],p=r,m=[],h=0,g=0,_=[];for(let t=0;t<s;t++){let n=t+1,r=await o.model.generate({system:c,messages:[...f],tools:u,temperature:o.temperature??0,signal:o.signal});if(r.usage&&_.push(r.usage),r.toolCalls&&r.toolCalls.length>0){f.push({role:`assistant`,content:r.text,toolCalls:r.toolCalls});for(let t of r.toolCalls){let r=t.args.ops,i=Array.isArray(r)?r:[];o.onProgress?.({type:`refine:tool-call`,filePath:o.filePath,stepNumber:n,ops:i});let{content:a,edits:s,verifiedCount:c,feedback:l,isError:u}=d(t,p);if(u?g++:(p=a,m.push(...s),h+=c,g=0),o.onProgress?.({type:`refine:tool-result`,filePath:o.filePath,stepNumber:n,appliedCount:s.length,isError:u,feedback:l}),f.push({role:`tool`,toolCallId:t.id,toolName:t.name,content:l}),g>=3)return o.onProgress?.({type:`refine:bailed`,filePath:o.filePath,reason:`Stopped after ${g} consecutive apply errors.`}),{edits:m,verifies:h,content:p,description:``,usage:e(_)}}continue}let i=(r.text??``).trim();return o.onProgress?.({type:`refine:final-text`,filePath:o.filePath,text:i}),{edits:m,verifies:h,content:p,description:i,usage:e(_)}}return{edits:m,verifies:h,content:p,description:``,usage:e(_)}}export{r as runRefinePass};
@@ -1 +1 @@
1
- import{checkResidualMarkers as e}from"../ports/verify.js";import{resolveChunkContent as t}from"../resolution/chunk-apply.js";import{createResolution as n}from"../resolution/create-resolution.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:_,issues:v}=await i(c,d,{model:u.model,git:u.git,config:u.config,onProgress:u.onProgress,signal:u.signal,verifier:u.verifier}),y=n(c,f,{confidence:m,description:h,metrics:g,...p?{fileStrategy:p}:{},...d.threeWayDiff?{threeWayDiff:d.threeWayDiff}:{}});v&&v.length>0&&(y={...y,issues:v});let b=_&&_.length>0;if(c.type===`text`&&b){u.onProgress?.({type:`refine:started`,filePath:c.filePath});let n=0;try{let i=await a(y.content,y.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 t=e(c.filePath,i.content);if(t.valid)y={...y,content:i.content,edits:i.edits,strategy:`ai`},n=i.edits.length;else{let e=(t.issues??[]).map(e=>e.message).join(` `);u.onProgress?.({type:`refine:discarded`,filePath:c.filePath,reason:`Edits introduced conflict markers: ${e}`})}}if(n>0){y={...y,confidence:Math.min(y.confidence,s.refineApplied)};let e=[];for(let n=0;n<c.markers.length;n++){let r=c.markers[n];if(!r||r.userResolution==null)continue;let a=t({markerIndex:n,...r.userResolution},r);a!==``&&(i.content.includes(a)||e.push(n))}e.length>0&&(u.onProgress?.({type:`refine:touched-user-region`,filePath:c.filePath,markerIndexes:e}),y={...y,note:`Refine modified user-resolved region(s): marker ${e.join(`, `)}.`})}else i.edits.length===0&&i.verifies>0||(y={...y,confidence:Math.min(y.confidence,s.refineNoAction)});if(i.usage){let e=r(y.metrics??{inputTokens:0,outputTokens:0},i.usage);y={...y,metrics:{...y.metrics,...e}}}u.onProgress?.({type:`refine:completed`,filePath:c.filePath,appliedCount:n})}catch(e){let t=e instanceof Error?e.message:String(e);throw u.onProgress?.({type:`refine:failed`,filePath:c.filePath,reason:t}),e}}else u.onProgress?.({type:`refine:skipped`,filePath:c.filePath,reason:`no-follow-ups`});return u.onProgress?.({type:`resolver:completed`,filePath:c.filePath,stepCount:g.stepCount??0}),y}export{c as resolveConflict};
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);throw u.onProgress?.({type:`refine:failed`,filePath:c.filePath,reason:t}),e}}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};
@@ -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}))}return{resolutions:g,errors:_,skipped:v,detection:await r(g,h,{git:e.git,parser:e.parser,signal:e.signal})}}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,verifier:r.verifier})}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{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);if(r.signal?.aborted)throw s;let 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};
@@ -5,7 +5,7 @@ import { OperationDigest, OperationSummary } from "../types/summary.js";
5
5
  //#region src/resolver/summarize-operation.d.ts
6
6
  /**
7
7
  * Options for {@link summarizeOperation}. Intentionally a flat bag rather than the resolver's
8
- * `ResolverDeps` shape: summarization is a leaf operation that needs no git, verifier, or progress
8
+ * `ResolverDeps` shape: summarization is a leaf operation that needs no git, validator, or progress
9
9
  * sink, and its `model` is optional — omit it for a digest-only result.
10
10
  */
11
11
  interface SummarizeOperationOptions {
@@ -16,7 +16,8 @@ interface SummarizeOperationOptions {
16
16
  reviewThreshold?: number;
17
17
  temperature?: number;
18
18
  }
19
- /** Pure, model-free rollup of a {@link StepResult}. */
19
+ /** Pure, model-free rollup of a {@link StepResult}, folding in `detection` so a file with an error
20
+ * finding — or a detection check that did not run — can never read as clean. */
20
21
  declare function buildOperationDigest(result: StepResult, reviewThreshold?: number): OperationDigest;
21
22
  /**
22
23
  * Summarize a resolve operation for a reviewer. Always returns a deterministic `digest`; when
@@ -1 +1 @@
1
- import{buildSummaryPrompt as e}from"./prompt.js";const t=.8;function n(e,n=t){let r={ai:0,"take-ours":0,"take-theirs":0,deleted:0,skipped:0},i=0,a=null,o=[];for(let t of e.resolutions)r[t.strategy]++,i+=t.chunks?.length??0,a=a===null?t.confidence:Math.min(a,t.confidence),t.confidence<n&&o.push({filePath:t.filePath,confidence:t.confidence});return o.sort((e,t)=>e.confidence-t.confidence),{files:e.resolutions.length,conflicts:i,byStrategy:r,minConfidence:a,needsReview:o,skipped:e.skipped?.length??0,errors:e.errors.length}}async function r(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,r as summarizeOperation};
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,6 +1,6 @@
1
- import { Issue } from "../types/issue.js";
2
1
  import { ResolvedChunk } from "../types/resolution.js";
3
2
  import { Conflict } from "../types/conflict.js";
3
+ import { Issue } from "../types/issue.js";
4
4
 
5
5
  //#region src/resolver/validate.d.ts
6
6
  interface ValidationResult {
@@ -1,6 +1,6 @@
1
- import { Issue } from "./issue.js";
2
1
  import { ResolutionStrategy, ResolvedChunk } from "./resolution.js";
3
2
  import { ConflictType } from "./conflict.js";
3
+ import { Issue } from "./issue.js";
4
4
  import { AiTokenUsage } from "@gitkraken/shared-tools";
5
5
 
6
6
  //#region src/types/events.d.ts
@@ -91,6 +91,10 @@ type ConflictProgressEvent = {
91
91
  type: 'refine:failed';
92
92
  filePath: string;
93
93
  reason: string;
94
+ } | {
95
+ type: 'refine:bailed';
96
+ filePath: string;
97
+ reason: string;
94
98
  } | {
95
99
  type: 'refine:touched-user-region';
96
100
  filePath: string;
@@ -99,6 +103,10 @@ type ConflictProgressEvent = {
99
103
  type: 'verification:issues';
100
104
  filePath: string;
101
105
  issues: Issue[];
106
+ } | {
107
+ type: 'validation:failed';
108
+ filePath: string;
109
+ reason: string;
102
110
  } | {
103
111
  type: 'resolver:tool-result';
104
112
  filePath: string;
@@ -1,8 +1,13 @@
1
1
  //#region src/types/findings.d.ts
2
- type FindingCategory = 'broken-reference' | 'duplicate-definition' | 'syntax-error';
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. */
5
+ type FindingCategory = 'broken-reference' | 'duplicate-definition' | 'syntax-error' | 'dropped-side';
3
6
  type FindingTier = 'file' | 'project';
4
7
  interface Finding {
5
- category: FindingCategory;
8
+ /** Finding kind. Open string — the package emits {@link FindingCategory} values; a consumer
9
+ * validator may use its own category strings. */
10
+ category: string;
6
11
  severity: 'error' | 'warning';
7
12
  tier: FindingTier;
8
13
  location: {
@@ -19,14 +24,24 @@ interface Finding {
19
24
  }
20
25
  /** Why a check produced its result — lets a consumer tell "ran, clean" apart from "did not run".
21
26
  * `errored` means the parser was present but threw; `partial-unsupported-language` means some files
22
- * in scope could not be parsed. */
23
- type CheckStatus = 'ran' | 'skipped-no-parser' | 'skipped-no-refs' | 'partial-unsupported-language' | 'errored';
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';
24
30
  interface DetectionReport {
25
31
  findings: Finding[];
26
32
  checks: {
27
33
  referenceIntegrity: CheckStatus;
28
34
  structural: CheckStatus;
29
35
  };
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). */
40
+ scope?: {
41
+ analyzed: number;
42
+ readable: number;
43
+ truncated: boolean;
44
+ };
30
45
  }
31
46
  //#endregion
32
47
  export { CheckStatus, DetectionReport, Finding, FindingCategory, FindingTier };
@@ -1,5 +1,5 @@
1
+ import { Finding } from "./findings.js";
1
2
  import { IntraFileEditOp } from "./hints.js";
2
- import { Issue } from "./issue.js";
3
3
  import { ConflictType } from "./conflict.js";
4
4
  import { AiTokenUsage } from "@gitkraken/shared-tools";
5
5
 
@@ -26,7 +26,9 @@ interface Resolution {
26
26
  confidence: number;
27
27
  description: string;
28
28
  note?: string;
29
- issues?: Issue[];
29
+ /** Non-blocking per-file findings from the consumer's validator port (post-refine). Folded into
30
+ * the operation's detection findings, hence into the verdict. */
31
+ findings?: Finding[];
30
32
  chunks?: ResolvedChunk[];
31
33
  edits?: IntraFileEditOp[];
32
34
  metrics?: ResolutionMetrics;
@@ -69,7 +71,16 @@ interface ResolutionContext {
69
71
  prDescription?: string;
70
72
  previousResolutions?: Resolution[];
71
73
  fileNeighbors?: string[];
74
+ /** Files the merge/sync touched beyond the conflict set (chiefly auto-merged incoming changes).
75
+ * Usually left unset — the reference-integrity detector discovers this set itself from `refs`.
76
+ * When provided, it overrides auto-discovery and defines the incoming detection scope. */
77
+ touchedFiles?: string[];
72
78
  refs?: ResolutionRefs;
79
+ /** Parent tips for reference-integrity detection's before/after comparison, when they differ from
80
+ * `refs`. `refs` is the resolver's three-way source (in a rebase, the in-flight per-step state);
81
+ * detection needs the two stable pre-merge branch tips. In a merge they coincide, so this is
82
+ * usually left unset and detection falls back to `refs`. */
83
+ detectionRefs?: ResolutionRefs;
73
84
  threeWayDiff?: ThreeWayDiff;
74
85
  /** Free-text guidance from the user that applies to the whole file ("prefer logging across the
75
86
  * board; throw on invalid input"). Rendered into the prompt as a top-level `<user-guidance>`
@@ -1,6 +1,6 @@
1
+ import { DetectionReport } from "./findings.js";
1
2
  import { Resolution } from "./resolution.js";
2
3
  import { ResolverConfig } from "../ports/model.js";
3
- import { DetectionReport } from "./findings.js";
4
4
 
5
5
  //#region src/types/step.d.ts
6
6
  interface StepResult {
@@ -2,11 +2,21 @@ 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 resolved file flagged for human review because its file confidence is below the threshold. */
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). */
6
8
  interface ReviewItem {
7
9
  filePath: string;
8
- confidence: number;
10
+ confidence?: number;
11
+ reason: 'low-confidence' | 'finding';
12
+ /** Categories of the error-severity findings on this file; present when `reason` is `'finding'`. */
13
+ findingCategories?: string[];
9
14
  }
15
+ /** Suggested honest verdict for the operation. `blocked` when any error-severity finding exists;
16
+ * `needs-review` when confidence is low, detection did not fully run, resolution errored, or a
17
+ * warning finding exists; `clean` only when none of those apply. Advisory — the consumer owns its
18
+ * own gate, but this must not read as clean when the build is known-broken. */
19
+ type OperationVerdict = 'clean' | 'needs-review' | 'blocked';
10
20
  /** Deterministic, model-free rollup of a resolve operation. */
11
21
  interface OperationDigest {
12
22
  /** Files that produced a resolution. */
@@ -17,12 +27,21 @@ interface OperationDigest {
17
27
  byStrategy: Record<ResolutionStrategy, number>;
18
28
  /** Lowest file confidence across all resolutions; null when there are none. */
19
29
  minConfidence: number | null;
20
- /** Resolutions below the review threshold, lowest confidence first. */
30
+ /** Files needing review — low-confidence resolutions and files with error findings, findings
31
+ * first, then lowest confidence first. */
21
32
  needsReview: ReviewItem[];
22
33
  /** Files skipped by rule or because they had no markers. */
23
34
  skipped: number;
24
35
  /** Files that errored during resolution. */
25
36
  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". */
40
+ detectionComplete: boolean;
41
+ /** Total detection findings across the operation. */
42
+ findingCount: number;
43
+ /** Suggested honest verdict; see {@link OperationVerdict}. */
44
+ verdict: OperationVerdict;
26
45
  }
27
46
  /** Result of summarizing an operation: always a digest, plus a narrative when a model was provided. */
28
47
  interface OperationSummary {
@@ -33,4 +52,4 @@ interface OperationSummary {
33
52
  usage?: AiTokenUsage;
34
53
  }
35
54
  //#endregion
36
- export { OperationDigest, OperationSummary, ReviewItem };
55
+ export { OperationDigest, OperationSummary, OperationVerdict, ReviewItem };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitkraken/conflict-tools",
3
- "version": "0.2.4-beta.1",
3
+ "version": "0.2.4-beta.10",
4
4
  "description": "AI-powered git conflict resolution.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Axosoft, LLC dba GitKraken",