@gitkraken/conflict-tools 0.7.1-beta.11 → 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
  });
@@ -271,7 +277,7 @@ const step = await resolveConflicts(
271
277
  model,
272
278
  git,
273
279
  parser, // optional — supply to enable set-level detection (stage 3)
274
- validator, // optional — advisory per-file checks
280
+ validator, // optional — advisory checks
275
281
  config: {
276
282
  rules: [
277
283
  { match: ['*.lock', 'pnpm-lock.yaml'], strategy: 'take-theirs' },
@@ -429,7 +435,9 @@ The scope includes the resolved files *and* the merge's auto-merged/incoming set
429
435
  ```typescript
430
436
  interface DetectionReport {
431
437
  findings: Finding[];
432
- 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;
433
441
  /** Honest coverage — present only when the reference check ran. */
434
442
  scope?: { inScope: number; readable: number; truncated: boolean; incomingDiscovered: boolean };
435
443
  }
@@ -479,7 +487,8 @@ import { buildOperationDigest } from '@gitkraken/conflict-tools';
479
487
 
480
488
  const digest = buildOperationDigest(step);
481
489
  // digest.verdict — 'clean' | 'needs-review' | 'blocked'
482
- // 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
483
492
  // digest.findingCount — total detection findings
484
493
  // digest.needsReview — ReviewItem[] (findings first, then lowest confidence first)
485
494
  // digest.minConfidence — lowest file confidence, or null
@@ -617,7 +626,8 @@ score(resolution, goldenFile);
617
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" |
618
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` |
619
628
  | `fix:budget-exhausted` | The global `maxModelCalls` budget was spent (`spent`, `limit`); the fix phase stopped and left the remainder in findings |
620
- | `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`) |
621
631
 
622
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`.
623
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};
@@ -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};
@@ -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 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 };
@@ -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.11",
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",