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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -131,15 +131,21 @@ Pass via `deps.parser`. When absent, detection is skipped and the report says so
131
131
 
132
132
  ### `Validator` (optional)
133
133
 
134
- The consumer's validation port. Plug in any checks — syntax, real import resolution, types, lint, project rules. It runs **post-refine over an overlay** and returns `ValidatorFinding[]`.
134
+ The consumer's validation port. Plug in any checks — syntax, real import resolution, types, lint, project rules. It is called with an overlay and a `call` context, and returns `ValidatorFinding[]` or `{ status: 'not-run', reason }`.
135
135
 
136
- The overlay is currently **always a single file**: the package validates one file at a time, right after that file's refine. `SourceOverlay` is the parameter type so a whole-set call can be added later without a breaking change, but no such call site exists yet — do not write a validator that only works across files.
136
+ `call` says what the overlay holds:
137
+
138
+ - **Per-file calls** (`scope: 'file'`) hold one file, right after that file's refine. `role` is `result` for the resolved content, and `ours` / `theirs` when the package builds a baseline from the pre-merge parents (findings the parents already had are subtracted). A validator that cannot check one file in isolation (a compile check that needs its siblings) returns `{ status: 'not-run', reason }` for these. A per-file `not-run` adds no findings and is reported as a `validation:not-run` event.
139
+ - **The whole-set call** (`scope: 'set'`, `role: 'result'`) runs **once per operation**, after every file of the step is resolved, and after the fix phase when it runs. The overlay holds every resolved file's final content: a resolution's own content, or for a whole-side take the taken side's content when the detection snapshots hold it (otherwise the file is left out). `call.deleted` lists the paths the step deletes, including retired rename paths; they are absent from the overlay, so treat them as removed. There is no parent baseline: errors the merged set has are the merge's to answer for. The call is skipped when there is nothing to validate.
140
+
141
+ The whole-set outcome is reported on `DetectionReport.checks.validator` (`ran`, `skipped-no-validator`, `not-run`, `errored`) with `validatorReason`. A whole-set `not-run` or throw never fails the operation, but it makes `detectionComplete` false, so the verdict cannot read `clean`.
137
142
 
138
143
  ```typescript
139
144
  import type { Validator, ValidatorFinding } from '@gitkraken/conflict-tools';
140
145
 
141
146
  const validator: Validator = {
142
- async validate(sources) {
147
+ async validate(sources, signal, call) {
148
+ if (call.scope === 'file') return { status: 'not-run', reason: 'needs the whole set' };
143
149
  const findings: ValidatorFinding[] = [];
144
150
  for (const [file, content] of sources) {
145
151
  for (const err of await typecheck(file, content)) {
@@ -151,7 +157,7 @@ const validator: Validator = {
151
157
  };
152
158
  ```
153
159
 
154
- **Advisory, never blocking.** Its findings fold into the operation's detection findings and the verdict; a validator finding never triggers a re-resolve, and a *throwing* validator never destroys the resolution — it is kept intact and the failure is reported. On the resolve/report path that report is a `validation:failed` event; inside the opt-in fix phase a validator throw folds into the phase's per-file isolation and surfaces as `fix:failed` (like any per-file throw there), so telemetry that must catch every validator failure should watch both. `ValidatorFinding.category` is an open string (e.g. `'type-error'`, `'lint:no-console'`) so consumer categories flow through alongside the package's own `FindingCategory` values.
160
+ **Advisory, never blocking.** Its findings fold into the operation's detection findings and the verdict; a validator finding never triggers a re-resolve, and a *throwing* validator never destroys the resolution — it is kept intact and the failure is reported. On the resolve/report path that report is a `validation:failed` event (`scope` says whether it was a per-file or the whole-set call); inside the opt-in fix phase a validator throw folds into the phase's per-file isolation and surfaces as `fix:failed` (like any per-file throw there), so telemetry that must catch every validator failure should watch both. `ValidatorFinding.category` is an open string (e.g. `'type-error'`, `'lint:no-console'`) so consumer categories flow through alongside the package's own `FindingCategory` values.
155
161
 
156
162
  The one blocking gate — **residual conflict markers** — is *not* this port's job. It is checked built-in and language-agnostic inside the resolve loop. (This was the old `defaultVerifier`; it is now internal. `defaultVerifier`, `ResolutionVerifier`, and `VerificationResult` are no longer exported.)
157
163
 
@@ -190,7 +196,7 @@ import { resolveConflict } from '@gitkraken/conflict-tools';
190
196
  const resolution = await resolveConflict(conflict, context, {
191
197
  model,
192
198
  git,
193
- validator, // optional — advisory per-file checks
199
+ validator, // optional — advisory checks
194
200
  config: { maxSteps: 15, temperature: 0, refineMaxSteps: 5, modelCallTimeoutMs: 300_000 },
195
201
  onProgress: (event) => console.log(event),
196
202
  });
