@gitkraken/conflict-tools 0.2.3 → 0.2.4-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Added
11
+
12
+ - Reference-integrity detection. `resolveConflicts` now attaches a `StepResult.detection` report listing broken references (symbols that resolved in a pre-merge parent but no longer resolve in the merged result, with provenance), duplicate definitions, and syntax errors in the resolved files. Detection is opt-in via a new `ParserPort` (`ResolverDeps.parser`) that supplies parsing/symbol resolution per surface (tree-sitter, language server); when it is absent, unavailable, or a file's language is unsupported, the report says so rather than implying a clean result. Report-only in this release — no automatic fixing.
13
+ - Auto-merged scope for reference-integrity detection: the detector discovers the merge's incoming/auto-merged set itself (new optional `changedFiles` git-port op; `git diff --name-only base..theirs`, base from `refs.base` or `mergeBase`) and includes it in scope, so dangling references in cleanly-merged files are caught. Scope is capped and reported via `DetectionReport.scope`; an optional `ResolutionContext.touchedFiles` overrides discovery. Adds a `dropped-side` finding when a whole-side take (take-ours/take-theirs/fallback) discards a substantial diff from the other side.
14
+
10
15
  ## [0.2.3] - 2026-07-06
11
16
 
12
17
  ### Added
@@ -8,6 +8,7 @@ declare function mergeBase(git: ConflictGitPort, a: string, b: string, opts?: Op
8
8
  declare function blame(git: ConflictGitPort, path: string, opts?: BlameOptions): Promise<string>;
9
9
  declare function grep(git: ConflictGitPort, pattern: string, opts?: GrepOptions): Promise<string>;
10
10
  declare function diff(git: ConflictGitPort, from: string, to: string, opts?: ConflictDiffOptions): Promise<string>;
11
+ declare function changedFiles(git: ConflictGitPort, from: string, to: string, opts?: OpOptions): Promise<string[]>;
11
12
  declare function log(git: ConflictGitPort, opts?: LogOptions): Promise<string>;
12
13
  declare function show(git: ConflictGitPort, sha: string, opts?: ShowOptions): Promise<string>;
13
14
  declare function writeFile(git: ConflictGitPort, path: string, content: string, opts?: OpOptions): Promise<void>;
@@ -15,4 +16,4 @@ declare function stageFiles(git: ConflictGitPort, paths: string[], opts?: OpOpti
15
16
  declare function checkoutFile(git: ConflictGitPort, path: string, side: 'ours' | 'theirs', opts?: OpOptions): Promise<void>;
16
17
  declare function removeFile(git: ConflictGitPort, path: string, opts?: OpOptions): Promise<void>;
17
18
  //#endregion
18
- export { blame, checkoutFile, diff, grep, log, mergeBase, readFile, removeFile, show, showFile, stageFiles, unmergedEntries, writeFile };
19
+ export { blame, changedFiles, checkoutFile, diff, grep, log, mergeBase, readFile, removeFile, show, showFile, stageFiles, unmergedEntries, writeFile };
@@ -1,2 +1,3 @@
1
1
  import{ConflictGitPortMissingOpError as e}from"../ports/git.js";import{sliceLines as t,truncateLines as n,truncateMatches as r}from"./truncation.js";const i={DD:`both-deleted`,AU:`added-by-us`,UD:`deleted-by-them`,UA:`added-by-them`,DU:`deleted-by-us`,AA:`both-added`,UU:`both-modified`};function a(e){return i[e]??e}function o(e){return`Search capped at ${e} matches. There may be more — narrow with a more specific pattern or path.`}function s(t,n){throw new e(t,n)}function c(e,t){return e!=null&&t!=null?`startLine=${e}, endLine=${t}`:e==null?`endLine=${t}`:`startLine=${e}`}async function l(e,t,n){return e.readFile?e.readFile(t,n):s(`readFile`)}async function u(e,r,i,a){let o;if(e.showFile)o=await e.showFile(r,i,a);else if(e.exec){let n=t(await e.exec([`show`,`${r}:${i}`],{signal:a?.signal}),a?.startLine,a?.endLine);if(!n.ok)return n.reason===`invalid-range`?`[Invalid range: ${c(n.startLine,n.endLine)} (startLine must be <= endLine).]`:`[Range out of bounds: file has ${n.total} lines, requested ${c(n.startLine,n.endLine)}.]`;o=n.content}else return s(`showFile`);return n(o,1e3,`Output capped at 1000 lines. Use startLine/endLine to read other regions of the file.`)}async function d(e,t){if(e.unmergedEntries)return e.unmergedEntries(t);if(e.exec){let n=await e.exec([`status`,`--porcelain=v2`],{signal:t?.signal}),r=[];for(let e of n.split(`
2
- `)){if(!e.startsWith(`u `))continue;let t=e.split(` `),n=t[1]??``,i=t.slice(10).join(` `);r.push({path:i,reason:a(n)})}return r}return s(`unmergedEntries`)}async function f(e,t,n,r){return e.mergeBase?e.mergeBase(t,n,r):e.exec?(await e.exec([`merge-base`,`--`,t,n],{signal:r?.signal})).trim():s(`mergeBase`)}async function p(e,t,r){let i=r?.startLine,a=r?.endLine,o=Number.isFinite(i),l=Number.isFinite(a);if(o&&l&&i>a)return`[Invalid range: ${c(i,a)} (startLine must be <= endLine).]`;let u;if(e.blame)u=await e.blame(t,r);else if(e.exec){let n=[`blame`,`--porcelain`],s=o?Math.max(1,i):void 0,c=l?Math.max(1,a):void 0;s!=null&&c!=null?n.push(`-L`,`${s},${c}`):s==null?c!=null&&n.push(`-L`,`1,${c}`):n.push(`-L`,`${s},`),r?.ref&&n.push(r.ref),n.push(`--`,t),u=await e.exec(n,{signal:r?.signal})}else return s(`blame`);return n(u,1e3,`Output capped at 1000 lines. Use startLine/endLine to inspect other regions.`)}async function m(e,t,n){let i=n?.maxResults,a=i!=null&&Number.isFinite(i)?Math.min(Math.max(1,i),100):100,c;if(e.grep)c=await e.grep(t,n);else if(e.exec){let r=[`grep`,`-n`,`--`,t];n?.ref&&r.push(n.ref),c=await e.exec(r,{signal:n?.signal})}else return s(`grep`);return r(c,a,o(a))}async function h(e,t,n,r){if(e.diff)return e.diff(t,n,r);if(e.exec){let i=[`diff`,t,n];return r?.path&&i.push(`--`,r.path),e.exec(i,{signal:r?.signal})}return s(`diff`)}async function g(e,t){let r=t?.maxCount,i=r!=null&&Number.isFinite(r)?Math.min(Math.max(1,r),100):100,a;if(e.log)a=await e.log({...t,maxCount:i});else if(e.exec){let n=[`log`,`--oneline`,`-n`,String(i)];t?.ref&&n.push(t.ref),t?.path&&n.push(`--`,t.path),a=await e.exec(n,{signal:t?.signal})}else return s(`log`);return n(a,100,`Output capped at 100 commits. Use path to scope to a file or refine the ref range.`)}async function _(e,t,r){let i;if(e.show)i=await e.show(t,r);else if(e.exec){let n=[`show`,t];r?.path&&n.push(`--`,r.path),i=await e.exec(n,{signal:r?.signal})}else return s(`show`);return n(i,500,`Output capped at 500 lines. Use show_file_at_ref with startLine/endLine to read specific regions.`)}async function v(e,t,n,r){return e.writeFile?e.writeFile(t,n,r):s(`writeFile`)}async function y(e,t,n){if(e.stageFiles)return e.stageFiles(t,n);if(e.exec){await e.exec([`add`,`--`,...t],{signal:n?.signal});return}return s(`stageFiles`)}async function b(e,t,n,r){if(e.checkoutFile)return e.checkoutFile(t,n,r);if(e.exec){await e.exec([`checkout`,`--${n}`,`--`,t],{signal:r?.signal});return}return s(`checkoutFile`)}async function x(e,t,n){if(e.removeFile)return e.removeFile(t,n);if(e.exec){await e.exec([`rm`,`--`,t],{signal:n?.signal});return}return s(`removeFile`)}export{p as blame,b as checkoutFile,h as diff,m as grep,g as log,f as mergeBase,l as readFile,x as removeFile,_ as show,u as showFile,y as stageFiles,d as unmergedEntries,v as writeFile};
2
+ `)){if(!e.startsWith(`u `))continue;let t=e.split(` `),n=t[1]??``,i=t.slice(10).join(` `);r.push({path:i,reason:a(n)})}return r}return s(`unmergedEntries`)}async function f(e,t,n,r){return e.mergeBase?e.mergeBase(t,n,r):e.exec?(await e.exec([`merge-base`,`--`,t,n],{signal:r?.signal})).trim():s(`mergeBase`)}async function p(e,t,r){let i=r?.startLine,a=r?.endLine,o=Number.isFinite(i),l=Number.isFinite(a);if(o&&l&&i>a)return`[Invalid range: ${c(i,a)} (startLine must be <= endLine).]`;let u;if(e.blame)u=await e.blame(t,r);else if(e.exec){let n=[`blame`,`--porcelain`],s=o?Math.max(1,i):void 0,c=l?Math.max(1,a):void 0;s!=null&&c!=null?n.push(`-L`,`${s},${c}`):s==null?c!=null&&n.push(`-L`,`1,${c}`):n.push(`-L`,`${s},`),r?.ref&&n.push(r.ref),n.push(`--`,t),u=await e.exec(n,{signal:r?.signal})}else return s(`blame`);return n(u,1e3,`Output capped at 1000 lines. Use startLine/endLine to inspect other regions.`)}async function m(e,t,n){let i=n?.maxResults,a=i!=null&&Number.isFinite(i)?Math.min(Math.max(1,i),100):100,c;if(e.grep)c=await e.grep(t,n);else if(e.exec){let r=[`grep`,`-n`,`--`,t];n?.ref&&r.push(n.ref),c=await e.exec(r,{signal:n?.signal})}else return s(`grep`);return r(c,a,o(a))}async function h(e,t,n,r){if(e.diff)return e.diff(t,n,r);if(e.exec){let i=[`diff`,t,n];return r?.path&&i.push(`--`,r.path),e.exec(i,{signal:r?.signal})}return s(`diff`)}async function g(e,t,n,r){return e.changedFiles?e.changedFiles(t,n,r):e.exec?(await e.exec([`diff`,`--name-only`,t,n],{signal:r?.signal})).split(`
3
+ `).map(e=>e.trim()).filter(e=>e.length>0):s(`changedFiles`)}async function _(e,t){let r=t?.maxCount,i=r!=null&&Number.isFinite(r)?Math.min(Math.max(1,r),100):100,a;if(e.log)a=await e.log({...t,maxCount:i});else if(e.exec){let n=[`log`,`--oneline`,`-n`,String(i)];t?.ref&&n.push(t.ref),t?.path&&n.push(`--`,t.path),a=await e.exec(n,{signal:t?.signal})}else return s(`log`);return n(a,100,`Output capped at 100 commits. Use path to scope to a file or refine the ref range.`)}async function v(e,t,r){let i;if(e.show)i=await e.show(t,r);else if(e.exec){let n=[`show`,t];r?.path&&n.push(`--`,r.path),i=await e.exec(n,{signal:r?.signal})}else return s(`show`);return n(i,500,`Output capped at 500 lines. Use show_file_at_ref with startLine/endLine to read specific regions.`)}async function y(e,t,n,r){return e.writeFile?e.writeFile(t,n,r):s(`writeFile`)}async function b(e,t,n){if(e.stageFiles)return e.stageFiles(t,n);if(e.exec){await e.exec([`add`,`--`,...t],{signal:n?.signal});return}return s(`stageFiles`)}async function x(e,t,n,r){if(e.checkoutFile)return e.checkoutFile(t,n,r);if(e.exec){await e.exec([`checkout`,`--${n}`,`--`,t],{signal:r?.signal});return}return s(`checkoutFile`)}async function S(e,t,n){if(e.removeFile)return e.removeFile(t,n);if(e.exec){await e.exec([`rm`,`--`,t],{signal:n?.signal});return}return s(`removeFile`)}export{p as blame,g as changedFiles,x as checkoutFile,h as diff,m as grep,_ as log,f as mergeBase,l as readFile,S as removeFile,v as show,u as showFile,b as stageFiles,d as unmergedEntries,y as writeFile};
package/dist/index.d.ts CHANGED
@@ -9,7 +9,10 @@ import { extractConflict } from "./extraction/extract.js";
9
9
  import { checkoutFile, removeFile, show } from "./git/port-dispatch.js";
10
10
  import { ToolCall, ToolDefinition, ToolResult } from "./types/tool.js";
11
11
  import { ConflictModelMessage, ConflictModelParams, ConflictModelPort, ConflictModelResult, ResolverConfig } from "./ports/model.js";
12
+ import { ParsedFile, ParserPort, SourceOverlay, SymbolDefinition, SymbolReference, SyntaxIssue } from "./ports/parser.js";
12
13
  import { ResolutionVerifier, VerificationResult, defaultVerifier } from "./ports/verify.js";
14
+ import { CheckStatus, DetectionReport, Finding, FindingCategory, FindingTier } from "./types/findings.js";
15
+ import { DetectDeps, detect } from "./reference-integrity/detect.js";
13
16
  import { createResolution } from "./resolution/create-resolution.js";
14
17
  import { takeOurs, takeTheirs } from "./resolution/take-strategy.js";
15
18
  import { applyResolutions } from "./resolver/apply-resolutions.js";
@@ -19,4 +22,4 @@ import { FileRule, StepConfig, StepResult } from "./types/step.js";
19
22
  import { resolveConflicts } from "./resolver/resolve-conflicts.js";
20
23
  import { OperationDigest, OperationSummary, ReviewItem } from "./types/summary.js";
21
24
  import { SummarizeOperationOptions, buildOperationDigest, summarizeOperation } from "./resolver/summarize-operation.js";
22
- export { AIError, type AIErrorCode, type BlameOptions, type Conflict, type ConflictDiffOptions, ConflictError, type ConflictErrorCode, type ConflictGitOps, type ConflictGitPort, ConflictGitPortMissingOpError, type ConflictMarker, type ConflictMarkerUserInput, type ConflictModelMessage, type ConflictModelParams, type ConflictModelPort, type ConflictModelResult, type ConflictProgressEvent, type ConflictType, type DiffStats, type FileRule, type GrepOptions, type IntraFileEditOp, type Issue, type LogOptions, type OpOptions, type OperationDigest, type OperationSummary, type Resolution, type ResolutionContext, type ResolutionMetrics, type ResolutionRawSignals, type ResolutionRefs, type ResolutionStrategy, type ResolutionVerifier, type ResolvedChunk, type ResolverConfig, type ReviewItem, type ShowFileOptions, type ShowOptions, type StepConfig, type StepResult, type SummarizeOperationOptions, type ThreeWayDiff, type ToolCall, type ToolDefinition, type ToolResult, type UnmergedEntry, type UnmergedReason, type VerificationResult, applyResolutions, buildOperationDigest, checkoutFile, createResolution, defaultVerifier, extractConflict, removeFile, resolveConflict, resolveConflicts, show, summarizeOperation, takeOurs, takeTheirs };
25
+ export { AIError, type AIErrorCode, type BlameOptions, type CheckStatus, type Conflict, type ConflictDiffOptions, ConflictError, type ConflictErrorCode, type ConflictGitOps, type ConflictGitPort, ConflictGitPortMissingOpError, type ConflictMarker, type ConflictMarkerUserInput, type ConflictModelMessage, type ConflictModelParams, type ConflictModelPort, type ConflictModelResult, type ConflictProgressEvent, type ConflictType, type DetectDeps, type DetectionReport, type DiffStats, type FileRule, type Finding, type FindingCategory, type FindingTier, type GrepOptions, type IntraFileEditOp, type Issue, type LogOptions, type OpOptions, type OperationDigest, type OperationSummary, type ParsedFile, type ParserPort, type Resolution, type ResolutionContext, type ResolutionMetrics, type ResolutionRawSignals, type ResolutionRefs, type ResolutionStrategy, type ResolutionVerifier, type ResolvedChunk, type ResolverConfig, type ReviewItem, type ShowFileOptions, type ShowOptions, type SourceOverlay, type StepConfig, type StepResult, type SummarizeOperationOptions, type SymbolDefinition, type SymbolReference, type SyntaxIssue, type ThreeWayDiff, type ToolCall, type ToolDefinition, type ToolResult, type UnmergedEntry, type UnmergedReason, type VerificationResult, applyResolutions, buildOperationDigest, checkoutFile, createResolution, defaultVerifier, detect, extractConflict, removeFile, resolveConflict, resolveConflicts, show, summarizeOperation, takeOurs, takeTheirs };
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import{AIError as e}from"./errors/ai.js";import{ConflictError as t}from"./errors/conflict.js";import{ConflictGitPortMissingOpError as n}from"./ports/git.js";import{checkoutFile as r,removeFile as i,show as a}from"./git/port-dispatch.js";import{extractConflict as o}from"./extraction/extract.js";import{defaultVerifier as s}from"./ports/verify.js";import{createResolution as c}from"./resolution/create-resolution.js";import{takeOurs as l,takeTheirs as u}from"./resolution/take-strategy.js";import{applyResolutions as d}from"./resolver/apply-resolutions.js";import{resolveConflict as f}from"./resolver/resolve-conflict.js";import{resolveConflicts as p}from"./resolver/resolve-conflicts.js";import{buildOperationDigest as m,summarizeOperation as h}from"./resolver/summarize-operation.js";export{e as AIError,t as ConflictError,n as ConflictGitPortMissingOpError,d as applyResolutions,m as buildOperationDigest,r as checkoutFile,c as createResolution,s as defaultVerifier,o as extractConflict,i as removeFile,f as resolveConflict,p as resolveConflicts,a as show,h as summarizeOperation,l as takeOurs,u as takeTheirs};
1
+ import{AIError as e}from"./errors/ai.js";import{ConflictError as t}from"./errors/conflict.js";import{ConflictGitPortMissingOpError as n}from"./ports/git.js";import{checkoutFile as r,removeFile as i,show as a}from"./git/port-dispatch.js";import{extractConflict as o}from"./extraction/extract.js";import{defaultVerifier as s}from"./ports/verify.js";import{detect as c}from"./reference-integrity/detect.js";import{createResolution as l}from"./resolution/create-resolution.js";import{takeOurs as u,takeTheirs as d}from"./resolution/take-strategy.js";import{applyResolutions as f}from"./resolver/apply-resolutions.js";import{resolveConflict as p}from"./resolver/resolve-conflict.js";import{resolveConflicts as m}from"./resolver/resolve-conflicts.js";import{buildOperationDigest as h,summarizeOperation as g}from"./resolver/summarize-operation.js";export{e as AIError,t as ConflictError,n as ConflictGitPortMissingOpError,f as applyResolutions,h as buildOperationDigest,r as checkoutFile,l as createResolution,s as defaultVerifier,c as detect,o as extractConflict,i as removeFile,p as resolveConflict,m as resolveConflicts,a as show,g as summarizeOperation,u as takeOurs,d as takeTheirs};
@@ -45,6 +45,11 @@ interface ConflictGitOps {
45
45
  showFile?: (ref: string, path: string, opts?: ShowFileOptions) => Promise<string>;
46
46
  unmergedEntries?: (opts?: OpOptions) => Promise<UnmergedEntry[]>;
47
47
  mergeBase?: (a: string, b: string, opts?: OpOptions) => Promise<string>;
48
+ /**
49
+ * Returns the paths changed between two refs (name-only). Used to discover the merge's
50
+ * incoming/auto-merged set for reference-integrity detection.
51
+ */
52
+ changedFiles?: (from: string, to: string, opts?: OpOptions) => Promise<string[]>;
48
53
  /**
49
54
  * Returns line-by-line authorship for `path`. When `opts.startLine`/`endLine` are provided,
50
55
  * implementations should restrict to that 1-indexed inclusive range. The library does not
@@ -0,0 +1,51 @@
1
+ //#region src/ports/parser.d.ts
2
+ /** File contents to analyze, keyed by path. The analyzer treats these as the authoritative content
3
+ * for those paths (an overlay): a tree-sitter backend sees only these files, while a language server
4
+ * may resolve against its workspace with these contents overlaid on top. */
5
+ type SourceOverlay = ReadonlyMap<string, string>;
6
+ interface SymbolDefinition {
7
+ name: string;
8
+ kind?: string;
9
+ /** 1-indexed. */
10
+ line: number;
11
+ }
12
+ interface SymbolReference {
13
+ name: string;
14
+ /** 1-indexed. */
15
+ line: number;
16
+ column?: number;
17
+ /** Whether the reference resolved to a definition reachable within the analyzed overlay. */
18
+ resolved: boolean;
19
+ target?: {
20
+ file: string;
21
+ line: number;
22
+ };
23
+ }
24
+ interface SyntaxIssue {
25
+ /** 1-indexed. */
26
+ line: number;
27
+ message: string;
28
+ }
29
+ /** Per-file parse result. `language` is `'unsupported'` when the backend cannot parse the file — the
30
+ * detector then skips it rather than emitting false findings. */
31
+ interface ParsedFile {
32
+ language: string;
33
+ definitions: SymbolDefinition[];
34
+ references: SymbolReference[];
35
+ syntaxErrors: SyntaxIssue[];
36
+ }
37
+ /**
38
+ * Parsing / symbol-resolution capability supplied per surface — merge-mate via tree-sitter,
39
+ * GitLens/GKD via a language server. The detector stays free of any parser dependency; the port
40
+ * abstracts the precision difference between backends.
41
+ */
42
+ interface ParserPort {
43
+ /**
44
+ * Parse and resolve every file in `sources`, treating those contents as authoritative for their
45
+ * paths. Returns one {@link ParsedFile} per input path. Works only from the provided content — it
46
+ * never reads the filesystem itself.
47
+ */
48
+ analyze(sources: SourceOverlay): Promise<ReadonlyMap<string, ParsedFile>>;
49
+ }
50
+ //#endregion
51
+ export { ParsedFile, ParserPort, SourceOverlay, SymbolDefinition, SymbolReference, SyntaxIssue };
@@ -0,0 +1 @@
1
+ export{};
@@ -0,0 +1,21 @@
1
+ import { ConflictGitPort } from "../ports/git.js";
2
+ import { Resolution, ResolutionContext } from "../types/resolution.js";
3
+ import { ParserPort } from "../ports/parser.js";
4
+ import { DetectionReport } from "../types/findings.js";
5
+
6
+ //#region src/reference-integrity/detect.d.ts
7
+ interface DetectDeps {
8
+ git: ConflictGitPort;
9
+ /** Parsing/symbol-resolution capability. When absent, detection is skipped and reported as such. */
10
+ parser?: ParserPort;
11
+ signal?: AbortSignal;
12
+ }
13
+ /**
14
+ * Deterministic post-resolution detection: flags references that resolved in a pre-merge parent
15
+ * (ours/theirs) but no longer resolve in the merged result, plus duplicate definitions and syntax
16
+ * errors in the resolved files. Report-only — it never mutates state or fails the operation. When no
17
+ * parser is supplied, or refs are missing, the report says so instead of implying a clean result.
18
+ */
19
+ declare function detect(resolutions: Resolution[], context: ResolutionContext, deps: DetectDeps): Promise<DetectionReport>;
20
+ //#endregion
21
+ export { DetectDeps, detect };
@@ -0,0 +1,3 @@
1
+ import{buildSnapshots as e}from"./snapshot.js";async function t(t,r,o){let c=o.parser;if(!c)return{findings:[],checks:{referenceIntegrity:`skipped-no-parser`,structural:`skipped-no-parser`}};try{let l=r.refs?await e(t,r,o):null;if(l){let[e,r,o]=await Promise.all([c.analyze(l.ours),c.analyze(l.theirs),c.analyze(l.result)]);return{findings:[...n(e,r,o),...i(o),...a(t,l)],checks:{referenceIntegrity:s([e,r,o])?`partial-unsupported-language`:`ran`,structural:s([o])?`partial-unsupported-language`:`ran`},scope:{analyzed:l.analyzed,truncated:l.truncated}}}let u=new Map(t.map(e=>[e.filePath,e.content])),d=await c.analyze(u);return{findings:i(d),checks:{referenceIntegrity:`skipped-no-refs`,structural:s([d])?`partial-unsupported-language`:`ran`}}}catch{return{findings:[],checks:{referenceIntegrity:`errored`,structural:`errored`}}}}function n(e,t,n){let i=new Map;r(e,`ours`,i),r(t,`theirs`,i);let a=[];for(let[e,t]of n)for(let n of t.references){if(n.resolved)continue;let t=i.get(c(e,n.name));t&&a.push({category:`broken-reference`,severity:`error`,tier:`project`,location:{file:e,line:n.line,symbol:n.name},provenance:t,message:`Reference "${n.name}" resolved before the merge (on ${t.side}) but no longer resolves.`})}return a}function r(e,t,n){for(let[r,i]of e)for(let e of i.references){if(!e.resolved)continue;let i=c(r,e.name);n.has(i)||n.set(i,{side:t,line:e.line})}}function i(e){let t=[];for(let[n,r]of e){let e=new Map;for(let i of r.definitions){let r=e.get(i.name);r==null?e.set(i.name,i.line):t.push({category:`duplicate-definition`,severity:`error`,tier:`file`,location:{file:n,line:i.line,symbol:i.name},message:`Duplicate definition of "${i.name}" (also defined at line ${r}).`})}for(let e of r.syntaxErrors)t.push({category:`syntax-error`,severity:`error`,tier:`file`,location:{file:n,line:e.line},message:e.message})}return t}function a(e,t){let n=[];for(let r of e){let e=r.strategy===`take-ours`?`theirs`:r.strategy===`take-theirs`?`ours`:null;if(!e)continue;let i=(e===`ours`?t.ours:t.theirs).get(r.filePath);if(i==null)continue;let a=o(i,r.content);a<20||n.push({category:`dropped-side`,severity:`error`,tier:`file`,location:{file:r.filePath,line:1},message:`Resolution took "${r.strategy}" wholesale, discarding ~${a} line(s) of the ${e} side.`})}return n}function o(e,t){let n=new Set(t.split(`
2
+ `)),r=0;for(let t of e.split(`
3
+ `))t.trim()!==``&&!n.has(t)&&r++;return r}function s(e){return e.some(e=>{for(let t of e.values())if(t.language===`unsupported`)return!0;return!1})}function c(e,t){return JSON.stringify([e,t])}export{t as detect};
@@ -0,0 +1,29 @@
1
+ import { ConflictGitPort } from "../ports/git.js";
2
+ import { Resolution, ResolutionContext } from "../types/resolution.js";
3
+ import { SourceOverlay } from "../ports/parser.js";
4
+
5
+ //#region src/reference-integrity/snapshot.d.ts
6
+ interface DetectionSnapshots {
7
+ ours: SourceOverlay;
8
+ theirs: SourceOverlay;
9
+ result: SourceOverlay;
10
+ /** Number of files actually included in the snapshots (after the cap). */
11
+ analyzed: number;
12
+ /** Whether the scope exceeded the cap and was truncated. */
13
+ truncated: boolean;
14
+ }
15
+ interface SnapshotDeps {
16
+ git: ConflictGitPort;
17
+ signal?: AbortSignal;
18
+ }
19
+ /**
20
+ * Builds the file-content overlays the detector compares: both pre-merge parents (read from git
21
+ * objects — no working tree needed) and the resolved result (in-memory resolutions overlaid on the
22
+ * surrounding files). Scope is the resolved files plus the merge's incoming/auto-merged set, which
23
+ * the detector discovers itself via the git port (`changedFiles(base, theirs)`), unless the consumer
24
+ * supplies `context.touchedFiles` as an override. Returns `null` when `context.refs` is absent — the
25
+ * before/after comparison is impossible without the parent refs.
26
+ */
27
+ declare function buildSnapshots(resolutions: Resolution[], context: ResolutionContext, deps: SnapshotDeps): Promise<DetectionSnapshots | null>;
28
+ //#endregion
29
+ export { DetectionSnapshots, SnapshotDeps, buildSnapshots };
@@ -0,0 +1 @@
1
+ import{changedFiles as e,mergeBase as t,readFile as n,showFile as r}from"../git/port-dispatch.js";async function i(e,t,n){let r=t.refs;if(!r)return null;let i=n.signal?{signal:n.signal}:void 0,c=new Map(e.map(e=>[e.filePath,e.content])),l=t.touchedFiles??await a(n.git,r,i),u=[],d=new Set;for(let e of[...c.keys(),...l,...t.fileNeighbors??[]])d.has(e)||(d.add(e),u.push(e));let f=u.length>400,p=f?u.slice(0,400):u,m=new Map,h=new Map,g=new Map;for(let e of p){let t=await o(n.git,r.ours,e,i);t!=null&&m.set(e,t);let a=await o(n.git,r.theirs,e,i);a!=null&&h.set(e,a);let l=c.get(e);if(l!=null)g.set(e,l);else{let t=await s(n.git,e,i);t!=null&&g.set(e,t)}}return{ours:m,theirs:h,result:g,analyzed:p.length,truncated:f}}async function a(n,r,i){try{return await e(n,r.base??await t(n,r.ours,r.theirs,i),r.theirs,i)}catch{return[]}}async function o(e,t,n,i){try{return await r(e,t,n,i)}catch{return null}}async function s(e,t,r){try{return await n(e,t,r)}catch{return null}}export{i as buildSnapshots};
@@ -1,5 +1,6 @@
1
1
  import { ConflictGitPort } from "../ports/git.js";
2
2
  import { ConflictModelPort, ResolverConfig } from "../ports/model.js";
3
+ import { ParserPort } from "../ports/parser.js";
3
4
  import { ResolutionVerifier } from "../ports/verify.js";
4
5
  import { ConflictProgressEvent } from "../types/events.js";
5
6
 
@@ -11,6 +12,7 @@ interface ResolverDeps<C extends ResolverConfig = ResolverConfig> {
11
12
  onProgress?: (event: ConflictProgressEvent) => void;
12
13
  signal?: AbortSignal;
13
14
  verifier?: ResolutionVerifier;
15
+ parser?: ParserPort;
14
16
  }
15
17
  //#endregion
16
18
  export { ResolverDeps };
@@ -1 +1 @@
1
- import{AIError as e}from"../errors/ai.js";import{unmergedEntries as t}from"../git/port-dispatch.js";import{extractConflict as n}from"../extraction/extract.js";import{createResolution as r}from"../resolution/create-resolution.js";import{takeOurs as i,takeTheirs as a}from"../resolution/take-strategy.js";import{resolveConflict as o}from"./resolve-conflict.js";import s from"picomatch";function c(e){let t=e.map(e=>{let t=(Array.isArray(e.match)?e.match:[e.match]).map(e=>s(e,{basename:!e.includes(`/`)}));return{isMatch:e=>t.some(t=>t(e)),strategy:e.strategy}});return e=>{for(let{isMatch:n,strategy:r}of t)if(n(e))return r}}async function l(e,o){let s=e.config?.rules??[],l=e.config?.defaultStrategy??`ai`,f=c(s),p=await t(e.git,{signal:e.signal}),m={...o??{}},h=[],g=[],_=[];for(let t of p){if(e.signal?.aborted)break;let o=f(t.path)??l;if(o===`skip`){_.push({filePath:t.path,reason:`excluded`}),e.onProgress?.({type:`conflict:excluded`,filePath:t.path});continue}let s;try{s=await n(t.path,{git:e.git,signal:e.signal},t.reason)}catch(e){g.push({filePath:t.path,error:d(e)});continue}if(!s){_.push({filePath:t.path,reason:`no-markers`}),e.onProgress?.({type:`conflict:skipped`,filePath:t.path,reason:`no-markers`,entryReason:t.reason});continue}e.onProgress?.({type:`conflict:found`,filePath:s.filePath,conflictType:s.type,markerCount:s.markers.length});let c=null;c=o===`take-ours`?i(s):o===`take-theirs`?a(s):o===`deleted`?r(s,[],{confidence:1,fileStrategy:`deleted`}):await u(s,m,e,g),c&&(m.previousResolutions=[...m.previousResolutions??[],c],h.push(c),e.onProgress?.({type:`resolution:applied`,filePath:c.filePath,strategy:c.strategy,confidence:c.confidence,description:c.description}))}return{resolutions:h,errors:g,skipped:_}}async function u(t,n,r,s){try{return await o(t,n,{model:r.model,git:r.git,config:r.config,onProgress:r.onProgress,signal:r.signal,verifier:r.verifier})}catch(n){let o=d(n),c=r.config?.fallbackStrategy;return c?(r.onProgress?.({type:`resolution:fallback`,filePath:t.filePath,error:o,cause:o instanceof e?o.code:void 0}),{...c===`take-ours`?i(t):a(t),confidence:0}):(s.push({filePath:t.filePath,error:o}),r.onProgress?.({type:`resolution:failed`,filePath:t.filePath,error:o}),null)}}function d(e){return e instanceof Error?e:Error(String(e))}export{l as resolveConflicts};
1
+ import{AIError as e}from"../errors/ai.js";import{unmergedEntries as t}from"../git/port-dispatch.js";import{extractConflict as n}from"../extraction/extract.js";import{detect as r}from"../reference-integrity/detect.js";import{createResolution as i}from"../resolution/create-resolution.js";import{takeOurs as a,takeTheirs as o}from"../resolution/take-strategy.js";import{resolveConflict as s}from"./resolve-conflict.js";import c from"picomatch";function l(e){let t=e.map(e=>{let t=(Array.isArray(e.match)?e.match:[e.match]).map(e=>c(e,{basename:!e.includes(`/`)}));return{isMatch:e=>t.some(t=>t(e)),strategy:e.strategy}});return e=>{for(let{isMatch:n,strategy:r}of t)if(n(e))return r}}async function u(e,s){let c=e.config?.rules??[],u=e.config?.defaultStrategy??`ai`,p=l(c),m=await t(e.git,{signal:e.signal}),h={...s??{}},g=[],_=[],v=[];for(let t of m){if(e.signal?.aborted)break;let r=p(t.path)??u;if(r===`skip`){v.push({filePath:t.path,reason:`excluded`}),e.onProgress?.({type:`conflict:excluded`,filePath:t.path});continue}let s;try{s=await n(t.path,{git:e.git,signal:e.signal},t.reason)}catch(e){_.push({filePath:t.path,error:f(e)});continue}if(!s){v.push({filePath:t.path,reason:`no-markers`}),e.onProgress?.({type:`conflict:skipped`,filePath:t.path,reason:`no-markers`,entryReason:t.reason});continue}e.onProgress?.({type:`conflict:found`,filePath:s.filePath,conflictType:s.type,markerCount:s.markers.length});let c=null;c=r===`take-ours`?a(s):r===`take-theirs`?o(s):r===`deleted`?i(s,[],{confidence:1,fileStrategy:`deleted`}):await d(s,h,e,_),c&&(h.previousResolutions=[...h.previousResolutions??[],c],g.push(c),e.onProgress?.({type:`resolution:applied`,filePath:c.filePath,strategy:c.strategy,confidence:c.confidence,description:c.description}))}return{resolutions:g,errors:_,skipped:v,detection:await r(g,h,{git:e.git,parser:e.parser,signal:e.signal})}}async function d(t,n,r,i){try{return await s(t,n,{model:r.model,git:r.git,config:r.config,onProgress:r.onProgress,signal:r.signal,verifier:r.verifier})}catch(n){let s=f(n),c=r.config?.fallbackStrategy;return c?(r.onProgress?.({type:`resolution:fallback`,filePath:t.filePath,error:s,cause:s instanceof e?s.code:void 0}),{...c===`take-ours`?a(t):o(t),confidence:0}):(i.push({filePath:t.filePath,error:s}),r.onProgress?.({type:`resolution:failed`,filePath:t.filePath,error:s}),null)}}function f(e){return e instanceof Error?e:Error(String(e))}export{u as resolveConflicts};
@@ -0,0 +1,39 @@
1
+ //#region src/types/findings.d.ts
2
+ type FindingCategory = 'broken-reference' | 'duplicate-definition' | 'syntax-error' | 'dropped-side';
3
+ type FindingTier = 'file' | 'project';
4
+ interface Finding {
5
+ category: FindingCategory;
6
+ severity: 'error' | 'warning';
7
+ tier: FindingTier;
8
+ location: {
9
+ file: string;
10
+ line: number;
11
+ symbol?: string;
12
+ };
13
+ /** Present for `broken-reference`: the pre-merge side that still resolved the reference, and where. */
14
+ provenance?: {
15
+ side: 'ours' | 'theirs';
16
+ line: number;
17
+ };
18
+ message: string;
19
+ }
20
+ /** Why a check produced its result — lets a consumer tell "ran, clean" apart from "did not run".
21
+ * `errored` means the parser was present but threw; `partial-unsupported-language` means some files
22
+ * in scope could not be parsed. */
23
+ type CheckStatus = 'ran' | 'skipped-no-parser' | 'skipped-no-refs' | 'partial-unsupported-language' | 'errored';
24
+ interface DetectionReport {
25
+ findings: Finding[];
26
+ checks: {
27
+ referenceIntegrity: CheckStatus;
28
+ structural: CheckStatus;
29
+ };
30
+ /** How many files were analyzed and whether the scope was capped — an honest coverage signal so
31
+ * empty findings on a truncated scope are not read as "clean". Present only when the reference
32
+ * check ran (refs available). */
33
+ scope?: {
34
+ analyzed: number;
35
+ truncated: boolean;
36
+ };
37
+ }
38
+ //#endregion
39
+ export { CheckStatus, DetectionReport, Finding, FindingCategory, FindingTier };
@@ -0,0 +1 @@
1
+ export{};
@@ -69,6 +69,10 @@ interface ResolutionContext {
69
69
  prDescription?: string;
70
70
  previousResolutions?: Resolution[];
71
71
  fileNeighbors?: string[];
72
+ /** Files the merge/sync touched beyond the conflict set (chiefly auto-merged incoming changes).
73
+ * Usually left unset — the reference-integrity detector discovers this set itself from `refs`.
74
+ * When provided, it overrides auto-discovery and defines the incoming detection scope. */
75
+ touchedFiles?: string[];
72
76
  refs?: ResolutionRefs;
73
77
  threeWayDiff?: ThreeWayDiff;
74
78
  /** Free-text guidance from the user that applies to the whole file ("prefer logging across the
@@ -1,5 +1,6 @@
1
1
  import { Resolution } from "./resolution.js";
2
2
  import { ResolverConfig } from "../ports/model.js";
3
+ import { DetectionReport } from "./findings.js";
3
4
 
4
5
  //#region src/types/step.d.ts
5
6
  interface StepResult {
@@ -12,6 +13,7 @@ interface StepResult {
12
13
  filePath: string;
13
14
  reason: string;
14
15
  }[];
16
+ detection?: DetectionReport;
15
17
  }
16
18
  interface FileRule {
17
19
  match: string | string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitkraken/conflict-tools",
3
- "version": "0.2.3",
3
+ "version": "0.2.4-beta.2",
4
4
  "description": "AI-powered git conflict resolution.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Axosoft, LLC dba GitKraken",