@@ -260,7 +266,6 @@ A `merged` chunk is also checked for brackets: the `{}`, `()` and `[]` balance (
260
266
  - `insert` — inserts `content` after the line matched by `anchor` at `startLine`.
261
267
  - `replace` — replaces lines `startLine`–`endLine` (matched by `anchor`) with `content`.
262
268
  - `delete` — removes lines `startLine`–`endLine` matched by `anchor`. No `content`.
263
- - `verify` — confirms lines at `startLine` (optionally through `endLine`) match `anchor` without modifying content.
264
269
 
265
270
  ### `resolveConflicts(deps, context?) + applyResolutions(resolutions, deps)` — batch
266
271
 
@@ -272,7 +277,7 @@ const step = await resolveConflicts(
272
277
  model,
273
278
  git,
274
279
  parser, // optional — supply to enable set-level detection (stage 3)
275
- validator, // optional — advisory per-file checks
280
+ validator, // optional — advisory checks
276
281
  config: {
277
282
  rules: [
278
283
  { match: ['*.lock', 'pnpm-lock.yaml'], strategy: 'take-theirs' },
@@ -430,7 +435,9 @@ The scope includes the resolved files *and* the merge's auto-merged/incoming set
430
435
  ```typescript
431
436
  interface DetectionReport {
432
437
  findings: Finding[];
433
- checks: { referenceIntegrity: CheckStatus; structural: CheckStatus };
438
+ checks: { referenceIntegrity: CheckStatus; structural: CheckStatus; validator: ValidatorCheckStatus };
439
+ /** Why the whole-set validator did not run or threw. */
440
+ validatorReason?: string;
434
441
  /** Honest coverage — present only when the reference check ran. */
435
442
  scope?: { inScope: number; readable: number; truncated: boolean; incomingDiscovered: boolean };
436
443
  }
@@ -480,7 +487,8 @@ import { buildOperationDigest } from '@gitkraken/conflict-tools';
480
487
 
481
488
  const digest = buildOperationDigest(step);
482
489
  // digest.verdict — 'clean' | 'needs-review' | 'blocked'
483
- // digest.detectionComplete — did every detection check run to completion
490
+ // digest.detectionComplete — did every detection check run to completion (a supplied validator that did not run counts as incomplete)
491
+ // digest.validation — ValidatorCheckStatus of the whole-set validator call; digest.validationReason says why when it did not run
484
492
  // digest.findingCount — total detection findings
485
493
  // digest.needsReview — ReviewItem[] (findings first, then lowest confidence first)
486
494
  // digest.minConfidence — lowest file confidence, or null
@@ -609,7 +617,7 @@ score(resolution, goldenFile);
609
617
  | `refine:final-text` | Refine model emitted its final description (`text`) |
610
618
  | `refine:completed` | Refine finished successfully (`appliedCount`) |
611
619
  | `refine:failed` | Refine threw before completing (`reason`) |
612
- | `refine:bailed` | Refine bailed early after consecutive apply errors, keeping edits that landed (`reason`) |
620
+ | `refine:bailed` | Refine bailed early after consecutive apply errors, or ran out of steps or model budget, keeping edits that landed (`reason` says which) |
613
621
  | `refine:discarded` | A refine/fix edit batch was rejected — it reintroduced conflict markers, or (in the fix phase) it introduced an error the file did not have (`reason` says which) |
614
622
  | `refine:lost-addition` | A side's added lines are missing from the resolution (`side`, `startLine`, `endLine`, `lines`, `phase`: `before-refine` or `after-refine`) |
615
623
  | `refine:bracket-imbalance` | A merged chunk's bracket balance matches neither side nor the base (`markerIndex`, `pair`, `merged`, `ours`, `theirs`) |
@@ -618,7 +626,8 @@ score(resolution, goldenFile);
618
626
  | `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" |
619
627
  | `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` |
620
628
  | `fix:budget-exhausted` | The global `maxModelCalls` budget was spent (`spent`, `limit`); the fix phase stopped and left the remainder in findings |
621
- | `validation:failed` | The consumer validator threw; reported and swallowed, resolution kept (`reason`) |
629
+ | `validation:failed` | The consumer validator threw; reported and swallowed, resolution kept (`scope`: `file` or `set`, `filePath?` — absent for the set call, `reason`) |
630
+ | `validation:not-run` | The consumer validator answered `not-run` (`scope`, `filePath?` — absent for the set call, `reason`) |
622
631
 
623
632
  Note: `refine:tool-result.appliedCount` reports edits applied to an in-progress buffer per turn. The final disposition is signaled by `refine:completed` / `refine:discarded` / `refine:failed` — only edits that survive residual-marker validation appear in `Resolution.edits`.
624
633
 
package/dist/index.d.ts CHANGED
@@ -11,12 +11,12 @@ import { ToolCall, ToolDefinition, ToolResult } from "./types/tool.js";
11
11
  import { ConflictModelMessage, ConflictModelParams, ConflictModelPort, ConflictModelResult, ResolverConfig } from "./ports/model.js";
12
12
  import { ParsedFile, ParserCapabilities, ParserPort, SourceOverlay, SymbolDefinition, SymbolReference, SyntaxIssue } from "./ports/parser.js";
13
13
  import { Validator, ValidatorFinding } from "./ports/validator.js";
14
+ import { ConflictProgressEvent } from "./types/events.js";
14
15
  import { DetectDeps, detect } from "./reference-integrity/detect.js";
15
16
  import { createResolution } from "./resolution/create-resolution.js";
16
17
  import { CodeLossDetection, detectFallbackCodeLoss } from "./resolution/detect-code-loss.js";
17
18
  import { takeOurs, takeTheirs } from "./resolution/take-strategy.js";
18
19
  import { applyResolutions } from "./resolver/apply-resolutions.js";
19
- import { ConflictProgressEvent } from "./types/events.js";
20
20
  import { resolveConflict } from "./resolver/resolve-conflict.js";
21
21
  import { FailedEntry, FailureReason, FileRule, SkippedEntry, StepConfig, StepResult } from "./types/step.js";
22
22
  import { resolveConflicts } from "./resolver/resolve-conflicts.js";
@@ -12,18 +12,34 @@ interface ValidatorFinding {
12
12
  line?: number;
13
13
  message: string;
14
14
  }
15
+ interface ValidationCall {
16
+ /** `file`: one file's content, for a per-file check or a parent baseline. `set`: every resolved file of the step, once, after all files are resolved. */
17
+ scope: 'file' | 'set';
18
+ /** Which content a `file` call holds. Always `result` for `set`. */
19
+ role: 'result' | 'ours' | 'theirs';
20
+ /** `set` only: paths the step deletes. They are absent from `sources`; treat them as removed. */
21
+ deleted?: readonly string[];
22
+ }
23
+ interface ValidatorNotRun {
24
+ status: 'not-run';
25
+ /** Short, human-readable: "siblings still conflicted", "unsupported language", "go.mod conflicted". */
26
+ reason: string;
27
+ }
28
+ type ValidatorOutcome = ValidatorFinding[] | ValidatorNotRun;
15
29
  /**
16
30
  * The consumer's validation port. Plug in any checks — syntax, real import resolution, types, lint,
17
31
  * project rules. It runs after refine; the opt-in fix loop may route new error-severity findings back
18
32
  * through refine. Residual conflict markers are the resolver's blocking gate and are checked separately.
19
33
  *
20
- * `sources` is always a single-file overlay. It may contain the result or a pre-merge parent version
21
- * of that file to build a baseline, so validate the content supplied rather than reading the working
22
- * tree. The parameter is an overlay so a whole-set call can be added without a breaking change, but
23
- * no such call site exists yet — do not write a validator that only works across files.
34
+ * `call` says what `sources` holds. Per-file calls (`scope: 'file'`) hold one file; `role` says which
35
+ * version: the `result`, or a pre-merge parent (`ours`/`theirs`) to build a baseline. The whole-set call
36
+ * (`scope: 'set'`) holds every resolved file of the step and runs once, after all files are resolved (and
37
+ * after the fix phase, when it runs); it has no baseline. Validate the content supplied rather than reading
38
+ * the working tree. A validator that cannot check a single file in isolation returns
39
+ * `{ status: 'not-run', reason }` for `file` calls and does its work on the `set` call.
24
40
  */
25
41
  interface Validator {
26
- validate(sources: SourceOverlay, signal?: AbortSignal): Promise<ValidatorFinding[]>;
42
+ validate(sources: SourceOverlay, signal: AbortSignal | undefined, call: ValidationCall): Promise<ValidatorOutcome>;
27
43
  }
28
44
  //#endregion
29
- export { Validator, ValidatorFinding };
45
+ export { ValidationCall, Validator, ValidatorFinding, ValidatorNotRun, ValidatorOutcome };
@@ -3,6 +3,7 @@ import { DetectionReport, Finding } from "../types/findings.js";
3
3
  import { Resolution, ResolutionContext } from "../types/resolution.js";
4
4
  import { ParserPort } from "../ports/parser.js";
5
5
  import { Validator } from "../ports/validator.js";
6
+ import { ConflictProgressEvent } from "../types/events.js";
6
7
 
7
8
  //#region src/reference-integrity/detect.d.ts
8
9
  interface DetectDeps {
@@ -34,16 +35,7 @@ interface FileFindingsDeps {
34
35
  parser?: ParserPort;
35
36
  validator?: Validator;
36
37
  signal?: AbortSignal;
37
- }
38
- /** Per-file findings split by who produced them. They are kept apart because only the `text` and
39
- * `validator` halves are the file's own to keep: parser findings computed over a *single-file* overlay are
40
- * provisional — a symbol defined in another file is unresolved there by construction — so the
41
- * authoritative version is whatever `detect()` says over the whole set. The fix loop uses all of them to
42
- * converge and persists `text` and `validator`. */
43
- interface FileFindingsDeps {
44
- parser?: ParserPort;
45
- validator?: Validator;
46
- signal?: AbortSignal;
38
+ onProgress?: (event: ConflictProgressEvent) => void;
47
39
  }
48
40
  /** Per-file findings split by who produced them. They are kept apart because only the `text` and
49
41
  * `validator` halves are the file's own to keep: parser findings computed over a *single-file* overlay are provisional — a
@@ -1,3 +1,3 @@
1
- import{duplicateBlockFindings as e}from"../resolution/duplicate-blocks.js";import{toFileFinding as t}from"../validation/findings.js";import{buildSnapshots as n,carriesNoOwnContent as r,safeReadFile as i}from"./snapshot.js";async function a(e,t,a){let s=a.parser;if(!s)return{findings:[],checks:{referenceIntegrity:`skipped-no-parser`,structural:`skipped-no-parser`}};try{let c=t.detectionRefs??t.refs?await n(e,t,a):null;if(c){let t=await s.analyze(c.ours,a.signal),n=await s.analyze(c.theirs,a.signal),r=await s.analyze(c.result,a.signal);return{findings:[...u(t,n,r).findings,...f(r,[t,n]).findings,...m(e,c)],checks:{referenceIntegrity:o(c,s,[t,n,r]),structural:g(r)?`partial-unsupported-language`:`ran`},scope:{inScope:c.inScope,readable:c.readable,truncated:c.truncated,incomingDiscovered:c.incomingDiscovered}}}let l=new Map;for(let t of e){let e=r(t)?await i(a.git,t.filePath,{signal:a.signal}):t.content;e!=null&&l.set(t.filePath,e)}let d=await s.analyze(l,a.signal);return{findings:f(d,[]).findings,checks:{referenceIntegrity:`skipped-no-refs`,structural:g(d)?`partial-unsupported-language`:`ran`}}}catch(e){if(a.signal?.aborted)throw e;return{findings:[],checks:{referenceIntegrity:`errored`,structural:`errored`}}}}function o(e,t,n){return e.inScope>0&&e.readable===0?`skipped-unreadable-source`:t.capabilities?.crossFileResolution?n.some(g)?`partial-unsupported-language`:`ran`:`partial-no-cross-file-resolution`}async function s(n,r,i,a){let o=a.parser,s=[],l=[];if(o){let e=async e=>e==null?new Map:o.analyze(new Map([[n,e]]),a.signal),t=await e(r.ours),c=await e(r.theirs),d=await o.analyze(new Map([[n,i]]),a.signal),p=u(t,c,d),m=f(d,[t,c]);s=[...p.findings.filter(e=>e.tier===`file`),...m.findings],l.push(...p.preExisting.filter(e=>e.tier===`file`),...m.preExisting)}let d=[],p=a.validator;if(p){let e=async e=>(await p.validate(new Map([[n,e]]),a.signal)).map(t),o=[];r.ours!=null&&o.push(await e(r.ours)),r.theirs!=null&&o.push(await e(r.theirs));let s=c(await e(i),o,e=>JSON.stringify([e.category,e.message]));d=s.kept,l.push(...s.preExisting)}return{parser:s,validator:d,preExisting:l,text:e(n,i,r)}}function c(e,t,n){let r=new Map;for(let e of t){let t=new Map;for(let r of e){let e=n(r);t.set(e,(t.get(e)??0)+1)}for(let[e,n]of t)r.set(e,Math.max(r.get(e)??0,n))}let i=[],a=[];for(let t of[...e].sort((e,t)=>e.location.line-t.location.line)){let e=n(t),o=r.get(e)??0;o>0?(a.push(t),r.set(e,o-1)):i.push(t)}return{kept:i,preExisting:a}}function l(e){let t=new Set;for(let n of e.definitions)t.add(n.name);for(let n of e.bindings??[])t.add(n.name);return t}function u(e,t,n){let r=d(e,t),i=[],a=[];for(let[o,s]of n){let n=l(s);for(let c of s.references){if(c.resolved||n.has(c.name))continue;let s=r.get(_(o,c.name));s&&([e,t].some(e=>{let t=e.get(o);return t!=null&&!l(t).has(c.name)&&t.references.some(e=>e.name===c.name&&!e.resolved)})?a:i).push({category:`broken-reference`,severity:`error`,tier:s.crossFile?`project`:`file`,location:{file:o,line:c.line,symbol:c.name},provenance:{side:s.side,line:s.line},...s.crossFile&&s.targetFile?{definitionFile:s.targetFile}:{},message:`Reference "${c.name}" resolved before the merge (on ${s.side}) but no longer resolves.`})}}return{findings:i,preExisting:a}}function d(e,t){let n=new Map,r=(e,t,r,i,a,o)=>{let s=_(t,r);n.has(s)||n.set(s,{side:e,line:i,crossFile:a,targetFile:o})},i=[[`ours`,e],[`theirs`,t]];for(let[e,t]of i)for(let[n,i]of t){for(let t of i.definitions)r(e,n,t.name,t.line,!1);for(let t of i.bindings??[])r(e,n,t.name,t.line,!1)}for(let[e,t]of i)for(let[n,i]of t)for(let t of i.references)t.resolved&&r(e,n,t.name,t.line,t.target?t.target.file!==n:!0,t.target?.file);return n}function f(e,t){let n=p(t),r=new Set;for(let e of t)for(let t of e.keys())r.add(t);let i=[],a=[];for(let[o,s]of e){let e=r.has(o),l=new Map;for(let t of s.definitions){let r=l.get(t.name);if(r==null){l.set(t.name,t.line);continue}!e||n.has(_(o,t.name))||i.push({category:`duplicate-definition`,severity:`warning`,tier:`file`,location:{file:o,line:t.line,symbol:t.name},message:`Duplicate definition of "${t.name}" (also defined at line ${r}).`})}let u=e=>e.map(e=>({category:`syntax-error`,severity:`error`,tier:`file`,location:{file:o,line:e.line},message:e.message})),d=t.map(e=>e.get(o)?.syntaxErrors??[]),f=c(u(s.syntaxErrors),d.map(u),e=>e.message),p=d.some(e=>e.length>0);for(let e of f.kept)p&&(e.severity=`warning`),i.push(e);a.push(...f.preExisting)}return{findings:i,preExisting:a}}function p(e){let t=new Set;for(let n of e)for(let[e,r]of n){let n=new Set;for(let i of r.definitions){let r=_(e,i.name);n.has(r)&&t.add(r),n.add(r)}}return t}function m(e,t){let n=[];for(let r of e){if(!r.fallback||r.strategy!==`take-ours`&&r.strategy!==`take-theirs`)continue;let e=r.strategy===`take-ours`?`theirs`:`ours`,i=(e===`ours`?t.ours:t.theirs).get(r.filePath);if(i==null)continue;let a=h(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 h(e,t){let n=new Set(t.split(`
1
+ import{duplicateBlockFindings as e}from"../resolution/duplicate-blocks.js";import{toFileFinding as t}from"../validation/findings.js";import{buildSnapshots as n,carriesNoOwnContent as r,safeReadFile as i}from"./snapshot.js";async function a(e,t,a){let s=a.parser;if(!s)return{findings:[],checks:{referenceIntegrity:`skipped-no-parser`,structural:`skipped-no-parser`,validator:`skipped-no-validator`}};try{let c=t.detectionRefs??t.refs?await n(e,t,a):null;if(c){let t=await s.analyze(c.ours,a.signal),n=await s.analyze(c.theirs,a.signal),r=await s.analyze(c.result,a.signal);return{findings:[...u(t,n,r).findings,...f(r,[t,n]).findings,...m(e,c)],checks:{referenceIntegrity:o(c,s,[t,n,r]),structural:g(r)?`partial-unsupported-language`:`ran`,validator:`skipped-no-validator`},scope:{inScope:c.inScope,readable:c.readable,truncated:c.truncated,incomingDiscovered:c.incomingDiscovered}}}let l=new Map;for(let t of e){let e=r(t)?await i(a.git,t.filePath,{signal:a.signal}):t.content;e!=null&&l.set(t.filePath,e)}let d=await s.analyze(l,a.signal);return{findings:f(d,[]).findings,checks:{referenceIntegrity:`skipped-no-refs`,structural:g(d)?`partial-unsupported-language`:`ran`,validator:`skipped-no-validator`}}}catch(e){if(a.signal?.aborted)throw e;return{findings:[],checks:{referenceIntegrity:`errored`,structural:`errored`,validator:`skipped-no-validator`}}}}function o(e,t,n){return e.inScope>0&&e.readable===0?`skipped-unreadable-source`:t.capabilities?.crossFileResolution?n.some(g)?`partial-unsupported-language`:`ran`:`partial-no-cross-file-resolution`}async function s(n,r,i,a){let o=a.parser,s=[],l=[];if(o){let e=async e=>e==null?new Map:o.analyze(new Map([[n,e]]),a.signal),t=await e(r.ours),c=await e(r.theirs),d=await o.analyze(new Map([[n,i]]),a.signal),p=u(t,c,d),m=f(d,[t,c]);s=[...p.findings.filter(e=>e.tier===`file`),...m.findings],l.push(...p.preExisting.filter(e=>e.tier===`file`),...m.preExisting)}let d=[],p=a.validator;if(p){let e=async(e,r)=>{let i=await p.validate(new Map([[n,e]]),a.signal,{scope:`file`,role:r});return Array.isArray(i)?i.map(t):(a.onProgress?.({type:`validation:not-run`,filePath:n,scope:`file`,reason:i.reason}),[])},o=[];r.ours!=null&&o.push(await e(r.ours,`ours`)),r.theirs!=null&&o.push(await e(r.theirs,`theirs`));let s=c(await e(i,`result`),o,e=>JSON.stringify([e.category,e.message]));d=s.kept,l.push(...s.preExisting)}return{parser:s,validator:d,preExisting:l,text:e(n,i,r)}}function c(e,t,n){let r=new Map;for(let e of t){let t=new Map;for(let r of e){let e=n(r);t.set(e,(t.get(e)??0)+1)}for(let[e,n]of t)r.set(e,Math.max(r.get(e)??0,n))}let i=[],a=[];for(let t of[...e].sort((e,t)=>e.location.line-t.location.line)){let e=n(t),o=r.get(e)??0;o>0?(a.push(t),r.set(e,o-1)):i.push(t)}return{kept:i,preExisting:a}}function l(e){let t=new Set;for(let n of e.definitions)t.add(n.name);for(let n of e.bindings??[])t.add(n.name);return t}function u(e,t,n){let r=d(e,t),i=[],a=[];for(let[o,s]of n){let n=l(s);for(let c of s.references){if(c.resolved||n.has(c.name))continue;let s=r.get(_(o,c.name));s&&([e,t].some(e=>{let t=e.get(o);return t!=null&&!l(t).has(c.name)&&t.references.some(e=>e.name===c.name&&!e.resolved)})?a:i).push({category:`broken-reference`,severity:`error`,tier:s.crossFile?`project`:`file`,location:{file:o,line:c.line,symbol:c.name},provenance:{side:s.side,line:s.line},...s.crossFile&&s.targetFile?{definitionFile:s.targetFile}:{},message:`Reference "${c.name}" resolved before the merge (on ${s.side}) but no longer resolves.`})}}return{findings:i,preExisting:a}}function d(e,t){let n=new Map,r=(e,t,r,i,a,o)=>{let s=_(t,r);n.has(s)||n.set(s,{side:e,line:i,crossFile:a,targetFile:o})},i=[[`ours`,e],[`theirs`,t]];for(let[e,t]of i)for(let[n,i]of t){for(let t of i.definitions)r(e,n,t.name,t.line,!1);for(let t of i.bindings??[])r(e,n,t.name,t.line,!1)}for(let[e,t]of i)for(let[n,i]of t)for(let t of i.references)t.resolved&&r(e,n,t.name,t.line,t.target?t.target.file!==n:!0,t.target?.file);return n}function f(e,t){let n=p(t),r=new Set;for(let e of t)for(let t of e.keys())r.add(t);let i=[],a=[];for(let[o,s]of e){let e=r.has(o),l=new Map;for(let t of s.definitions){let r=l.get(t.name);if(r==null){l.set(t.name,t.line);continue}!e||n.has(_(o,t.name))||i.push({category:`duplicate-definition`,severity:`warning`,tier:`file`,location:{file:o,line:t.line,symbol:t.name},message:`Duplicate definition of "${t.name}" (also defined at line ${r}).`})}let u=e=>e.map(e=>({category:`syntax-error`,severity:`error`,tier:`file`,location:{file:o,line:e.line},message:e.message})),d=t.map(e=>e.get(o)?.syntaxErrors??[]),f=c(u(s.syntaxErrors),d.map(u),e=>e.message),p=d.some(e=>e.length>0);for(let e of f.kept)p&&(e.severity=`warning`),i.push(e);a.push(...f.preExisting)}return{findings:i,preExisting:a}}function p(e){let t=new Set;for(let n of e)for(let[e,r]of n){let n=new Set;for(let i of r.definitions){let r=_(e,i.name);n.has(r)&&t.add(r),n.add(r)}}return t}function m(e,t){let n=[];for(let r of e){if(!r.fallback||r.strategy!==`take-ours`&&r.strategy!==`take-theirs`)continue;let e=r.strategy===`take-ours`?`theirs`:`ours`,i=(e===`ours`?t.ours:t.theirs).get(r.filePath);if(i==null)continue;let a=h(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 h(e,t){let n=new Set(t.split(`
2
2
  `)),r=0;for(let t of e.split(`
3
3
  `))t.trim()!==``&&!n.has(t)&&r++;return r}function g(e){for(let t of e.values())if(!t.supported)return!0;return!1}function _(e,t){return`${e}\u0000${t}`}export{s as collectFileFindings,a as detect};
@@ -0,0 +1,24 @@
1
+ import { DetectionReport, Finding, ValidatorCheckStatus } from "../types/findings.js";
2
+ import { Resolution } from "../types/resolution.js";
3
+ import { Validator } from "../ports/validator.js";
4
+ import { ConflictProgressEvent } from "../types/events.js";
5
+ import { DetectionSnapshots } from "./snapshot.js";
6
+
7
+ //#region src/reference-integrity/validate-set.d.ts
8
+ interface SetValidation {
9
+ findings: Finding[];
10
+ status: ValidatorCheckStatus;
11
+ reason?: string;
12
+ }
13
+ interface SetValidationDeps {
14
+ validator?: Validator;
15
+ signal?: AbortSignal;
16
+ onProgress?: (event: ConflictProgressEvent) => void;
17
+ }
18
+ /** The one whole-set validator call of an operation: every resolution's final content, and the paths the
19
+ * step deletes. A resolution carrying no content of its own contributes the taken side's content from the
20
+ * snapshots when they hold it, and is left out otherwise. Never throws except on an aborted signal. */
21
+ declare function validateResolvedSet(resolutions: Resolution[], snapshots: DetectionSnapshots | null, deps: SetValidationDeps): Promise<SetValidation>;
22
+ declare function withSetValidation(detection: DetectionReport, set: SetValidation): DetectionReport;
23
+ //#endregion
24
+ export { SetValidation, SetValidationDeps, validateResolvedSet, withSetValidation };
@@ -0,0 +1 @@
1
+ import{toFileFinding as e}from"../validation/findings.js";import{carriesNoOwnContent as t}from"./snapshot.js";async function n(n,r,i){let a=i.validator;if(!a)return{findings:[],status:`skipped-no-validator`};let o=new Map,s=[];for(let e of n){if(e.strategy===`deleted`){s.push(e.filePath,...e.retiredPaths??[]);continue}let n=t(e)?r?.result.get(e.filePath):e.content;n!=null&&o.set(e.filePath,n)}if(o.size===0&&s.length===0)return{findings:[],status:`ran`};try{let t=await a.validate(o,i.signal,{scope:`set`,role:`result`,deleted:s});return Array.isArray(t)?{findings:t.map(e),status:`ran`}:(i.onProgress?.({type:`validation:not-run`,scope:`set`,reason:t.reason}),{findings:[],status:`not-run`,reason:t.reason})}catch(e){if(i.signal?.aborted)throw e;let t=e instanceof Error?e.message:String(e);return i.onProgress?.({type:`validation:failed`,scope:`set`,reason:t}),{findings:[],status:`errored`,reason:t}}}function r(e,t){return{...e,findings:[...e.findings,...t.findings],checks:{...e.checks,validator:t.status},...t.reason?{validatorReason:t.reason}:{}}}export{n as validateResolvedSet,r as withSetValidation};
@@ -1,8 +1,7 @@
1
1
  function e(e,t){let n=e.split(`
2
- `);if(t.kind===`insert`){if(t.startLine<1||t.startLine>n.length)return{valid:!1,reason:`out-of-bounds`};let e=n[t.startLine-1]??``;return e===t.anchor?{valid:!0}:{valid:!1,reason:`anchor-mismatch`,expected:t.anchor,actual:e}}if(t.kind===`verify`){let e=t.endLine??t.startLine;if(t.startLine<1||t.startLine>e||e>n.length)return{valid:!1,reason:`out-of-bounds`};let r=n.slice(t.startLine-1,e).join(`
3
- `);return r===t.anchor?{valid:!0}:{valid:!1,reason:`anchor-mismatch`,expected:t.anchor,actual:r}}let r=t.endLine;if(t.startLine<1||t.startLine>r||r>n.length)return{valid:!1,reason:`out-of-bounds`};let i=n.slice(t.startLine-1,r).join(`
4
- `);return i===t.anchor?{valid:!0}:{valid:!1,reason:`anchor-mismatch`,expected:t.anchor,actual:i}}function t(e,t){if(e.kind===`verify`||t.kind===`verify`||e.kind===`insert`&&t.kind===`insert`)return!1;let n=e.startLine,r=e.kind===`insert`?e.startLine:e.endLine,i=t.startLine;return n<=(t.kind===`insert`?t.startLine:t.endLine)&&i<=r}function n(n,r){let i=new Map,a=r.map(t=>e(n,t));for(let e=0;e<r.length;e++){let t=a[e];t&&!t.valid&&(t.reason===`anchor-mismatch`?i.set(e,{opIndex:e,status:`skipped`,reason:`anchor-mismatch`,expected:t.expected,actual:t.actual}):i.set(e,{opIndex:e,status:`skipped`,reason:t.reason}))}let o=[];for(let e=0;e<r.length;e++)i.has(e)||o.push(e);let s=new Set;for(let e=0;e<o.length;e++)for(let n=e+1;n<o.length;n++){let i=o[e],a=o[n],c=i===void 0?void 0:r[i],l=a===void 0?void 0:r[a];i!==void 0&&a!==void 0&&c&&l&&t(c,l)&&(s.add(i),s.add(a))}for(let e of s)i.set(e,{opIndex:e,status:`skipped`,reason:`overlap`});let c=o.filter(e=>!s.has(e));c.sort((e,t)=>(r[t]?.startLine??0)-(r[e]?.startLine??0));let l=n.split(`
5
- `),u=[];for(let e of c){let t=r[e];if(t){if(t.kind===`verify`){i.set(e,{opIndex:e,status:`verified`});continue}t.kind===`insert`?l.splice(t.startLine,0,...t.content.split(`
2
+ `);if(t.kind===`insert`){if(t.startLine<1||t.startLine>n.length)return{valid:!1,reason:`out-of-bounds`};let e=n[t.startLine-1]??``;return e===t.anchor?{valid:!0}:{valid:!1,reason:`anchor-mismatch`,expected:t.anchor,actual:e}}let r=t.endLine;if(t.startLine<1||t.startLine>r||r>n.length)return{valid:!1,reason:`out-of-bounds`};let i=n.slice(t.startLine-1,r).join(`
3
+ `);return i===t.anchor?{valid:!0}:{valid:!1,reason:`anchor-mismatch`,expected:t.anchor,actual:i}}function t(e,t){if(e.kind===`insert`&&t.kind===`insert`)return!1;let n=e.startLine,r=e.kind===`insert`?e.startLine:e.endLine,i=t.startLine;return n<=(t.kind===`insert`?t.startLine:t.endLine)&&i<=r}function n(n,r){let i=new Map,a=r.map(t=>e(n,t));for(let e=0;e<r.length;e++){let t=a[e];t&&!t.valid&&(t.reason===`anchor-mismatch`?i.set(e,{opIndex:e,status:`skipped`,reason:`anchor-mismatch`,expected:t.expected,actual:t.actual}):i.set(e,{opIndex:e,status:`skipped`,reason:t.reason}))}let o=[];for(let e=0;e<r.length;e++)i.has(e)||o.push(e);let s=new Set;for(let e=0;e<o.length;e++)for(let n=e+1;n<o.length;n++){let i=o[e],a=o[n],c=i===void 0?void 0:r[i],l=a===void 0?void 0:r[a];i!==void 0&&a!==void 0&&c&&l&&t(c,l)&&(s.add(i),s.add(a))}for(let e of s)i.set(e,{opIndex:e,status:`skipped`,reason:`overlap`});let c=o.filter(e=>!s.has(e));c.sort((e,t)=>(r[t]?.startLine??0)-(r[e]?.startLine??0));let l=n.split(`
4
+ `),u=[];for(let e of c){let t=r[e];t&&(t.kind===`insert`?l.splice(t.startLine,0,...t.content.split(`
6
5
  `)):t.kind===`replace`?l.splice(t.startLine-1,t.endLine-t.startLine+1,...t.content.split(`
7
- `)):l.splice(t.startLine-1,t.endLine-t.startLine+1),u.push(t),i.set(e,{opIndex:e,status:`applied`})}}let d=[];for(let e=0;e<r.length;e++){let t=i.get(e);t&&d.push(t)}return{content:l.join(`
6
+ `)):l.splice(t.startLine-1,t.endLine-t.startLine+1),u.push(t),i.set(e,{opIndex:e,status:`applied`}))}let d=[];for(let e=0;e<r.length;e++){let t=i.get(e);t&&d.push(t)}return{content:l.join(`
8
7
  `),edits:u,results:d}}export{n as applyEditOps};
@@ -4,8 +4,8 @@ import { Resolution, ResolutionContext } from "../types/resolution.js";
4
4
  import { ConflictModelPort } from "../ports/model.js";
5
5
  import { ParserPort } from "../ports/parser.js";
6
6
  import { Validator } from "../ports/validator.js";
7
- import { ParentContents } from "../reference-integrity/detect.js";
8
7
  import { ConflictProgressEvent } from "../types/events.js";
8
+ import { ParentContents } from "../reference-integrity/detect.js";
9
9
  import { StepConfig } from "../types/step.js";
10
10
  import { DetectionSnapshots } from "../reference-integrity/snapshot.js";
11
11
  import { ModelBudget } from "./model-budget.js";
@@ -1,2 +1,2 @@
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";async function s(e,n,s){let m=s.config?.fixLoopMaxIterations??3,h={parser:s.parser,validator:s.validator,signal:s.signal},g={content:e.content,findings:await(r=>t(e.filePath,n,r,h))(e.content)};g.findings.preExisting.length>0&&s.onProgress?.({type:`findings:pre-existing`,filePath:e.filePath,findings:g.findings.preExisting});let y=u(e,g.findings),b=l(g.findings);if(b.length===0)return f(s,e.filePath,0,0),d(e,y,g.findings);if(s.budget?.exhausted())return s.onBudgetExhausted?.(),f(s,e.filePath,0,b.length),c(d(e,y,g.findings),b);s.onProgress?.({type:`fix:started`,filePath:e.filePath,findingCount:b.length});let x=[],S=0,C=_(b);for(;S<m&&b.length>0;){if(s.budget?.exhausted()){s.onBudgetExhausted?.();break}S++;let o=await a(g.content,e.description,p(b),{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,h),u=l(c);if(v(g.findings,c)){s.onProgress?.({type:`refine:discarded`,filePath:e.filePath,reason:`Fix edits regressed the file (was ${b.length} error(s), now ${u.length}).`});break}g={content:o.content,findings:c},x.push(...o.edits),b=u;let d=_(b);if(d===C)break;C=d}f(s,e.filePath,S,b.length);let w=d({...e,content:g.content},y,g.findings),T=l(g.findings);return x.length===0?c(w,T):c({...w,strategy:`ai`,edits:[...e.edits??[],...x],confidence:Math.min(w.confidence,o.refineApplied)},T)}function c(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 l(e){return[...e.parser,...e.validator,...e.text].filter(e=>e.severity===`error`||e.category===`duplicate-block`)}function u(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 d(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 f(e,t,n,r){e.onProgress?.({type:`fix:completed`,filePath:t,iterations:n,remainingFindings:r})}function p(e){return e.slice(0,20).map(m)}function m(e){let t=e.location.symbol?h(e.location.symbol):void 0,n=t?`"${t}" (line ${e.location.line})`:`line ${e.location.line}`;return`${h(e.category)} at ${n}: ${h(e.message)}`}function h(e){return e.replace(/[\p{Cc}\p{Cf}]/gu,` `).replace(/\s+/g,` `).trim().slice(0,300)}function g(e){return JSON.stringify([e.category,e.location.file,e.location.symbol??``])}function _(e){return e.map(g).sort().join(`
2
- `)}function v(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(g));return i.some(e=>!a.has(g(e)))}async function y(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}),c=()=>i.onProgress?.({type:`fix:budget-exhausted`,spent:i.budget?.spent()??0,limit:i.budget?.limit()??0}),l={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(c)};return await x(t,s,l),s?S(t,r,s,o,l,i):n(t,r,o)}function b(e){return e.strategy===`ai`?!0:e.strategy===`take-ours`||e.strategy===`take-theirs`?!e.fallback&&!e.deliberateTake:!1}async function x(e,t,n){for(let r=0;r<e.length;r++){let i=e[r];if(b(i)){if(n.signal?.aborted)return;try{e[r]=await s(i,C(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 S(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=T(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 w(s,n,C(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 C(e,t){return{ours:e?.ours.get(t),theirs:e?.theirs.get(t)}}async function w(e,n,s,c,l){let f={parser:l.parser,validator:l.validator,signal:l.signal},m=await t(e.filePath,s,e.content,f),h=l.parser?await E(n,c,l.parser,l.signal):void 0,g=await a(e.content,e.description,p(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:h});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,f);return v(m,_)?(l.onProgress?.({type:`refine:discarded`,filePath:e.filePath,reason:`Cross-file fix left the file with a new or additional error.`}),e):d({...e,content:g.content,strategy:`ai`,edits:[...e.edits??[],...g.edits],confidence:Math.min(e.confidence,o.refineApplied)},u(e,m),_)}function T(e,t){let n=new Set(t.filter(b).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 E(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=h(e.name),n=e.kind?h(e.kind):void 0;return n?`${t} (${n}, line ${e.line})`:`${t} (line ${e.line})`});a.push({file:h(e),symbols:s})}return a.length>0?a:void 0}export{b as isFixable,C as parentsOf,s as runFixLoop,y as runFixPhase};
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};
@@ -125,11 +125,10 @@ When the user message says "Delete-modify conflict", one side deleted the file w
125
125
 
126
126
  ## Optional Refinement Phase (Phase 2)
127
127
 
128
- After you respond, an optional Phase 2 may run on the resolved file. Phase 2 can apply targeted line-level edits using an \`edit_file_lines\` tool with four operation kinds:
128
+ After you respond, an optional Phase 2 may run on the resolved file. Phase 2 can apply targeted line-level edits using an \`edit_file_lines\` tool with three operation kinds:
129
129
  - \`insert\`: add new lines after a specified line
130
130
  - \`replace\`: rewrite a line range
131
131
  - \`delete\`: remove a line range
132
- - \`verify\`: confirm specific lines are present without modifying them
133
132
 
134
133
  Phase 2 is driven by the \`followUpInstructions\` you provide. This means you should:
135
134
  - Prefer a clean base (file-level "ours"/"theirs" or simple per-chunk resolution) + specific followUpInstructions over large "merged" chunks that attempt to inline everything.
@@ -166,7 +165,6 @@ The \`edit_file_lines\` tool accepts an \`ops\` array of structured operations.
166
165
  - \`insert\`: inserts \`content\` after \`startLine\`. Anchor must equal the text of line \`startLine\`. No \`endLine\`.
167
166
  - \`replace\`: replaces lines \`startLine\`–\`endLine\` (inclusive) with \`content\`. Anchor must equal the joined text of those lines.
168
167
  - \`delete\`: deletes lines \`startLine\`–\`endLine\` (inclusive). Anchor must equal the joined text of those lines.
169
- - \`verify\`: asserts that lines \`startLine\`–\`endLine\` (or just \`startLine\`) match the anchor WITHOUT modifying them. Use to confirm critical content is present. No \`content\` field needed.
170
168
 
171
169
  All ops in a single call are validated atomically server-side. If any anchor mismatches or range is out-of-bounds, no changes are applied and the error is returned.
172
170
 
@@ -176,8 +174,7 @@ Example call:
176
174
  "ops": [
177
175
  { "kind": "insert", "startLine": 3, "anchor": "import { Foo } from './foo';", "content": "import { Bar } from './bar';" },
178
176
  { "kind": "replace", "startLine": 10, "endLine": 12, "anchor": " old line A\\n old line B\\n old line C", "content": " new merged line" },
179
- { "kind": "delete", "startLine": 20, "endLine": 21, "anchor": " duplicate line\\n duplicate line 2" },
180
- { "kind": "verify", "startLine": 15, "anchor": " const result = await fetchData();" }
177
+ { "kind": "delete", "startLine": 20, "endLine": 21, "anchor": " duplicate line\\n duplicate line 2" }
181
178
  ]
182
179
  }
183
180
  \`\`\`
@@ -25,7 +25,6 @@ interface RefinePassDeps {
25
25
  }
26
26
  interface RefinePassResult {
27
27
  edits: IntraFileEditOp[];
28
- verifies: number;
29
28
  content: string;
30
29
  description: string;
31
30
  usage?: AiTokenUsage;
@@ -1 +1 @@
1
- import{sumUsages as e}from"../resolution/metrics.js";import{buildRefinePrompt as t}from"./prompt.js";import{generateWithTimeout as n}from"./model-timeout.js";import{createRefineTools as r}from"./refine-tools.js";async function i(i,a,o,s){let c=s.refineMaxSteps??5,{system:l,userMessage:u}=t(i,a,o,s.relatedDefinitions,s.fileGuidance),{definitions:d,execute:f}=r(),p=[{role:`user`,content:u}],m=i,h=[],g=0,_=0,v=[],y=t=>({edits:h,verifies:g,content:m,description:t,usage:e(v)});for(let e=0;e<c;e++){if(s.budget?.exhausted())return y(``);let t=e+1,r=await n(s.model,{system:l,messages:[...p],tools:d,temperature:s.temperature??0,signal:s.signal},s.modelCallTimeoutMs);if(r.usage&&v.push(r.usage),r.toolCalls&&r.toolCalls.length>0){p.push({role:`assistant`,content:r.text,toolCalls:r.toolCalls});for(let e of r.toolCalls){let n=e.args.ops,r=Array.isArray(n)?n:[];s.onProgress?.({type:`refine:tool-call`,filePath:s.filePath,stepNumber:t,ops:r});let{content:i,edits:a,verifiedCount:o,feedback:c,isError:l}=f(e,m);if(l?_++:(m=i,h.push(...a),g+=o,_=0),s.onProgress?.({type:`refine:tool-result`,filePath:s.filePath,stepNumber:t,appliedCount:a.length,isError:l,feedback:c}),p.push({role:`tool`,toolCallId:e.id,toolName:e.name,content:c}),_>=3)return s.onProgress?.({type:`refine:bailed`,filePath:s.filePath,reason:`Stopped after ${_} consecutive apply errors.`}),y(``)}continue}let i=(r.text??``).trim();return s.onProgress?.({type:`refine:final-text`,filePath:s.filePath,text:i}),y(i)}return y(``)}export{i as runRefinePass};
1
+ import{sumUsages as e}from"../resolution/metrics.js";import{buildRefinePrompt as t}from"./prompt.js";import{generateWithTimeout as n}from"./model-timeout.js";import{createRefineTools as r}from"./refine-tools.js";async function i(i,a,o,s){let c=s.refineMaxSteps??5,{system:l,userMessage:u}=t(i,a,o,s.relatedDefinitions,s.fileGuidance),{definitions:d,execute:f}=r(),p=[{role:`user`,content:u}],m=i,h=[],g=0,_=[],v=t=>({edits:h,content:m,description:t,usage:e(_)});for(let e=0;e<c;e++){if(s.budget?.exhausted())return s.onProgress?.({type:`refine:bailed`,filePath:s.filePath,reason:`Stopped: model budget exhausted.`}),v(``);let t=e+1,r=await n(s.model,{system:l,messages:[...p],tools:d,temperature:s.temperature??0,signal:s.signal},s.modelCallTimeoutMs);if(r.usage&&_.push(r.usage),r.toolCalls&&r.toolCalls.length>0){p.push({role:`assistant`,content:r.text,toolCalls:r.toolCalls});for(let e of r.toolCalls){let n=e.args.ops,r=Array.isArray(n)?n:[];s.onProgress?.({type:`refine:tool-call`,filePath:s.filePath,stepNumber:t,ops:r});let{content:i,edits:a,feedback:o,isError:c}=f(e,m);if(c?g++:(m=i,h.push(...a),g=0),s.onProgress?.({type:`refine:tool-result`,filePath:s.filePath,stepNumber:t,appliedCount:a.length,isError:c,feedback:o}),p.push({role:`tool`,toolCallId:e.id,toolName:e.name,content:o}),g>=3)return s.onProgress?.({type:`refine:bailed`,filePath:s.filePath,reason:`Stopped after ${g} consecutive apply errors.`}),v(``)}continue}let i=(r.text??``).trim();return s.onProgress?.({type:`refine:final-text`,filePath:s.filePath,text:i}),v(i)}return s.onProgress?.({type:`refine:bailed`,filePath:s.filePath,reason:`Stopped after ${c} step(s) without a final reply.`}),v(``)}export{i as runRefinePass};
@@ -5,7 +5,6 @@ import { ToolCall, ToolDefinition } from "../types/tool.js";
5
5
  interface RefineToolExecuteResult {
6
6
  content: string;
7
7
  edits: IntraFileEditOp[];
8
- verifiedCount: number;
9
8
  feedback: string;
10
9
  isError: boolean;
11
10
  }
@@ -1,8 +1,8 @@
1
- import{truncateChars as e}from"../git/truncation.js";import{applyEditOps as t}from"../resolution/edit-apply.js";const n=new Set([`insert`,`replace`,`delete`,`verify`]);function r(e){if(typeof e!=`object`||!e)return{ok:!1,error:`"args" must be an object.`};let t=e;if(!Array.isArray(t.ops))return{ok:!1,error:`"ops" must be an array.`};if(t.ops.length===0)return{ok:!1,error:`"ops" must contain at least one item.`};let r=[];for(let e=0;e<t.ops.length;e++){let i=t.ops[e];if(typeof i!=`object`||!i)return{ok:!1,error:`op[${e}] must be an object.`};let a=i;if(!n.has(a.kind))return{ok:!1,error:`op[${e}].kind must be 'insert', 'replace', 'delete', or 'verify' (got ${JSON.stringify(a.kind)}).`};if(!Number.isInteger(a.startLine))return{ok:!1,error:`op[${e}].startLine must be an integer (got ${JSON.stringify(a.startLine)}).`};if(typeof a.anchor!=`string`)return{ok:!1,error:`op[${e}].anchor must be a string (got ${JSON.stringify(a.anchor)}).`};if(a.endLine!==void 0&&!Number.isInteger(a.endLine))return{ok:!1,error:`op[${e}].endLine must be an integer when provided (got ${JSON.stringify(a.endLine)}).`};if(a.content!==void 0&&typeof a.content!=`string`)return{ok:!1,error:`op[${e}].content must be a string when provided (got ${JSON.stringify(a.content)}).`};if(a.kind===`insert`){if(a.endLine!==void 0)return{ok:!1,error:`op[${e}].endLine must not be provided for 'insert' operations.`};if(a.content===void 0)return{ok:!1,error:`op[${e}].content is required for 'insert' operations.`};r.push({kind:`insert`,startLine:a.startLine,anchor:a.anchor,content:a.content})}else if(a.kind===`replace`){if(a.endLine===void 0)return{ok:!1,error:`op[${e}].endLine is required for 'replace' operations.`};if(a.content===void 0)return{ok:!1,error:`op[${e}].content is required for 'replace' operations.`};r.push({kind:`replace`,startLine:a.startLine,endLine:a.endLine,anchor:a.anchor,content:a.content})}else if(a.kind===`delete`){if(a.endLine===void 0)return{ok:!1,error:`op[${e}].endLine is required for 'delete' operations.`};if(a.content!==void 0)return{ok:!1,error:`op[${e}].content must not be provided for 'delete' operations.`};r.push({kind:`delete`,startLine:a.startLine,endLine:a.endLine,anchor:a.anchor})}else{if(a.content!==void 0)return{ok:!1,error:`op[${e}].content must not be provided for 'verify' operations.`};let t={kind:`verify`,startLine:a.startLine,anchor:a.anchor};a.endLine!==void 0&&(t.endLine=a.endLine),r.push(t)}}return{ok:!0,ops:r}}function i(){let n=[{name:`edit_file_lines`,description:`Apply structured edit operations to the resolved file. Each op targets a specific line range validated by an anchor. All ops in a single call are validated atomically — if any fail, no changes are applied.`,parameters:{type:`object`,required:[`ops`],properties:{ops:{type:`array`,minItems:1,description:`Edit operations, applied together. Each item is an object with "kind" (one of insert, replace, delete, verify), "startLine", "anchor", and, depending on kind, "endLine" and "content". insert: startLine, anchor, content (no endLine). replace: startLine, endLine, anchor, content. delete: startLine, endLine, anchor (no content). verify: startLine, anchor, optional endLine (no content).`,items:{type:`object`,required:[`kind`,`startLine`,`anchor`],properties:{kind:{type:`string`,description:`Operation: "insert" adds content after startLine (needs content, no endLine); "replace" rewrites startLine-endLine with content (needs endLine and content); "delete" removes startLine-endLine (needs endLine, no content); "verify" only checks that the lines match the anchor (no content, endLine optional).`},startLine:{type:`integer`,description:`1-based first line the operation targets.`},endLine:{type:`integer`,description:`1-based last line, inclusive. Required for replace and delete, optional for verify, not allowed for insert.`},anchor:{type:`string`,description:`Exact current text of the targeted lines (startLine only for insert), joined with newlines.`},content:{type:`string`,description:`New text. Required for insert and replace, not allowed for delete and verify.`}}}}}}}];function i(e,n){let i=r(e.args);if(!i.ok)return{content:n,edits:[],verifiedCount:0,feedback:i.error,isError:!0};let a=i.ops,{content:o,edits:s,results:c}=t(n,a),l=c.filter(e=>e.status===`skipped`);if(l.length>0){let e=n.split(`
2
- `);return{content:n,edits:[],verifiedCount:0,feedback:`Failed to apply ops:\n${l.map(t=>{if(t.status!==`skipped`)return``;let n=a[t.opIndex],r=n&&n.kind!==`insert`?n.endLine:void 0,i=n?.kind===`insert`||r===void 0?String(n?.startLine??`?`):`${n.startLine}-${r}`,o=` op[${t.opIndex}] (${n?.kind??`unknown`} startLine=${i}): ${t.reason}`;if(t.reason===`anchor-mismatch`&&n){let i=n.startLine,a=r??n.startLine,s=e.slice(i-1,a).map((e,t)=>` ${i+t}: ${e}`).join(`
1
+ import{truncateChars as e}from"../git/truncation.js";import{applyEditOps as t}from"../resolution/edit-apply.js";const n=new Set([`insert`,`replace`,`delete`]);function r(e){if(typeof e!=`object`||!e)return{ok:!1,error:`"args" must be an object.`};let t=e;if(!Array.isArray(t.ops))return{ok:!1,error:`"ops" must be an array.`};if(t.ops.length===0)return{ok:!1,error:`"ops" must contain at least one item.`};let r=[];for(let e=0;e<t.ops.length;e++){let i=t.ops[e];if(typeof i!=`object`||!i)return{ok:!1,error:`op[${e}] must be an object.`};let a=i;if(!n.has(a.kind))return{ok:!1,error:`op[${e}].kind must be 'insert', 'replace', or 'delete' (got ${JSON.stringify(a.kind)}). If no edit is needed, reply with text instead of calling the tool.`};if(!Number.isInteger(a.startLine))return{ok:!1,error:`op[${e}].startLine must be an integer (got ${JSON.stringify(a.startLine)}).`};if(typeof a.anchor!=`string`)return{ok:!1,error:`op[${e}].anchor must be a string (got ${JSON.stringify(a.anchor)}).`};if(a.endLine!==void 0&&!Number.isInteger(a.endLine))return{ok:!1,error:`op[${e}].endLine must be an integer when provided (got ${JSON.stringify(a.endLine)}).`};if(a.content!==void 0&&typeof a.content!=`string`)return{ok:!1,error:`op[${e}].content must be a string when provided (got ${JSON.stringify(a.content)}).`};if(a.kind===`insert`){if(a.endLine!==void 0)return{ok:!1,error:`op[${e}].endLine must not be provided for 'insert' operations.`};if(a.content===void 0)return{ok:!1,error:`op[${e}].content is required for 'insert' operations.`};r.push({kind:`insert`,startLine:a.startLine,anchor:a.anchor,content:a.content})}else if(a.kind===`replace`){if(a.endLine===void 0)return{ok:!1,error:`op[${e}].endLine is required for 'replace' operations.`};if(a.content===void 0)return{ok:!1,error:`op[${e}].content is required for 'replace' operations.`};r.push({kind:`replace`,startLine:a.startLine,endLine:a.endLine,anchor:a.anchor,content:a.content})}else if(a.kind===`delete`){if(a.endLine===void 0)return{ok:!1,error:`op[${e}].endLine is required for 'delete' operations.`};if(a.content!==void 0)return{ok:!1,error:`op[${e}].content must not be provided for 'delete' operations.`};r.push({kind:`delete`,startLine:a.startLine,endLine:a.endLine,anchor:a.anchor})}}return{ok:!0,ops:r}}function i(){let n=[{name:`edit_file_lines`,description:`Apply structured edit operations to the resolved file. Each op targets a specific line range validated by an anchor. All ops in a single call are validated atomically — if any fail, no changes are applied.`,parameters:{type:`object`,required:[`ops`],properties:{ops:{type:`array`,minItems:1,description:`Edit operations, applied together. Each item is an object with "kind" (one of insert, replace, delete), "startLine", "anchor", and, depending on kind, "endLine" and "content". insert: startLine, anchor, content (no endLine). replace: startLine, endLine, anchor, content. delete: startLine, endLine, anchor (no content).`,items:{type:`object`,required:[`kind`,`startLine`,`anchor`],properties:{kind:{type:`string`,enum:[`insert`,`replace`,`delete`],description:`Operation: "insert" adds content after startLine (needs content, no endLine); "replace" rewrites startLine-endLine with content (needs endLine and content); "delete" removes startLine-endLine (needs endLine, no content).`},startLine:{type:`integer`,description:`1-based first line the operation targets.`},endLine:{type:`integer`,description:`1-based last line, inclusive. Required for replace and delete, not allowed for insert.`},anchor:{type:`string`,description:`Exact current text of the targeted lines (startLine only for insert), joined with newlines.`},content:{type:`string`,description:`New text. Required for insert and replace, not allowed for delete.`}}}}}}}];function i(e,n){let i=r(e.args);if(!i.ok)return{content:n,edits:[],feedback:i.error,isError:!0};let a=i.ops,{content:o,edits:s,results:c}=t(n,a),l=c.filter(e=>e.status===`skipped`);if(l.length>0){let e=n.split(`
2
+ `);return{content:n,edits:[],feedback:`Failed to apply ops:\n${l.map(t=>{if(t.status!==`skipped`)return``;let n=a[t.opIndex],r=n&&n.kind!==`insert`?n.endLine:void 0,i=n?.kind===`insert`||r===void 0?String(n?.startLine??`?`):`${n.startLine}-${r}`,o=` op[${t.opIndex}] (${n?.kind??`unknown`} startLine=${i}): ${t.reason}`;if(t.reason===`anchor-mismatch`&&n){let i=n.startLine,a=r??n.startLine,s=e.slice(i-1,a).map((e,t)=>` ${i+t}: ${e}`).join(`
3
3
  `);return[o,` Your anchor:`,t.expected.split(`
4
4
  `).map(e=>` | ${e}`).join(`
5
5
  `),` Actual lines ${i}-${a} in current content:`,s].join(`
6
6
  `)}return t.reason===`out-of-bounds`&&n?`${o}\n Content has ${e.length} lines; you targeted startLine=${n.startLine}${r===void 0?``:`, endLine=${r}`}.`:o}).join(`
7
- `)}\n\nHint: anchor must EXACTLY match the content (including whitespace and indentation) at the specified line range. Re-read the resolved content and copy the lines verbatim.`,isError:!0}}let u=c.filter(e=>e.status===`verified`).length,d=o.split(`
8
- `).length;return{content:o,edits:s,verifiedCount:u,feedback:`Applied ${s.length} op(s). New content length: ${d} lines.`,isError:!1}}function a(t,n){let r=i(t,n);return{...r,feedback:e(r.feedback,`Retry with fewer operations and smaller targeted line ranges/anchors.`).content}}return{definitions:n,execute:a}}export{i as createRefineTools};
7
+ `)}\n\nHint: anchor must EXACTLY match the content (including whitespace and indentation) at the specified line range. Re-read the resolved content and copy the lines verbatim.`,isError:!0}}let u=o.split(`
8
+ `).length;return{content:o,edits:s,feedback:`Applied ${s.length} op(s). New content length: ${u} lines.`,isError:!1}}function a(t,n){let r=i(t,n);return{...r,feedback:e(r.feedback,`Retry with fewer operations and smaller targeted line ranges/anchors.`).content}}return{definitions:n,execute:a}}export{i as createRefineTools};
@@ -1 +1 @@
1
- import{ConflictGitPortMissingOpError as e}from"../ports/git.js";import{log as t,mergeBase as n}from"../git/port-dispatch.js";import{toFileFinding as r}from"../validation/findings.js";import{resolveChunkContent as i}from"../resolution/chunk-apply.js";import{createResolution as a}from"../resolution/create-resolution.js";import{mergeUsages as o}from"../resolution/metrics.js";import{checkResidualMarkers as s}from"../validation/markers.js";import{bracketImbalances as c,bracketInstruction as l,fileBracketFinding as u}from"./bracket-balance.js";import{matchFileGuidance as d}from"./file-guidance.js";import{runResolverLoop as f}from"./loop.js";import{lostAdditionFinding as p,lostAdditions as m,refineInstructions as h}from"./lost-additions.js";import{runRefinePass as g}from"./refine-pass.js";import{buildThreeWayDiff as _}from"./three-way-diff.js";const v={refineNoAction:.6,refineApplied:.9,errorsRemain:.5};async function y(e,t,n,y=[e.filePath]){let x={...t,fileGuidance:d(t.fileGuidance,y)},S=x;if(x.refs){let t=x.operation!==void 0&&x.upstreamCommits===void 0,r=x.threeWayDiff??await _(e.filePath,x.refs,{git:n.git,signal:n.signal}),i=t?await b(e.filePath,x.refs,x.operation===`merge`?x.refs.theirs:x.refs.ours,n):x.upstreamCommits;S={...x,threeWayDiff:r,...i===void 0?{}:{upstreamCommits:i}}}let{chunks:C,strategy:w,confidence:T,description:E,metrics:D,followUpInstructions:O}=await f(e,S,{model:n.model,git:n.git,config:n.config,onProgress:n.onProgress,signal:n.signal}),k=a(e,C,{confidence:T,description:E,metrics:D,...w?{fileStrategy:w}:{},...S.threeWayDiff?{threeWayDiff:S.threeWayDiff}:{}}),A=(t,r)=>{let i=S.threeWayDiff;if(e.type!==`text`||!i)return[];let a=[...m(i.oursDiff,`ours`,r),...m(i.theirsDiff,`theirs`,r)];for(let r of a)n.onProgress?.({type:`refine:lost-addition`,filePath:e.filePath,side:r.side,startLine:r.startLine,endLine:r.endLine,lines:r.lines.length,phase:t});return a},j=e.type===`text`?c(C,e):[];for(let t of j)n.onProgress?.({type:`refine:bracket-imbalance`,filePath:e.filePath,...t});let M=h(O,A(`before-refine`,k.content),j.map(e=>l(e,S.operation)));if(e.type===`text`&&M.length>0){n.onProgress?.({type:`refine:started`,filePath:e.filePath});let t=0;try{let r=await g(k.content,k.description,M,{model:n.model,signal:n.signal,temperature:n.config?.temperature,refineMaxSteps:n.config?.refineMaxSteps,modelCallTimeoutMs:n.config?.modelCallTimeoutMs,fileGuidance:S.fileGuidance,onProgress:n.onProgress,filePath:e.filePath});if(r.edits.length>0){let i=s(e.filePath,r.content);if(i.valid)k={...k,content:r.content,edits:r.edits,strategy:`ai`},t=r.edits.length;else{let t=(i.issues??[]).map(e=>e.message).join(` `);n.onProgress?.({type:`refine:discarded`,filePath:e.filePath,reason:`Edits introduced conflict markers: ${t}`})}}if(t>0){k={...k,confidence:Math.min(k.confidence,v.refineApplied)};let t=[];for(let n=0;n<e.markers.length;n++){let a=e.markers[n];if(!a||a.userResolution==null)continue;let o=i({markerIndex:n,...a.userResolution},a);o!==``&&(r.content.includes(o)||t.push(n))}t.length>0&&(n.onProgress?.({type:`refine:touched-user-region`,filePath:e.filePath,markerIndexes:t}),k={...k,note:`Refine modified user-resolved region(s): marker ${t.join(`, `)}.`})}else r.edits.length===0&&r.verifies>0||(k={...k,confidence:Math.min(k.confidence,v.refineNoAction)});if(r.usage){let e=o(k.metrics??{inputTokens:0,outputTokens:0},r.usage);k={...k,metrics:{...k.metrics,...e}}}n.onProgress?.({type:`refine:completed`,filePath:e.filePath,appliedCount:t})}catch(t){if(n.signal?.aborted)throw t;let r=t instanceof Error?t.message:String(t);n.onProgress?.({type:`refine:failed`,filePath:e.filePath,reason:r}),k={...k,confidence:Math.min(k.confidence,v.refineNoAction)}}}else n.onProgress?.({type:`refine:skipped`,filePath:e.filePath,reason:`no-follow-ups`});let N=A(`after-refine`,k.content);N.length>0&&(k={...k,findings:[...k.findings??[],...N.map(t=>p(t,e.filePath))]});let P=e.type===`text`?u(e,k.content,S.operation,j):void 0;if(P&&(k={...k,findings:[...k.findings??[],P]}),n.validator&&e.type===`text`)try{let t=await n.validator.validate(new Map([[e.filePath,k.content]]),n.signal);t.length>0&&(k={...k,findings:[...k.findings??[],...t.map(r)]})}catch(t){if(n.signal?.aborted)throw t;let r=t instanceof Error?t.message:String(t);n.onProgress?.({type:`validation:failed`,filePath:e.filePath,reason:r})}return n.onProgress?.({type:`resolver:completed`,filePath:e.filePath,stepCount:D.stepCount??0}),k}async function b(r,i,a,o){try{let e=i.base??await n(o.git,i.ours,i.theirs,{signal:o.signal});return await t(o.git,{ref:`${e}..${a}`,path:r,signal:o.signal})}catch(t){if(o.signal?.aborted||!(t instanceof e))throw t;return}}export{v as CONFIDENCE_CAPS,y as resolveConflict};
1
+ import{ConflictGitPortMissingOpError as e}from"../ports/git.js";import{log as t,mergeBase as n}from"../git/port-dispatch.js";import{toFileFinding as r}from"../validation/findings.js";import{resolveChunkContent as i}from"../resolution/chunk-apply.js";import{createResolution as a}from"../resolution/create-resolution.js";import{mergeUsages as o}from"../resolution/metrics.js";import{checkResidualMarkers as s}from"../validation/markers.js";import{bracketImbalances as c,bracketInstruction as l,fileBracketFinding as u}from"./bracket-balance.js";import{matchFileGuidance as d}from"./file-guidance.js";import{runResolverLoop as f}from"./loop.js";import{lostAdditionFinding as p,lostAdditions as m,refineInstructions as h}from"./lost-additions.js";import{runRefinePass as g}from"./refine-pass.js";import{buildThreeWayDiff as _}from"./three-way-diff.js";const v={refineNoAction:.6,refineApplied:.9,errorsRemain:.5};async function y(e,t,n,y=[e.filePath]){let x={...t,fileGuidance:d(t.fileGuidance,y)},S=x;if(x.refs){let t=x.operation!==void 0&&x.upstreamCommits===void 0,r=x.threeWayDiff??await _(e.filePath,x.refs,{git:n.git,signal:n.signal}),i=t?await b(e.filePath,x.refs,x.operation===`merge`?x.refs.theirs:x.refs.ours,n):x.upstreamCommits;S={...x,threeWayDiff:r,...i===void 0?{}:{upstreamCommits:i}}}let{chunks:C,strategy:w,confidence:T,description:E,metrics:D,followUpInstructions:O}=await f(e,S,{model:n.model,git:n.git,config:n.config,onProgress:n.onProgress,signal:n.signal}),k=a(e,C,{confidence:T,description:E,metrics:D,...w?{fileStrategy:w}:{},...S.threeWayDiff?{threeWayDiff:S.threeWayDiff}:{}}),A=(t,r)=>{let i=S.threeWayDiff;if(e.type!==`text`||!i)return[];let a=[...m(i.oursDiff,`ours`,r),...m(i.theirsDiff,`theirs`,r)];for(let r of a)n.onProgress?.({type:`refine:lost-addition`,filePath:e.filePath,side:r.side,startLine:r.startLine,endLine:r.endLine,lines:r.lines.length,phase:t});return a},j=e.type===`text`?c(C,e):[];for(let t of j)n.onProgress?.({type:`refine:bracket-imbalance`,filePath:e.filePath,...t});let M=h(O,A(`before-refine`,k.content),j.map(e=>l(e,S.operation)));if(e.type===`text`&&M.length>0){n.onProgress?.({type:`refine:started`,filePath:e.filePath});let t=0;try{let r=await g(k.content,k.description,M,{model:n.model,signal:n.signal,temperature:n.config?.temperature,refineMaxSteps:n.config?.refineMaxSteps,modelCallTimeoutMs:n.config?.modelCallTimeoutMs,fileGuidance:S.fileGuidance,onProgress:n.onProgress,filePath:e.filePath});if(r.edits.length>0){let i=s(e.filePath,r.content);if(i.valid)k={...k,content:r.content,edits:r.edits,strategy:`ai`},t=r.edits.length;else{let t=(i.issues??[]).map(e=>e.message).join(` `);n.onProgress?.({type:`refine:discarded`,filePath:e.filePath,reason:`Edits introduced conflict markers: ${t}`})}}if(t>0){k={...k,confidence:Math.min(k.confidence,v.refineApplied)};let t=[];for(let n=0;n<e.markers.length;n++){let a=e.markers[n];if(!a||a.userResolution==null)continue;let o=i({markerIndex:n,...a.userResolution},a);o!==``&&(r.content.includes(o)||t.push(n))}t.length>0&&(n.onProgress?.({type:`refine:touched-user-region`,filePath:e.filePath,markerIndexes:t}),k={...k,note:`Refine modified user-resolved region(s): marker ${t.join(`, `)}.`})}else k={...k,confidence:Math.min(k.confidence,v.refineNoAction)};if(r.usage){let e=o(k.metrics??{inputTokens:0,outputTokens:0},r.usage);k={...k,metrics:{...k.metrics,...e}}}n.onProgress?.({type:`refine:completed`,filePath:e.filePath,appliedCount:t})}catch(t){if(n.signal?.aborted)throw t;let r=t instanceof Error?t.message:String(t);n.onProgress?.({type:`refine:failed`,filePath:e.filePath,reason:r}),k={...k,confidence:Math.min(k.confidence,v.refineNoAction)}}}else n.onProgress?.({type:`refine:skipped`,filePath:e.filePath,reason:`no-follow-ups`});let N=A(`after-refine`,k.content);N.length>0&&(k={...k,findings:[...k.findings??[],...N.map(t=>p(t,e.filePath))]});let P=e.type===`text`?u(e,k.content,S.operation,j):void 0;if(P&&(k={...k,findings:[...k.findings??[],P]}),n.validator&&e.type===`text`)try{let t=await n.validator.validate(new Map([[e.filePath,k.content]]),n.signal,{scope:`file`,role:`result`});Array.isArray(t)?t.length>0&&(k={...k,findings:[...k.findings??[],...t.map(r)]}):n.onProgress?.({type:`validation:not-run`,filePath:e.filePath,scope:`file`,reason:t.reason})}catch(t){if(n.signal?.aborted)throw t;let r=t instanceof Error?t.message:String(t);n.onProgress?.({type:`validation:failed`,filePath:e.filePath,scope:`file`,reason:r})}return n.onProgress?.({type:`resolver:completed`,filePath:e.filePath,stepCount:D.stepCount??0}),k}async function b(r,i,a,o){try{let e=i.base??await n(o.git,i.ours,i.theirs,{signal:o.signal});return await t(o.git,{ref:`${e}..${a}`,path:r,signal:o.signal})}catch(t){if(o.signal?.aborted||!(t instanceof e))throw t;return}}export{v as CONFIDENCE_CAPS,y as resolveConflict};
@@ -1 +1 @@
1
- import{AIError as e}from"../errors/ai.js";import{ConflictError as t}from"../errors/conflict.js";import{mergeAttributes as n,readFile as r,unmergedEntries as i,unmergedStages as a}from"../git/port-dispatch.js";import{extractConflict as o,extractConflictWithSource as s}from"../extraction/extract.js";import{duplicateBlockFindings as c}from"../resolution/duplicate-blocks.js";import{buildSnapshots as l}from"../reference-integrity/snapshot.js";import{detect as u}from"../reference-integrity/detect.js";import{createResolution as d}from"../resolution/create-resolution.js";import{detectFallbackCodeLoss as f}from"../resolution/detect-code-loss.js";import{takeOurs as p,takeTheirs as m}from"../resolution/take-strategy.js";import{buildPathMatcher as h,guidanceNamesFile as g,matchFileGuidance as _}from"./file-guidance.js";import{resolveConflict as v}from"./resolve-conflict.js";import{classifiedResolution as y,deleteModifyResolution as b,noMarkerResolution as x}from"./entry-decision.js";import{isFixable as S,parentsOf as C,runFixPhase as w}from"./fix-loop.js";import{classifyUnmergedEntries as T,loneSideStageMode as E}from"./mechanical.js";import{createModelBudget as D}from"./model-budget.js";import{chooseSurvivingName as O}from"./rename-choice.js";import{findRenameRenameGroup as k}from"./rename-group.js";function A(e){let t=e.map(e=>({isMatch:h(e.match),strategy:e.strategy}));return e=>{for(let{isMatch:n,strategy:r}of t)if(n(e))return r}}async function j(e,t){let r=e.config?.rules??[],a=e.config?.defaultStrategy??`ai`,o=A(r),c=await N(await i(e.git,{signal:e.signal}),e),l=T(c),u=D(e.config?.maxModelCalls),f=u.wrap(e.model),h={...t??{}},_=[],v=[],S=[],C=new Set,w,O=()=>(w??=n(e.git,c.map(e=>e.path),{signal:e.signal}).catch(()=>new Map),w),j=k(c),R=j?[j.basePath,j.oursPath,j.theirsPath]:[],H=j!==null&&R.every(e=>(o(e)??a)===`ai`),U=new Set;if(j&&H&&!e.signal?.aborted&&await F(j,e)){await I(j,c,h,{...e,model:f},{resolutions:_,errors:v,skipped:S});for(let e of R)U.add(e)}for(let t of c){if(e.signal?.aborted)break;if(U.has(t.path))continue;let n=o(t.path)??a;if(n===`skip`){S.push({filePath:t.path,reason:`excluded`}),e.onProgress?.({type:`conflict:excluded`,filePath:t.path});continue}let r=l.get(t.path),i=y(t.path,r,n!==`ai`);if(i){M(i,_,e),i.strategy===`mechanical`&&t.stages!=null&&E(t.stages)===`120000`&&C.add(t.path);continue}if(n===`ai`&&r?.kind===`skip`){S.push({filePath:t.path,reason:r.reason,entryReason:t.reason,stages:t.stages}),e.onProgress?.({type:`conflict:skipped`,filePath:t.path,reason:r.reason,entryReason:t.reason});continue}let c,u;try{({conflict:c,rawContent:u}=await s(t.path,{git:e.git,signal:e.signal},t.reason))}catch(n){let r=V(n);v.push({filePath:t.path,error:r,reason:B(r,e.signal)});continue}if(!c){if(n===`take-ours`||n===`take-theirs`||n===`deleted`){let r=x(t.path,n,t.stages);M({...r,...r.strategy===`deleted`?{}:{deliberateTake:!0}},_,e);continue}let r=await z(t,u,O)?`unmergeable`:`no-markers`;S.push({filePath:t.path,reason:r,entryReason:t.reason,stages:t.stages}),e.onProgress?.({type:`conflict:skipped`,filePath:t.path,reason:r,entryReason:t.reason});continue}e.onProgress?.({type:`conflict:found`,filePath:c.filePath,conflictType:c.type,markerCount:c.markers.length});let w=null;w=n===`take-ours`?{...p(c),deliberateTake:!0}:n===`take-theirs`?{...m(c),deliberateTake:!0}:n===`deleted`?d(c,[],{confidence:1,fileStrategy:`deleted`}):c.type===`delete-modify`&&!g(c.filePath,h)?await b(c,h,e):await L(c,h,{...e,model:f},v),w&&(h.previousResolutions=[...h.previousResolutions??[],w],M(w,_,e))}return{resolutions:_,errors:v,skipped:S,detection:await P(_,h,S,v,C,u,{...e,model:f})}}function M(e,t,n){t.push(e),n.onProgress?.({type:`resolution:applied`,filePath:e.filePath,strategy:e.strategy,confidence:e.confidence,description:e.description})}async function N(e,t){if(!t.git.exec||!e.some(e=>e.stages===void 0))return e;let n=await a(t.git,{signal:t.signal});return e.map(e=>{if(e.stages)return e;let t=n.get(e.path);return t?{...e,stages:t}:e})}async function P(e,t,n,r,i,a,o){let s=new Set([...n.map(e=>e.filePath),...r.map(e=>e.filePath),...e.flatMap(e=>e.retiredPaths??[]),...i]),d={git:o.git,parser:o.parser,signal:o.signal,excludePaths:s,maxScopeFiles:o.config?.maxDetectionScopeFiles},f;if(o.signal?.aborted)f={findings:[],checks:{referenceIntegrity:`errored`,structural:`errored`}};else if(o.config?.fix)try{f=await w(e,t,{model:o.model,git:o.git,parser:o.parser,validator:o.validator,config:o.config,onProgress:o.onProgress,signal:o.signal,budget:a,excludePaths:s})}catch(n){if(o.signal?.aborted)throw n;o.onProgress?.({type:`fix:failed`,reason:V(n).message}),f=await u(e,t,d),f={...f,checks:{referenceIntegrity:`errored`,structural:`errored`}}}else{f=await u(e,t,d);let n=await l(e,t,d);for(let t of e.filter(S))f.findings.push(...c(t.filePath,t.content,C(n,t.filePath)))}let p=e.flatMap(e=>e.findings??[]);return{...f,findings:H([...f.findings,...p])}}async function F(e,t){try{return await r(t.git,e.oursPath,{signal:t.signal})===await r(t.git,e.theirsPath,{signal:t.signal})}catch(e){if(t.signal?.aborted)throw V(e);return!1}}async function I(e,t,n,r,i){let a=e=>{for(let n of e){let e=t.find(e=>e.path===n);i.skipped.push({filePath:n,reason:`rename-choice`,entryReason:e?.reason,stages:e?.stages}),r.onProgress?.({type:`conflict:skipped`,filePath:n,reason:`rename-choice`,entryReason:e?.reason})}},s={...n,fileGuidance:_(n.fileGuidance,[e.basePath,e.oursPath,e.theirsPath])},c;try{c=await O(e,s,{model:r.model,config:r.config,signal:r.signal})}catch(t){if(r.signal?.aborted)throw V(t);a([e.basePath,e.oursPath,e.theirsPath]);return}let l=c.path,u=[l===e.oursPath?e.theirsPath:e.oursPath,e.basePath],d=t.find(e=>e.path===l),f;try{f=await o(l,{git:r.git,signal:r.signal},d?.reason)}catch(e){if(r.signal?.aborted)throw V(e);let t=V(e);i.errors.push({filePath:l,error:t,reason:B(t,r.signal)}),a(u);return}let p;if(!f)p={filePath:l,content:``,strategy:`mechanical`,confidence:c.confidence,description:c.description??`Both sides renamed ${e.basePath}; kept ${l}. Git settled the content itself, which is staged unchanged.`,retiredPaths:u};else{r.onProgress?.({type:`conflict:found`,filePath:f.filePath,conflictType:f.type,markerCount:f.markers.length});let t=await L(f,n,r,i.errors,[e.basePath,e.oursPath,e.theirsPath]);if(!t){a(u);return}let o=[t.note,c.description].filter(e=>!!e).join(` `);p={...t,confidence:Math.min(t.confidence,c.confidence),retiredPaths:u,...o?{note:o}:{}}}i.resolutions.push(p),n.previousResolutions=[...n.previousResolutions??[],p],r.onProgress?.({type:`resolution:applied`,filePath:p.filePath,strategy:p.strategy,confidence:p.confidence,description:p.description})}async function L(n,r,i,a,o){let s,c={generate:async e=>{try{return await i.model.generate(e)}catch(e){throw s=e,e}}};try{return await v(n,r,{model:c,git:i.git,config:i.config,onProgress:i.onProgress,signal:i.signal,validator:i.validator},o)}catch(r){let o=V(r);if(i.signal?.aborted)throw o;let c=r===s||o instanceof DOMException&&o.name===`TimeoutError`?`model-error`:o instanceof e&&o.code===`VALIDATION_EXHAUSTED`?`validation-exhausted`:`unknown`,l=i.config?.fallbackStrategy;if(!l)return a.push({filePath:n.filePath,error:o,reason:c}),i.onProgress?.({type:`resolution:failed`,filePath:n.filePath,error:o}),null;let u=i.config?.allowLossyFallback?null:f(n,l);if(u){let e=new t(`CODE_LOSS_RISK`,`AI resolution failed and the '${l}' fallback would take one side at every conflict marker in ${n.filePath} without a decision, discarding the ${u.discardedSide} side's conflicting changes. Stopping for manual resolution. Resolver failure: ${o.message}`,{cause:o});return a.push({filePath:n.filePath,error:e,reason:`code-loss-risk`,causeReason:c}),i.onProgress?.({type:`resolution:failed`,filePath:n.filePath,error:e}),null}return i.onProgress?.({type:`resolution:fallback`,filePath:n.filePath,error:o,cause:o instanceof e?o.code:void 0}),{...l===`take-ours`?p(n):m(n),confidence:0,fallback:!0}}}const R=new Set([`unspecified`,`set`,`text`]);async function z(e,t,n){if(e.stages?.stage2==null||e.stages?.stage3==null)return!1;if(t.includes(`\0`))return!0;let r=(await n()).get(e.path);return r!==void 0&&!R.has(r)}function B(e,n){return n?.aborted?`aborted`:e instanceof t&&e.code===`PARSE_FAILED`?`parse-failed`:`read-failed`}function V(e){return e instanceof Error?e:Error(String(e))}function H(e){let t=new Set,n=[];for(let r of e){let{category:e,location:i}=r,a=`${e}\u0000${i.file}\u0000${i.line}\u0000${i.symbol??``}`;t.has(a)||(t.add(a),n.push(r))}return n}export{j as resolveConflicts};
1
+ import{AIError as e}from"../errors/ai.js";import{ConflictError as t}from"../errors/conflict.js";import{mergeAttributes as n,readFile as r,unmergedEntries as i,unmergedStages as a}from"../git/port-dispatch.js";import{extractConflict as o,extractConflictWithSource as s}from"../extraction/extract.js";import{duplicateBlockFindings as c}from"../resolution/duplicate-blocks.js";import{buildSnapshots as l}from"../reference-integrity/snapshot.js";import{detect as u}from"../reference-integrity/detect.js";import{createResolution as d}from"../resolution/create-resolution.js";import{detectFallbackCodeLoss as f}from"../resolution/detect-code-loss.js";import{takeOurs as p,takeTheirs as m}from"../resolution/take-strategy.js";import{buildPathMatcher as h,guidanceNamesFile as g,matchFileGuidance as _}from"./file-guidance.js";import{resolveConflict as v}from"./resolve-conflict.js";import{validateResolvedSet as y,withSetValidation as b}from"../reference-integrity/validate-set.js";import{classifiedResolution as x,deleteModifyResolution as S,noMarkerResolution as C}from"./entry-decision.js";import{isFixable as w,parentsOf as T,runFixPhase as E}from"./fix-loop.js";import{classifyUnmergedEntries as D,loneSideStageMode as O}from"./mechanical.js";import{createModelBudget as k}from"./model-budget.js";import{chooseSurvivingName as A}from"./rename-choice.js";import{findRenameRenameGroup as j}from"./rename-group.js";function M(e){let t=e.map(e=>({isMatch:h(e.match),strategy:e.strategy}));return e=>{for(let{isMatch:n,strategy:r}of t)if(n(e))return r}}async function N(e,t){let r=e.config?.rules??[],a=e.config?.defaultStrategy??`ai`,o=M(r),c=await F(await i(e.git,{signal:e.signal}),e),l=D(c),u=k(e.config?.maxModelCalls),f=u.wrap(e.model),h={...t??{}},_=[],v=[],y=[],b=new Set,w,T=()=>(w??=n(e.git,c.map(e=>e.path),{signal:e.signal}).catch(()=>new Map),w),E=j(c),A=E?[E.basePath,E.oursPath,E.theirsPath]:[],N=E!==null&&A.every(e=>(o(e)??a)===`ai`),B=new Set;if(E&&N&&!e.signal?.aborted&&await L(E,e)){await R(E,c,h,{...e,model:f},{resolutions:_,errors:v,skipped:y});for(let e of A)B.add(e)}for(let t of c){if(e.signal?.aborted)break;if(B.has(t.path))continue;let n=o(t.path)??a;if(n===`skip`){y.push({filePath:t.path,reason:`excluded`}),e.onProgress?.({type:`conflict:excluded`,filePath:t.path});continue}let r=l.get(t.path),i=x(t.path,r,n!==`ai`);if(i){P(i,_,e),i.strategy===`mechanical`&&t.stages!=null&&O(t.stages)===`120000`&&b.add(t.path);continue}if(n===`ai`&&r?.kind===`skip`){y.push({filePath:t.path,reason:r.reason,entryReason:t.reason,stages:t.stages}),e.onProgress?.({type:`conflict:skipped`,filePath:t.path,reason:r.reason,entryReason:t.reason});continue}let c,u;try{({conflict:c,rawContent:u}=await s(t.path,{git:e.git,signal:e.signal},t.reason))}catch(n){let r=U(n);v.push({filePath:t.path,error:r,reason:H(r,e.signal)});continue}if(!c){if(n===`take-ours`||n===`take-theirs`||n===`deleted`){let r=C(t.path,n,t.stages);P({...r,...r.strategy===`deleted`?{}:{deliberateTake:!0}},_,e);continue}let r=await V(t,u,T)?`unmergeable`:`no-markers`;y.push({filePath:t.path,reason:r,entryReason:t.reason,stages:t.stages}),e.onProgress?.({type:`conflict:skipped`,filePath:t.path,reason:r,entryReason:t.reason});continue}e.onProgress?.({type:`conflict:found`,filePath:c.filePath,conflictType:c.type,markerCount:c.markers.length});let w=null;w=n===`take-ours`?{...p(c),deliberateTake:!0}:n===`take-theirs`?{...m(c),deliberateTake:!0}:n===`deleted`?d(c,[],{confidence:1,fileStrategy:`deleted`}):c.type===`delete-modify`&&!g(c.filePath,h)?await S(c,h,e):await z(c,h,{...e,model:f},v),w&&(h.previousResolutions=[...h.previousResolutions??[],w],P(w,_,e))}return{resolutions:_,errors:v,skipped:y,detection:await I(_,h,y,v,b,u,{...e,model:f})}}function P(e,t,n){t.push(e),n.onProgress?.({type:`resolution:applied`,filePath:e.filePath,strategy:e.strategy,confidence:e.confidence,description:e.description})}async function F(e,t){if(!t.git.exec||!e.some(e=>e.stages===void 0))return e;let n=await a(t.git,{signal:t.signal});return e.map(e=>{if(e.stages)return e;let t=n.get(e.path);return t?{...e,stages:t}:e})}async function I(e,t,n,r,i,a,o){let s=new Set([...n.map(e=>e.filePath),...r.map(e=>e.filePath),...e.flatMap(e=>e.retiredPaths??[]),...i]),d={git:o.git,parser:o.parser,signal:o.signal,excludePaths:s,maxScopeFiles:o.config?.maxDetectionScopeFiles},f;if(o.signal?.aborted)f={findings:[],checks:{referenceIntegrity:`errored`,structural:`errored`,validator:`errored`}};else if(o.config?.fix)try{f=await E(e,t,{model:o.model,git:o.git,parser:o.parser,validator:o.validator,config:o.config,onProgress:o.onProgress,signal:o.signal,budget:a,excludePaths:s})}catch(n){if(o.signal?.aborted)throw n;o.onProgress?.({type:`fix:failed`,reason:U(n).message}),f=await u(e,t,d),f={...f,checks:{referenceIntegrity:`errored`,structural:`errored`,validator:o.validator?`errored`:`skipped-no-validator`}}}else{f=await u(e,t,d);let n=await l(e,t,d);for(let t of e.filter(w))f.findings.push(...c(t.filePath,t.content,T(n,t.filePath)));f=b(f,await y(e,n,{validator:o.validator,signal:o.signal,onProgress:o.onProgress}))}let p=e.flatMap(e=>e.findings??[]);return{...f,findings:W([...f.findings,...p])}}async function L(e,t){try{return await r(t.git,e.oursPath,{signal:t.signal})===await r(t.git,e.theirsPath,{signal:t.signal})}catch(e){if(t.signal?.aborted)throw U(e);return!1}}async function R(e,t,n,r,i){let a=e=>{for(let n of e){let e=t.find(e=>e.path===n);i.skipped.push({filePath:n,reason:`rename-choice`,entryReason:e?.reason,stages:e?.stages}),r.onProgress?.({type:`conflict:skipped`,filePath:n,reason:`rename-choice`,entryReason:e?.reason})}},s={...n,fileGuidance:_(n.fileGuidance,[e.basePath,e.oursPath,e.theirsPath])},c;try{c=await A(e,s,{model:r.model,config:r.config,signal:r.signal})}catch(t){if(r.signal?.aborted)throw U(t);a([e.basePath,e.oursPath,e.theirsPath]);return}let l=c.path,u=[l===e.oursPath?e.theirsPath:e.oursPath,e.basePath],d=t.find(e=>e.path===l),f;try{f=await o(l,{git:r.git,signal:r.signal},d?.reason)}catch(e){if(r.signal?.aborted)throw U(e);let t=U(e);i.errors.push({filePath:l,error:t,reason:H(t,r.signal)}),a(u);return}let p;if(!f)p={filePath:l,content:``,strategy:`mechanical`,confidence:c.confidence,description:c.description??`Both sides renamed ${e.basePath}; kept ${l}. Git settled the content itself, which is staged unchanged.`,retiredPaths:u};else{r.onProgress?.({type:`conflict:found`,filePath:f.filePath,conflictType:f.type,markerCount:f.markers.length});let t=await z(f,n,r,i.errors,[e.basePath,e.oursPath,e.theirsPath]);if(!t){a(u);return}let o=[t.note,c.description].filter(e=>!!e).join(` `);p={...t,confidence:Math.min(t.confidence,c.confidence),retiredPaths:u,...o?{note:o}:{}}}i.resolutions.push(p),n.previousResolutions=[...n.previousResolutions??[],p],r.onProgress?.({type:`resolution:applied`,filePath:p.filePath,strategy:p.strategy,confidence:p.confidence,description:p.description})}async function z(n,r,i,a,o){let s,c={generate:async e=>{try{return await i.model.generate(e)}catch(e){throw s=e,e}}};try{return await v(n,r,{model:c,git:i.git,config:i.config,onProgress:i.onProgress,signal:i.signal,validator:i.validator},o)}catch(r){let o=U(r);if(i.signal?.aborted)throw o;let c=r===s||o instanceof DOMException&&o.name===`TimeoutError`?`model-error`:o instanceof e&&o.code===`VALIDATION_EXHAUSTED`?`validation-exhausted`:`unknown`,l=i.config?.fallbackStrategy;if(!l)return a.push({filePath:n.filePath,error:o,reason:c}),i.onProgress?.({type:`resolution:failed`,filePath:n.filePath,error:o}),null;let u=i.config?.allowLossyFallback?null:f(n,l);if(u){let e=new t(`CODE_LOSS_RISK`,`AI resolution failed and the '${l}' fallback would take one side at every conflict marker in ${n.filePath} without a decision, discarding the ${u.discardedSide} side's conflicting changes. Stopping for manual resolution. Resolver failure: ${o.message}`,{cause:o});return a.push({filePath:n.filePath,error:e,reason:`code-loss-risk`,causeReason:c}),i.onProgress?.({type:`resolution:failed`,filePath:n.filePath,error:e}),null}return i.onProgress?.({type:`resolution:fallback`,filePath:n.filePath,error:o,cause:o instanceof e?o.code:void 0}),{...l===`take-ours`?p(n):m(n),confidence:0,fallback:!0}}}const B=new Set([`unspecified`,`set`,`text`]);async function V(e,t,n){if(e.stages?.stage2==null||e.stages?.stage3==null)return!1;if(t.includes(`\0`))return!0;let r=(await n()).get(e.path);return r!==void 0&&!B.has(r)}function H(e,n){return n?.aborted?`aborted`:e instanceof t&&e.code===`PARSE_FAILED`?`parse-failed`:`read-failed`}function U(e){return e instanceof Error?e:Error(String(e))}function W(e){let t=new Set,n=[];for(let r of e){let{category:e,location:i}=r,a=`${e}\u0000${i.file}\u0000${i.line}\u0000${i.symbol??``}`;t.has(a)||(t.add(a),n.push(r))}return n}export{N as resolveConflicts};
@@ -1 +1 @@
1
- import{buildSummaryPrompt as e}from"./prompt.js";const t=.8;function n(e,n=t){let s={ai:0,"take-ours":0,"take-theirs":0,deleted:0,skipped:0,mechanical:0},c=0,l=null,u=new Map;for(let t of e.resolutions)s[t.strategy]++,c+=t.chunks?.length??0,l=l===null?t.confidence:Math.min(l,t.confidence),t.confidence<n&&u.set(t.filePath,{filePath:t.filePath,confidence:t.confidence,reason:`low-confidence`});let d=e.detection,f=d?.findings??[],p=new Map,m=!1;for(let e of f){e.severity===`error`&&(m=!0);let t=p.get(e.location.file)??new Set;t.add(e.category),p.set(e.location.file,t)}for(let[e,t]of p){let n=u.get(e);u.set(e,{filePath:e,...n?.confidence==null?{}:{confidence:n.confidence},reason:`finding`,findingCategories:[...t].sort()})}for(let t of e.skipped??[])t.reason!==`excluded`&&(u.has(t.filePath)||u.set(t.filePath,{filePath:t.filePath,reason:`unresolved`}));let h=[...u.values()].sort(a),g=d!=null&&r(d.checks.referenceIntegrity)&&r(d.checks.structural)&&i(d.scope),_=o({hasErrorFinding:m,hasWarningFinding:f.some(e=>e.severity===`warning`),needsReview:h.length>0,detectionComplete:g,hasResolutionErrors:e.errors.length>0});return{files:e.resolutions.length,conflicts:c,byStrategy:s,minConfidence:l,needsReview:h,skipped:e.skipped?.length??0,errors:e.errors.length,detectionComplete:g,findingCount:f.length,verdict:_}}function r(e){return e===`ran`||e===`partial-unsupported-language`}function i(e){return e?!e.truncated&&e.incomingDiscovered:!0}function a(e,t){return e.reason===t.reason?(e.confidence??1)-(t.confidence??1):e.reason===`finding`?-1:1}function o(e){return e.hasErrorFinding?`blocked`:e.needsReview||!e.detectionComplete||e.hasResolutionErrors||e.hasWarningFinding?`needs-review`:`clean`}async function s(r,i={}){let a=n(r,i.reviewThreshold??t);if(!i.model)return{digest:a};let{system:o,userMessage:s}=e(r,a),c=await i.model.generate({system:o,messages:[{role:`user`,content:s}],temperature:i.temperature??0,signal:i.signal}),l=c.text?.trim();return{digest:a,...l?{prose:l}:{},...c.usage?{usage:c.usage}:{}}}export{n as buildOperationDigest,s as summarizeOperation};
1
+ import{buildSummaryPrompt as e}from"./prompt.js";const t=.8;function n(e,n=t){let s={ai:0,"take-ours":0,"take-theirs":0,deleted:0,skipped:0,mechanical:0},c=0,l=null,u=new Map;for(let t of e.resolutions)s[t.strategy]++,c+=t.chunks?.length??0,l=l===null?t.confidence:Math.min(l,t.confidence),t.confidence<n&&u.set(t.filePath,{filePath:t.filePath,confidence:t.confidence,reason:`low-confidence`});let d=e.detection,f=d?.findings??[],p=new Map,m=!1;for(let e of f){e.severity===`error`&&(m=!0);let t=p.get(e.location.file)??new Set;t.add(e.category),p.set(e.location.file,t)}for(let[e,t]of p){let n=u.get(e);u.set(e,{filePath:e,...n?.confidence==null?{}:{confidence:n.confidence},reason:`finding`,findingCategories:[...t].sort()})}for(let t of e.skipped??[])t.reason!==`excluded`&&(u.has(t.filePath)||u.set(t.filePath,{filePath:t.filePath,reason:`unresolved`}));let h=[...u.values()].sort(a),g=d!=null&&r(d.checks.referenceIntegrity)&&r(d.checks.structural)&&(d.checks.validator===`ran`||d.checks.validator===`skipped-no-validator`)&&i(d.scope),_=o({hasErrorFinding:m,hasWarningFinding:f.some(e=>e.severity===`warning`),needsReview:h.length>0,detectionComplete:g,hasResolutionErrors:e.errors.length>0});return{files:e.resolutions.length,conflicts:c,byStrategy:s,minConfidence:l,needsReview:h,skipped:e.skipped?.length??0,errors:e.errors.length,detectionComplete:g,validation:d?.checks.validator??`skipped-no-validator`,...d?.validatorReason?{validationReason:d.validatorReason}:{},findingCount:f.length,verdict:_}}function r(e){return e===`ran`||e===`partial-unsupported-language`}function i(e){return e?!e.truncated&&e.incomingDiscovered:!0}function a(e,t){return e.reason===t.reason?(e.confidence??1)-(t.confidence??1):e.reason===`finding`?-1:1}function o(e){return e.hasErrorFinding?`blocked`:e.needsReview||!e.detectionComplete||e.hasResolutionErrors||e.hasWarningFinding?`needs-review`:`clean`}async function s(r,i={}){let a=n(r,i.reviewThreshold??t);if(!i.model)return{digest:a};let{system:o,userMessage:s}=e(r,a),c=await i.model.generate({system:o,messages:[{role:`user`,content:s}],temperature:i.temperature??0,signal:i.signal}),l=c.text?.trim();return{digest:a,...l?{prose:l}:{},...c.usage?{usage:c.usage}:{}}}export{n as buildOperationDigest,s as summarizeOperation};
@@ -156,7 +156,13 @@ type ConflictProgressEvent = {
156
156
  limit: number;
157
157
  } | {
158
158
  type: 'validation:failed';
159
- filePath: string;
159
+ filePath?: string;
160
+ scope: 'file' | 'set';
161
+ reason: string;
162
+ } | {
163
+ type: 'validation:not-run';
164
+ filePath?: string;
165
+ scope: 'file' | 'set';
160
166
  reason: string;
161
167
  } | {
162
168
  type: 'resolver:tool-result';
@@ -34,12 +34,18 @@ interface Finding {
34
34
  * `skipped-unreadable-source` means the parent snapshots came back empty (unreadable source), so the
35
35
  * before/after comparison had nothing to compare against. */
36
36
  type CheckStatus = 'ran' | 'skipped-no-parser' | 'skipped-no-refs' | 'skipped-unreadable-source' | 'partial-unsupported-language' | 'partial-no-cross-file-resolution' | 'errored';
37
+ /** Whether the consumer's validator ran on the whole resolved set. `not-run` means it answered that it
38
+ * did not; `errored` means it threw. */
39
+ type ValidatorCheckStatus = 'ran' | 'skipped-no-validator' | 'not-run' | 'errored';
37
40
  interface DetectionReport {
38
41
  findings: Finding[];
39
42
  checks: {
40
43
  referenceIntegrity: CheckStatus;
41
44
  structural: CheckStatus;
45
+ validator: ValidatorCheckStatus;
42
46
  };
47
+ /** Why the whole-set validator did not run or threw. Present with `checks.validator` `not-run` or `errored`. */
48
+ validatorReason?: string;
43
49
  /** Honest coverage signal so empty findings are not read as "clean". Present only when the reference
44
50
  * check ran (refs available). */
45
51
  scope?: {
@@ -54,4 +60,4 @@ interface DetectionReport {
54
60
  };
55
61
  }
56
62
  //#endregion
57
- export { CheckStatus, DetectionReport, Finding, FindingCategory, FindingTier };
63
+ export { CheckStatus, DetectionReport, Finding, FindingCategory, FindingTier, ValidatorCheckStatus };
@@ -15,18 +15,10 @@ type IntraFileEditOp = {
15
15
  startLine: number;
16
16
  endLine: number;
17
17
  anchor: string;
18
- } | {
19
- kind: 'verify';
20
- startLine: number;
21
- endLine?: number;
22
- anchor: string;
23
18
  };
24
19
  type EditOpApplyResult = {
25
20
  opIndex: number;
26
21
  status: 'applied';
27
- } | {
28
- opIndex: number;
29
- status: 'verified';
30
22
  } | {
31
23
  opIndex: number;
32
24
  status: 'skipped';
@@ -1,3 +1,4 @@
1
+ import { ValidatorCheckStatus } from "./findings.js";
1
2
  import { ResolutionStrategy } from "./resolution.js";
2
3
  import { AiTokenUsage } from "@gitkraken/shared-tools";
3
4
 
@@ -45,6 +46,11 @@ interface OperationDigest {
45
46
  * references, or part of the scope was never looked at — so empty findings must NOT be read as
46
47
  * "clean". */
47
48
  detectionComplete: boolean;
49
+ /** Whether the consumer's validator ran on the whole resolved set. `'skipped-no-validator'` when none
50
+ * was supplied or no detection was attached. */
51
+ validation: ValidatorCheckStatus;
52
+ /** Why the validator did not run or threw; see {@link validation}. */
53
+ validationReason?: string;
48
54
  /** Total detection findings across the operation. */
49
55
  findingCount: number;
50
56
  /** Suggested honest verdict; see {@link OperationVerdict}. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitkraken/conflict-tools",
3
- "version": "0.7.1-beta.10",
3
+ "version": "0.7.1-beta.12",
4
4
  "description": "AI-powered git conflict resolution.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Axosoft, LLC dba GitKraken",