@gitkraken/conflict-tools 0.2.4-beta.23 → 0.2.4-beta.25

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
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
9
9
 
10
10
  ### Added
11
11
 
12
+ - `StepConfig.maxDetectionScopeFiles` — override the reference-integrity scope cap (default 400). Beyond the cap the scope is `truncated`, which holds the verdict off `clean`; raising it lets a large merge stay on the `--fix` auto-apply path. Threaded through `DetectDeps`/`SnapshotDeps` as `maxScopeFiles`.
12
13
  - `StepConfig.maxModelCalls` — an optional global model-call budget for the whole operation. Enforced per model call (the refine pass checks it before every step), not merely per pass. When set and the budget is spent, the opt-in fix phase stops attempting further fixes and leaves the remainder in the findings (so the verdict still reflects the true post-fix state), emitting a new `fix:budget-exhausted` progress event (`{ spent, limit }`). When unset, the fix phase is not budget-limited.
13
14
  - `fix:failed` progress event (`{ filePath?, reason }`) — emitted when the opt-in fix phase throws a non-abort error, so a consumer can observe the graceful degradation rather than only seeing the checks flip to `errored`. `filePath` is set when one file failed and the phase carried on without it; absent when the whole phase failed.
14
15
  - `ParserPort.capabilities` — a backend declares what it can resolve (currently `crossFileResolution`). A backend that cannot resolve references across files reports every import as unresolved on both parents *and* the result, so no cross-file break can ever surface; detection now reports `partial-no-cross-file-resolution` for that instead of letting the empty result pass as a clean cross-file check. Omitted means the conservative reading (no cross-file resolution).
@@ -21,9 +22,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
21
22
  - `ResolutionContext.detectionRefs` — optional parent tips for reference-integrity detection's before/after comparison, separate from `refs` (which drives the resolver's three-way diff). Detection uses `detectionRefs ?? refs`. Needed when the resolver's refs are the in-flight per-step state (e.g. a rebase) while detection needs the two stable pre-merge branch tips; in a merge they coincide and it can be left unset.
22
23
  - Binding-aware broken-reference detection. `ParsedFile` gains an optional `bindings` (names bound at any scope, module-level and nested), and reference-integrity now treats a name as "resolvable before the merge" if a parent resolved it as a reference **or** defined/bound it. This catches a reference broken by removing a nested binding (e.g. a `const` used by a closure elsewhere in the same file): the dangling use was never a resolved reference — because it was locally bound — so the before/after reference diff alone could not see the regression. Backends that only surface top-level `definitions` can omit `bindings`; they simply do not gain the nested-binding case.
23
24
  - Operation-level assessment: `buildOperationDigest`/`summarizeOperation` now fold `StepResult.detection` into the rollup. A file with any finding is added to `needsReview` (`reason: 'finding'`) regardless of confidence — even a file with no resolution of its own; an error finding (broken-reference, syntax-error, dropped-side) drives `verdict: 'blocked'`, while a warning finding (duplicate-definition, consumer-validator warnings) drives `needs-review`; `OperationDigest.detectionComplete` reports whether every detection check ran (so empty findings on a skipped check are not read as clean); and a derived `OperationDigest.verdict` (`'clean' | 'needs-review' | 'blocked'`) gives consumers an honest gate signal. `ReviewItem` gains `reason` and optional `findingCategories`, and its `confidence` is now optional.
25
+ - `detectFallbackCodeLoss(conflict, strategy)` and the `CodeLossDetection` type (carrying `discardedSide`), exported from the package root.
26
+ - `StepConfig.allowLossyFallback?: boolean` escape hatch.
27
+ - `ConflictError` code `CODE_LOSS_RISK`.
24
28
 
25
29
  ### Changed
26
30
 
31
+ - **Breaking for `fallbackStrategy` users:** a wholesale `take-ours`/`take-theirs` fallback (taken when the AI resolver fails) is no longer applied to text conflicts. The file is recorded as a `CODE_LOSS_RISK` error with a `resolution:failed` event instead of completing silently, so the consumer stops for manual resolution. The fallback is applied with `git checkout --ours/--theirs`, which replaces the whole file with one stage and discards the other side plus anything git auto-merged outside the conflict markers; on rebase "ours" is everything integrated so far, so a blind take-theirs could wipe it (AGP-430). Only a delete-modify conflict resolved toward the side that keeps content still applies the fallback. Set `StepConfig.allowLossyFallback: true` to opt back into the previous behavior. Deliberate `rules` (e.g. `take-theirs` for lock files) are unaffected.
27
32
  - Reference-integrity findings are now tiered by locality: an intra-file break — a local definition/binding the merge dropped while a use of it remains in the same file (e.g. a `const` used by a closure below) — is a `tier: 'file'` finding; a broken cross-file reference (an import whose target stopped resolving) stays `tier: 'project'`. Previously every `broken-reference` was `'project'`. Classification uses the reference's `target` file when the parser supplies it, else whether the name was a local definition/binding in the parent. This sets up the per-file vs cross-file fix loops.
28
33
  - **Breaking**: consumer validation moved to a `Validator` port — `validate(sources: SourceOverlay): Promise<ValidatorFinding[]>` (replaces the per-file `ResolutionVerifier.verify`). It runs post-refine, non-blocking, over an overlay; its findings fold into the operation's detection findings and verdict. The overlay is currently always a single file — `SourceOverlay` is the parameter type so a whole-set call can be added without a further breaking change, but no such call site exists yet. `Resolution.findings` replaces `Resolution.issues`. `Finding.category` (and `ValidatorFinding.category`) is now `FindingCategory | (string & {})` — open to consumer categories while keeping the package's own values discoverable and autocompleting, matching the existing `UnmergedReason` precedent.
29
34
  - **Breaking**: the resolver's in-loop blocking check is now **residual conflict markers only** (built-in, language-agnostic). Consumer validation (the `Validator` port, above) no longer gates the resolve loop or triggers re-resolution — it runs once **after** refine, **non-blocking**, and its findings are folded into the report (advisory). This stops the resolver from burning its step budget on non-marker validation (which was exhausting into a whole-file `take-theirs` fallback), and puts consumer validation in its intended post-refine, report-only role. `ResolverLoopResult` no longer carries `issues`.
@@ -35,10 +40,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
35
40
  - **Breaking**: `ResolutionVerifier` — replaced by the `Validator` port (see Changed). `defaultVerifier` and `VerificationResult` are no longer exported either; `checkResidualMarkers` stays internal as the built-in marker gate.
36
41
  - **Breaking**: the `Issue` type is no longer exported from the package root. It only ever described the internal marker-gate result, which is not part of the public surface now that `VerificationResult` is unexported.
37
42
  - **Breaking**: the `verification:issues` progress event. Consumer validation no longer runs inside the resolve loop, so the event had no emitter; validator output arrives as `Finding`s on the detection report, and a throwing validator is reported via `validation:failed`.
38
- - **Breaking**: `ResolverConfig.maxModelCalls` moved to `StepConfig` (see Added). The fix phase is what enforces the budget and the fix phase is batch-only, so on `resolveConflict` the field was silently inert.
39
43
 
40
44
  ### Fixed
41
45
 
46
+ - An abort landing before the report-only detection no longer rejects the operation and discards the partial resolutions the resolve loop deliberately kept. The `else`/report-only path fed the already-aborted signal to `detect()`, which (per the parser-port contract, honored by any real adapter) rethrows on abort. A cancelled operation now returns its partial `StepResult` with an honest incomplete report (`checks: errored`), matching the fix-off loop-break intent; only the fake parser used in some tests, which ignores the signal, hid this.
47
+ - The fix phase's regression guard no longer accepts a round that adds a second same-category error. `introducedNewError` keyed on category/file/symbol (line and count excluded), so a round turning one `syntax-error` (or validator error, both symbol-less) into two shared a fingerprint and read as "no new kind" — the worse content was kept and confidence capped as if improved. The guard (now `regressed`) also rejects when the error count grows, at both the per-file and cross-file sites, restoring the "never ship a file worse than the one it was handed" invariant.
42
48
  - Files nobody resolved no longer poison the detection scope. A conflicted file excluded by a `skip` rule, or one that errored during extraction, has no resolution, so the detector read it from the working tree — conflict markers and all — and the parser reported the marker text as syntax errors. That forced `verdict: 'blocked'` for the whole operation, which made `clean` unreachable for anyone using skip rules (the README's own lockfile guidance). Skipped and errored paths are now excluded from scope outright.
43
49
  - `OperationDigest.detectionComplete` no longer ignores coverage gaps. Both checks could report `'ran'` while part of the scope was never examined — past the `MAX_SCOPE_FILES` cap (`scope.truncated`), or when the git port could not enumerate the incoming set — yielding `verdict: 'clean'` with hundreds of files unlooked-at. `detectionComplete` now also requires the scope to be fully covered, and `DetectionReport.scope` gained `incomingDiscovered` so "the other side changed nothing" is distinguishable from "we could not ask".
44
50
  - `duplicate-definition` no longer fires on legal same-name declarations. It ran over the whole result overlay — up to 400 auto-merged files — and reported every TS function/method overload, `interface`/`namespace`/`enum` declaration merging, and platform-conditional definition as a warning, which drives `needs-review` and made `clean` unreachable on ordinary TypeScript. Duplicates are now diffed against the parents like broken references already were: only a duplicate the merge introduced is reported. Without a parent baseline none can be attributed, so none are reported.
@@ -55,7 +61,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
55
61
  - Fix instructions are bounded and stripped of control characters. Finding messages come from a parser or a consumer validator and therefore carry repository-derived text; a multi-line message landed in a numbered instruction list as several forged instructions, and nothing capped how many findings or how much text reached the model on every iteration.
56
62
  - Cross-side provenance no longer misclassifies an intra-file break as cross-file. Definitions and bindings were collected one side at a time, so a name that `ours` resolved via an import but `theirs` defined locally recorded `crossFile: true` and was tiered `project` — routed to the slower cross-file stage instead of the per-file loop. Both sides' definitions and bindings are now collected before either side's references.
57
63
  - `fix: true` without `refs` is no longer a total no-op. The per-file stage runs on what needs no parent content (syntax errors, validator findings); only cross-file routing, which is driven by the whole-set before/after report, is skipped.
58
-
59
64
  - The opt-in fix phase is now fault-isolated. A parser, validator, or model throw inside the fix phase no longer rejects the whole operation: the resolutions (possibly partially fixed) are kept, both detection checks are reported as `errored` (so the verdict is `needs-review`, never a false `clean`), and the throw is surfaced via `fix:failed`. A user abort still propagates and cancels the operation.
60
65
  - `OperationDigest.verdict` can now reach `clean` on a real repository. `detectionComplete` treated `partial-unsupported-language` as "did not run", so any operation whose scope contained a non-code file (package.json, README, lockfiles — always present in a real merge) forced `verdict: 'needs-review'` even with zero findings and full confidence. Since `--fix` gates auto-apply on `verdict === 'clean'`, this made auto-apply effectively unreachable. A check now counts as complete when it ran, including when some scope files were an unsupported language (they carry no references to break); only a check that could not run at all — no parser, no refs, an unreadable parent snapshot, or a thrown parser — leaves detection incomplete.
61
66
  - A non-abort error in the refine pass no longer destroys the pre-refine resolution. Previously the refine `catch` rethrew, bubbling to `resolveOne` and degrading a good resolve-stage result into a take-side fallback (confidence 0) — the same class of failure as a throwing validator, one stage later. The pre-refine resolution is now kept, its confidence capped (the enhancement did not complete), and the error reported via `refine:failed`. A user abort still cancels (rethrown past the fallback).
package/README.md CHANGED
@@ -99,7 +99,7 @@ const parser: ParserPort = {
99
99
  };
100
100
  ```
101
101
 
102
- Three properties define the contract:
102
+ Four properties define the contract:
103
103
 
104
104
  - **Content-in overlay, no FS access.** `analyze(sources)` sees exactly the `SourceOverlay` contents (a `ReadonlyMap<path, content>`) and never reads the filesystem itself. This is what lets the package hand it pre-merge parent snapshots *and* the post-merge result and diff the two. A tree-sitter backend sees only these files; a language server may resolve against its workspace with these contents overlaid on top.
105
105
  - **Language-agnostic.** A file type the backend has no grammar for comes back `supported: false` with empty facts; the detector skips it (and reports `partial-unsupported-language`) rather than emitting false findings. A file it *does* handle but cannot parse is `supported: true` with the failure in `syntaxErrors` — that is a finding, not a coverage gap, and conflating the two would let a broken merge result read as clean.
@@ -279,7 +279,8 @@ if (digest.verdict === 'clean') {
279
279
  - `rules` — array of `FileRule` objects. Each rule has a `match` (glob pattern or array of patterns) and a `strategy`. First matching rule wins. Patterns without `/` match on basename; patterns with `/` match on full path.
280
280
  - `defaultStrategy` — strategy for files not matching any rule. Defaults to `'ai'`.
281
281
  - `fix`, `fixLoopMaxIterations`, `crossFileMaxIterations`, `maxModelCalls` — the fix phase and its bounds. See [Fix phase](#fix-phase-opt-in). All four are batch-only because the fix phase is what enforces them.
282
- - `fallbackStrategy` — when AI fails for a file, fall back to `'take-ours'` or `'take-theirs'` instead of recording an error.
282
+ - `fallbackStrategy` — when AI fails for a file, fall back to `'take-ours'` or `'take-theirs'` instead of recording an error. **Guarded against code loss, and the guard stops every text conflict.** The fallback is applied with `git checkout --ours/--theirs`, which replaces the whole file with one stage, so it discards the other side plus anything git auto-merged outside the conflict markers. On rebase "ours" is everything integrated so far, so a blind take-theirs can wipe it. The file is recorded as a `CODE_LOSS_RISK` error with a `resolution:failed` event instead of completing silently. The one case that still applies is a delete-modify conflict resolved toward the side that keeps content. Set `allowLossyFallback` to apply the fallback regardless.
283
+ - `allowLossyFallback` — opt back into the pre-guard behavior: apply `fallbackStrategy` wholesale even when it would discard integrated work. Off by default.
283
284
 
284
285
  Available strategies for rules and `defaultStrategy`:
285
286
 
@@ -305,7 +306,7 @@ import { detect } from '@gitkraken/conflict-tools';
305
306
  const report = await detect(step.resolutions, { refs }, { git, parser });
306
307
  ```
307
308
 
308
- `DetectDeps` is `{ git; parser?; signal? }`. It flags:
309
+ `DetectDeps` is `{ git; parser?; signal?; excludePaths? }`. `excludePaths` (a `ReadonlySet<string>`) keeps paths out of scope — pass the files you skipped or failed to resolve, since those still carry conflict markers on disk and the parser would report the marker text as syntax errors. It flags:
309
310
 
310
311
  - references that resolved in a pre-merge parent (ours/theirs) but no longer resolve in the merged result (`broken-reference`, with `provenance`),
311
312
  - duplicate definitions and syntax errors across the whole analyzed scope — the resolved files *and* the auto-merged/incoming ones (`duplicate-definition`, `syntax-error`). Duplicates are diffed against the parents, so a legal same-name declaration that already existed (a TS overload, `interface`/`namespace` merging) is not reported; only a duplicate the merge introduced is,
@@ -380,7 +381,7 @@ const digest = buildOperationDigest(step);
380
381
 
381
382
  **Gate auto-apply on the verdict, not on confidence.** A refined or fixed file has its confidence capped at 0.9 — so a "confidence must be 1.0" gate would reject exactly the files the tool improved. `verdict === 'clean'` is the honest signal. And an *absent* verdict must NOT be read as clean: if you never ran detection (no parser) or never summarized, `detectionComplete` is `false` and empty findings mean "we didn't look," not "nothing's wrong." Only `verdict === 'clean'` is clean.
382
383
 
383
- A `ReviewItem` distinguishes a low-confidence flag from a finding: `reason: 'low-confidence' | 'finding'`, with `confidence?` (absent for a file flagged purely by a finding, e.g. an auto-merged file with no resolution of its own) and `findingCategories?` (present when `reason` is `'finding'`).
384
+ A `ReviewItem` distinguishes a low-confidence flag from a finding: `reason: 'low-confidence' | 'finding' | 'unresolved'` (`'unresolved'` marks an unmerged entry the tool left untouched — a no-markers skip like a binary add/add — which keeps the operation off `clean`), with `confidence?` (absent for a file flagged purely by a finding or as unresolved, e.g. an auto-merged file with no resolution of its own) and `findingCategories?` (present when `reason` is `'finding'`).
384
385
 
385
386
  ## Fix phase (opt-in)
386
387
 
@@ -406,7 +407,7 @@ Without `ResolutionContext.refs` (or `detectionRefs`) the per-file stage still r
406
407
  | `crossFileMaxIterations` | `2` | Max cross-file fix rounds (route project-level breaks → re-detect the set) |
407
408
  | `maxModelCalls` | unset | Global model-call budget for the whole operation. When set and the budget is spent, the fix phase stops attempting further fixes and leaves the remainder in the findings, emitting `fix:budget-exhausted`. When unset, the fix phase is not budget-limited |
408
409
 
409
- The fix phase is **fault-isolated**: a parser, validator, or model throw inside it does not reject the whole operation. The resolutions computed so far (possibly partially fixed, since the fix phase mutates them as it goes) are kept, both detection checks are reported as `errored`, and a `fix:failed` event is emitted (a user abort still propagates and cancels the operation).
410
+ The fix phase is **fault-isolated** at two levels. A throw on one file (parser, validator, or model) leaves that file at its pre-fix resolution, emits `fix:failed` with its `filePath`, and the phase moves on — detection still runs over the set. A throw that escapes the whole phase does not reject the operation either: the resolutions computed so far (possibly partially fixed, since the phase mutates them as it goes) are kept, both detection checks are reported as `errored`, and `fix:failed` is emitted without a `filePath`. A user abort always propagates and cancels the operation.
410
411
 
411
412
  ## Consumer examples
412
413
 
@@ -492,11 +493,11 @@ score(resolution, goldenFile);
492
493
  | `refine:completed` | Refine finished successfully (`appliedCount`) |
493
494
  | `refine:failed` | Refine threw before completing (`reason`) |
494
495
  | `refine:bailed` | Refine bailed early after consecutive apply errors, keeping edits that landed (`reason`) |
495
- | `refine:discarded` | A refine/fix edit batch was rejected because it reintroduced conflict markers (`reason`) |
496
+ | `refine:discarded` | A refine/fix edit batch was rejected — it reintroduced conflict markers, or (in the fix phase) it introduced an error the file did not have (`reason` says which) |
496
497
  | `refine:touched-user-region` | Refine modified a user-resolved marker region (`markerIndexes`) |
497
498
  | `fix:started` | The fix loop began fixing a file (`findingCount`) |
498
499
  | `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" |
499
- | `fix:failed` | The opt-in fix phase threw a non-abort error (`filePath?`, `reason`); the operation degrades gracefully — the resolutions (possibly partially fixed) are kept and both checks are reported as `errored` |
500
+ | `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` |
500
501
  | `fix:budget-exhausted` | The global `maxModelCalls` budget was spent (`spent`, `limit`); the fix phase stopped and left the remainder in findings |
501
502
  | `validation:failed` | The consumer validator threw; reported and swallowed, resolution kept (`reason`) |
502
503
 
@@ -509,6 +510,7 @@ Operation-level events (rebase started, step completed, etc.) are the consumer's
509
510
  | Error | Codes | When |
510
511
  |---|---|---|
511
512
  | `ConflictError` | `PARSE_FAILED` | Conflict extraction failed |
513
+ | `ConflictError` | `CODE_LOSS_RISK` | A wholesale `fallbackStrategy` would have discarded integrated work; the fallback was not applied |
512
514
  | `AIError` | `VALIDATION_EXHAUSTED`, `INCOMPLETE_RESOLUTION` | Model could not produce a valid resolution after retries |
513
515
  | `ConflictGitPortMissingOpError` | — | A required git op is missing and no `exec` fallback was provided |
514
516
 
@@ -526,7 +528,14 @@ try {
526
528
  }
527
529
  ```
528
530
 
529
- In the batch entry point, errors that have a configured fallback do not throw — they are downgraded to `resolution:fallback` events, and the resulting resolutions appear in `step.resolutions`. Errors without a fallback land in `step.errors` and the loop continues with the next file. The advisory stages (refine, validator, detection, fix phase) are fault-isolated — a non-abort throw there is reported and swallowed rather than destroying a good resolution. A user abort (`signal.aborted`) always propagates and cancels the operation.
531
+ In the batch entry point, errors do not throw; the loop continues with the next file. Where they land depends on the fallback:
532
+
533
+ - No `fallbackStrategy`: the error goes to `step.errors`.
534
+ - `fallbackStrategy` set, text conflict: the code-loss guard stops it, so a `CODE_LOSS_RISK` error goes to `step.errors` with a `resolution:failed` event. This is the common path, not the exception.
535
+ - `fallbackStrategy` set, delete-modify resolved toward the side that keeps content: downgraded to a `resolution:fallback` event, with the resolution in `step.resolutions`.
536
+ - `allowLossyFallback: true`: the guard is off, so every fallback takes the `resolution:fallback` path.
537
+
538
+ The advisory stages (refine, validator, detection, fix phase) are fault-isolated — a non-abort throw there is reported and swallowed rather than destroying a good resolution. A user abort (`signal.aborted`) always propagates and cancels the operation.
530
539
 
531
540
  ## Semver policy
532
541
 
@@ -1,5 +1,5 @@
1
1
  //#region src/errors/conflict.d.ts
2
- type ConflictErrorCode = 'PARSE_FAILED';
2
+ type ConflictErrorCode = 'PARSE_FAILED' | 'CODE_LOSS_RISK';
3
3
  declare class ConflictError extends Error {
4
4
  readonly code: ConflictErrorCode;
5
5
  constructor(code: ConflictErrorCode, message: string, options?: {
@@ -1,2 +1,2 @@
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(`
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 a?.fullContent?o: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
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([`-c`,`core.quotepath=false`,`diff`,`--name-only`,`-z`,`--end-of-options`,t,n],{signal:r?.signal})).split(`\0`).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
@@ -13,6 +13,7 @@ import { ParsedFile, ParserCapabilities, ParserPort, SourceOverlay, SymbolDefini
13
13
  import { Validator, ValidatorFinding } from "./ports/verify.js";
14
14
  import { DetectDeps, detect } from "./reference-integrity/detect.js";
15
15
  import { createResolution } from "./resolution/create-resolution.js";
16
+ import { CodeLossDetection, detectFallbackCodeLoss } from "./resolution/detect-code-loss.js";
16
17
  import { takeOurs, takeTheirs } from "./resolution/take-strategy.js";
17
18
  import { applyResolutions } from "./resolver/apply-resolutions.js";
18
19
  import { ConflictProgressEvent } from "./types/events.js";
@@ -21,4 +22,4 @@ import { FileRule, StepConfig, StepResult } from "./types/step.js";
21
22
  import { resolveConflicts } from "./resolver/resolve-conflicts.js";
22
23
  import { OperationDigest, OperationSummary, OperationVerdict, ReviewItem } from "./types/summary.js";
23
24
  import { SummarizeOperationOptions, buildOperationDigest, summarizeOperation } from "./resolver/summarize-operation.js";
24
- 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 LogOptions, type OpOptions, type OperationDigest, type OperationSummary, type OperationVerdict, type ParsedFile, type ParserCapabilities, type ParserPort, type Resolution, type ResolutionContext, type ResolutionMetrics, type ResolutionRawSignals, type ResolutionRefs, type ResolutionStrategy, type ResolvedChunk, type ResolverConfig, type ReviewItem, type ShowFileOptions, type ShowOptions, type SourceOverlay, type StepConfig, type StepResult, type SummarizeOperationOptions, type SymbolDefinition, type SymbolReference, type SyntaxIssue, type ThreeWayDiff, type ToolCall, type ToolDefinition, type ToolResult, type UnmergedEntry, type UnmergedReason, type Validator, type ValidatorFinding, applyResolutions, buildOperationDigest, checkoutFile, createResolution, detect, extractConflict, removeFile, resolveConflict, resolveConflicts, show, summarizeOperation, takeOurs, takeTheirs };
25
+ export { AIError, type AIErrorCode, type BlameOptions, type CheckStatus, type CodeLossDetection, 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 LogOptions, type OpOptions, type OperationDigest, type OperationSummary, type OperationVerdict, type ParsedFile, type ParserCapabilities, type ParserPort, type Resolution, type ResolutionContext, type ResolutionMetrics, type ResolutionRawSignals, type ResolutionRefs, type ResolutionStrategy, type ResolvedChunk, type ResolverConfig, type ReviewItem, type ShowFileOptions, type ShowOptions, type SourceOverlay, type StepConfig, type StepResult, type SummarizeOperationOptions, type SymbolDefinition, type SymbolReference, type SyntaxIssue, type ThreeWayDiff, type ToolCall, type ToolDefinition, type ToolResult, type UnmergedEntry, type UnmergedReason, type Validator, type ValidatorFinding, applyResolutions, buildOperationDigest, checkoutFile, createResolution, detect, detectFallbackCodeLoss, 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{detect as s}from"./reference-integrity/detect.js";import{createResolution as c}from"./resolution/create-resolution.js";import{takeOurs as l,takeTheirs as u}from"./resolution/take-strategy.js";import{applyResolutions as d}from"./resolver/apply-resolutions.js";import{resolveConflict as f}from"./resolver/resolve-conflict.js";import{resolveConflicts as p}from"./resolver/resolve-conflicts.js";import{buildOperationDigest as m,summarizeOperation as h}from"./resolver/summarize-operation.js";export{e as AIError,t as ConflictError,n as ConflictGitPortMissingOpError,d as applyResolutions,m as buildOperationDigest,r as checkoutFile,c as createResolution,s as detect,o as extractConflict,i as removeFile,f as resolveConflict,p as resolveConflicts,a as show,h as summarizeOperation,l as takeOurs,u as takeTheirs};
1
+ import{AIError as e}from"./errors/ai.js";import{ConflictError as t}from"./errors/conflict.js";import{ConflictGitPortMissingOpError as n}from"./ports/git.js";import{checkoutFile as r,removeFile as i,show as a}from"./git/port-dispatch.js";import{extractConflict as o}from"./extraction/extract.js";import{detect as s}from"./reference-integrity/detect.js";import{createResolution as c}from"./resolution/create-resolution.js";import{detectFallbackCodeLoss as l}from"./resolution/detect-code-loss.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,c as createResolution,s as detect,l as detectFallbackCodeLoss,o as extractConflict,i as removeFile,p as resolveConflict,m as resolveConflicts,a as show,g as summarizeOperation,u as takeOurs,d as takeTheirs};
@@ -11,6 +11,10 @@ interface ConflictDiffOptions extends OpOptions {
11
11
  interface ShowFileOptions extends OpOptions {
12
12
  startLine?: number;
13
13
  endLine?: number;
14
+ /** Skip the line cap the dispatcher applies by default. The cap keeps file content read for model
15
+ * prompts small; a caller feeding the content to a parser (not a prompt) needs the whole file, since
16
+ * a definition past the cap would silently drop out of the before/after comparison. */
17
+ fullContent?: boolean;
14
18
  }
15
19
  interface BlameOptions extends OpOptions {
16
20
  ref?: string;
@@ -13,6 +13,8 @@ interface DetectDeps {
13
13
  /** Paths to keep out of scope — the caller's skipped and failed files, which still carry conflict
14
14
  * markers on disk. See {@link import('./snapshot.js').SnapshotDeps.excludePaths}. */
15
15
  excludePaths?: ReadonlySet<string>;
16
+ /** Override the per-operation scope cap. See {@link import('./snapshot.js').SnapshotDeps.maxScopeFiles}. */
17
+ maxScopeFiles?: number;
16
18
  }
17
19
  /** Parent contents for one file, named so the two sides cannot be transposed at a call site. */
18
20
  interface ParentContents {
@@ -1,3 +1,3 @@
1
- import{toFileFinding as e}from"../ports/verify.js";import{buildSnapshots as t}from"./snapshot.js";async function n(e,n,i){let o=i.parser;if(!o)return{findings:[],checks:{referenceIntegrity:`skipped-no-parser`,structural:`skipped-no-parser`}};try{let c=n.detectionRefs??n.refs?await t(e,n,i):null;if(c){let t=await o.analyze(c.ours,i.signal),n=await o.analyze(c.theirs,i.signal),u=await o.analyze(c.result,i.signal);return{findings:[...a(t,n,u),...s(u,[t,n]),...l(e,c)],checks:{referenceIntegrity:r(c,o,[t,n,u]),structural:d(u)?`partial-unsupported-language`:`ran`},scope:{inScope:c.inScope,readable:c.readable,truncated:c.truncated,incomingDiscovered:c.incomingDiscovered}}}let u=new Map(e.map(e=>[e.filePath,e.content])),f=await o.analyze(u,i.signal);return{findings:s(f,[]),checks:{referenceIntegrity:`skipped-no-refs`,structural:d(f)?`partial-unsupported-language`:`ran`}}}catch(e){if(i.signal?.aborted)throw e;return{findings:[],checks:{referenceIntegrity:`errored`,structural:`errored`}}}}function r(e,t,n){return e.inScope>0&&e.readable===0?`skipped-unreadable-source`:t.capabilities?.crossFileResolution?n.some(d)?`partial-unsupported-language`:`ran`:`partial-no-cross-file-resolution`}async function i(t,n,r,i){let o=async e=>e==null?new Map:i.parser.analyze(new Map([[t,e]]),i.signal),c=await o(n.ours),l=await o(n.theirs),u=await i.parser.analyze(new Map([[t,r]]),i.signal),d=[...a(c,l,u).filter(e=>e.tier===`file`),...s(u,[c,l])],f=[];if(i.validator){let n=await i.validator.validate(new Map([[t,r]]),i.signal);for(let t of n)f.push(e(t))}return{parser:d,validator:f}}function a(e,t,n){let r=o(e,t),i=[];for(let[e,t]of n)for(let n of t.references){if(n.resolved)continue;let t=r.get(f(e,n.name));t&&i.push({category:`broken-reference`,severity:`error`,tier:t.crossFile?`project`:`file`,location:{file:e,line:n.line,symbol:n.name},provenance:{side:t.side,line:t.line},message:`Reference "${n.name}" resolved before the merge (on ${t.side}) but no longer resolves.`})}return i}function o(e,t){let n=new Map,r=(e,t,r,i,a)=>{let o=f(t,r);n.has(o)||n.set(o,{side:e,line:i,crossFile:a})},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);return n}function s(e,t){let n=c(t),r=[];for(let[t,i]of e){let e=new Map;for(let a of i.definitions){let i=e.get(a.name);if(i==null){e.set(a.name,a.line);continue}n.has(f(t,a.name))||r.push({category:`duplicate-definition`,severity:`warning`,tier:`file`,location:{file:t,line:a.line,symbol:a.name},message:`Duplicate definition of "${a.name}" (also defined at line ${i}).`})}for(let e of i.syntaxErrors)r.push({category:`syntax-error`,severity:`error`,tier:`file`,location:{file:t,line:e.line},message:e.message})}return r}function c(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=f(e,i.name);n.has(r)&&t.add(r),n.add(r)}}return t}function l(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=u(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 u(e,t){let n=new Set(t.split(`
1
+ import{toFileFinding as e}from"../ports/verify.js";import{buildSnapshots as t}from"./snapshot.js";async function n(e,n,i){let a=i.parser;if(!a)return{findings:[],checks:{referenceIntegrity:`skipped-no-parser`,structural:`skipped-no-parser`}};try{let s=n.detectionRefs??n.refs?await t(e,n,i):null;if(s){let t=await a.analyze(s.ours,i.signal),n=await a.analyze(s.theirs,i.signal),l=await a.analyze(s.result,i.signal);return{findings:[...o(t,n,l),...c(l,[t,n]),...u(e,s)],checks:{referenceIntegrity:r(s,a,[t,n,l]),structural:f(l)?`partial-unsupported-language`:`ran`},scope:{inScope:s.inScope,readable:s.readable,truncated:s.truncated,incomingDiscovered:s.incomingDiscovered}}}let l=new Map(e.map(e=>[e.filePath,e.content])),d=await a.analyze(l,i.signal);return{findings:c(d,[]),checks:{referenceIntegrity:`skipped-no-refs`,structural:f(d)?`partial-unsupported-language`:`ran`}}}catch(e){if(i.signal?.aborted)throw e;return{findings:[],checks:{referenceIntegrity:`errored`,structural:`errored`}}}}function r(e,t,n){return e.inScope>0&&e.readable===0?`skipped-unreadable-source`:t.capabilities?.crossFileResolution?n.some(f)?`partial-unsupported-language`:`ran`:`partial-no-cross-file-resolution`}async function i(t,n,r,i){let a=async e=>e==null?new Map:i.parser.analyze(new Map([[t,e]]),i.signal),s=await a(n.ours),l=await a(n.theirs),u=await i.parser.analyze(new Map([[t,r]]),i.signal),d=[...o(s,l,u).filter(e=>e.tier===`file`),...c(u,[s,l])],f=[];if(i.validator){let n=await i.validator.validate(new Map([[t,r]]),i.signal);for(let t of n)f.push(e(t))}return{parser:d,validator:f}}function a(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 o(e,t,n){let r=s(e,t),i=[];for(let[e,t]of n){let n=a(t);for(let a of t.references){if(a.resolved||n.has(a.name))continue;let t=r.get(p(e,a.name));t&&i.push({category:`broken-reference`,severity:`error`,tier:t.crossFile?`project`:`file`,location:{file:e,line:a.line,symbol:a.name},provenance:{side:t.side,line:t.line},message:`Reference "${a.name}" resolved before the merge (on ${t.side}) but no longer resolves.`})}}return i}function s(e,t){let n=new Map,r=(e,t,r,i,a)=>{let o=p(t,r);n.has(o)||n.set(o,{side:e,line:i,crossFile:a})},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);return n}function c(e,t){let n=l(t),r=new Set;for(let e of t)for(let t of e.keys())r.add(t);let i=[];for(let[t,a]of e){let e=r.has(t),o=new Map;for(let r of a.definitions){let a=o.get(r.name);if(a==null){o.set(r.name,r.line);continue}!e||n.has(p(t,r.name))||i.push({category:`duplicate-definition`,severity:`warning`,tier:`file`,location:{file:t,line:r.line,symbol:r.name},message:`Duplicate definition of "${r.name}" (also defined at line ${a}).`})}for(let e of a.syntaxErrors)i.push({category:`syntax-error`,severity:`error`,tier:`file`,location:{file:t,line:e.line},message:e.message})}return i}function l(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=p(e,i.name);n.has(r)&&t.add(r),n.add(r)}}return t}function u(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=d(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 d(e,t){let n=new Set(t.split(`
2
2
  `)),r=0;for(let t of e.split(`
3
- `))t.trim()!==``&&!n.has(t)&&r++;return r}function d(e){for(let t of e.values())if(!t.supported)return!0;return!1}function f(e,t){return`${e}\u0000${t}`}export{i as collectFileFindings,n as detect};
3
+ `))t.trim()!==``&&!n.has(t)&&r++;return r}function f(e){for(let t of e.values())if(!t.supported)return!0;return!1}function p(e,t){return`${e}\u0000${t}`}export{i as collectFileFindings,n as detect};
@@ -3,6 +3,10 @@ import { Resolution, ResolutionContext } from "../types/resolution.js";
3
3
  import { SourceOverlay } from "../ports/parser.js";
4
4
 
5
5
  //#region src/reference-integrity/snapshot.d.ts
6
+ /** Default backstop on how many files the detector will parse in one operation, overridable via
7
+ * `SnapshotDeps.maxScopeFiles`. The scope is ordered resolved-first, so a cap drops the least-important
8
+ * tail; truncation is reported, never silent. */
9
+ declare const DEFAULT_MAX_SCOPE_FILES = 400;
6
10
  interface DetectionSnapshots {
7
11
  ours: SourceOverlay;
8
12
  theirs: SourceOverlay;
@@ -24,15 +28,18 @@ interface SnapshotDeps {
24
28
  * still carry conflict markers in the working tree, so parsing them would report the marker text as
25
29
  * syntax errors and blame the merge for a file nobody resolved. */
26
30
  excludePaths?: ReadonlySet<string>;
31
+ /** Cap on files parsed per operation. Default {@link DEFAULT_MAX_SCOPE_FILES}. Exceeding it sets
32
+ * `truncated`, which keeps the verdict off `clean`; raise it so a large merge can still auto-apply. */
33
+ maxScopeFiles?: number;
27
34
  }
28
35
  /**
29
36
  * Builds the file-content overlays the detector compares: both pre-merge parents (read from git
30
37
  * objects — no working tree needed) and the resolved result (in-memory resolutions overlaid on the
31
38
  * surrounding files). Scope is the resolved files plus the merge's incoming/auto-merged set, which
32
39
  * the detector discovers itself via the git port (`changedFiles(base, theirs)`), unless the consumer
33
- * supplies `context.touchedFiles` as an override. Returns `null` when `context.refs` is absent — the
34
- * before/after comparison is impossible without the parent refs.
40
+ * supplies `context.touchedFiles` as an override. Returns `null` when neither `context.detectionRefs`
41
+ * nor `context.refs` is present — the before/after comparison is impossible without the parent refs.
35
42
  */
36
43
  declare function buildSnapshots(resolutions: Resolution[], context: ResolutionContext, deps: SnapshotDeps): Promise<DetectionSnapshots | null>;
37
44
  //#endregion
38
- export { DetectionSnapshots, SnapshotDeps, buildSnapshots };
45
+ export { DEFAULT_MAX_SCOPE_FILES, DetectionSnapshots, SnapshotDeps, buildSnapshots };
@@ -1 +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.detectionRefs??t.refs;if(!r)return null;let i=n.signal?{signal:n.signal}:void 0,c=new Map(e.map(e=>[e.filePath,e.content])),l=t.touchedFiles?{files:t.touchedFiles,discovered:!0}:await a(n.git,r,i),u=n.excludePaths,d=[...new Set([...c.keys(),...l.files,...t.fileNeighbors??[]])].filter(e=>!u?.has(e)),f=d.length>400,p=f?d.slice(0,400):d,m=new Map,h=new Map,g=new Map,_=0;for(let e of p){let t=await o(n.git,r.ours,e,i);t&&m.set(e,t);let a=await o(n.git,r.theirs,e,i);a&&h.set(e,a),(t||a)&&_++;let l=c.get(e);if(l!=null)g.set(e,l);else{let t=await s(n.git,e,i);t!=null&&g.set(e,t)}}return{ours:m,theirs:h,result:g,inScope:p.length,readable:_,truncated:f,incomingDiscovered:l.discovered}}async function a(n,r,i){try{return{files:await e(n,r.base??await t(n,r.ours,r.theirs,i),r.theirs,i),discovered:!0}}catch(e){return c(e,i),{files:[],discovered:!1}}}async function o(e,t,n,i){try{return await r(e,t,n,i)}catch(e){return c(e,i),null}}async function s(e,t,r){try{return await n(e,t,r)}catch(e){return c(e,r),null}}function c(e,t){if(t?.signal?.aborted)throw e}export{i as buildSnapshots};
1
+ import{changedFiles as e,mergeBase as t,readFile as n,showFile as r}from"../git/port-dispatch.js";const i=400;async function a(e,t,n){let r=t.detectionRefs??t.refs;if(!r)return null;let i=n.signal?{signal:n.signal}:void 0,a={...i,fullContent:!0},l=new Map(e.map(e=>[e.filePath,e.content])),u=t.touchedFiles?{files:t.touchedFiles,discovered:!0}:await o(n.git,r,i),d=n.excludePaths,f=[...new Set([...l.keys(),...u.files,...t.fileNeighbors??[]])].filter(e=>!d?.has(e)),p=n.maxScopeFiles??400,m=f.length>p,h=m?f.slice(0,p):f,g=new Map,_=new Map,v=new Map,y=0;for(let e of h){let t=await s(n.git,r.ours,e,a);t&&g.set(e,t);let o=await s(n.git,r.theirs,e,a);o&&_.set(e,o),(t||o)&&y++;let u=l.get(e);if(u!=null)v.set(e,u);else{let t=await c(n.git,e,i);t!=null&&v.set(e,t)}}return{ours:g,theirs:_,result:v,inScope:h.length,readable:y,truncated:m,incomingDiscovered:u.discovered}}async function o(n,r,i){try{return{files:await e(n,r.base??await t(n,r.ours,r.theirs,i),r.theirs,i),discovered:!0}}catch(e){return l(e,i),{files:[],discovered:!1}}}async function s(e,t,n,i){try{return await r(e,t,n,i)}catch(e){return l(e,i),null}}async function c(e,t,r){try{return await n(e,t,r)}catch(e){return l(e,r),null}}function l(e,t){if(t?.signal?.aborted)throw e}export{i as DEFAULT_MAX_SCOPE_FILES,a as buildSnapshots};
@@ -0,0 +1,23 @@
1
+ import { Conflict } from "../types/conflict.js";
2
+
3
+ //#region src/resolution/detect-code-loss.d.ts
4
+ interface CodeLossDetection {
5
+ /** The side thrown away by the wholesale fallback. */
6
+ discardedSide: 'ours' | 'theirs';
7
+ }
8
+ /**
9
+ * Reports whether a wholesale `take-ours`/`take-theirs` fallback would discard already-integrated
10
+ * work. On rebase "ours" is everything resolved so far, so taking the other side wholesale can wipe
11
+ * it (AGP-430).
12
+ *
13
+ * Any text conflict counts as loss. The fallback is applied with `git checkout --ours/--theirs`,
14
+ * which swaps the whole file for one stage, so whatever git auto-merged outside the conflict markers
15
+ * is discarded with it. Comparing marker regions cannot rule that out: it answers a narrower question
16
+ * than the write asks.
17
+ *
18
+ * Returns `null` only for the delete-modify direction that keeps content, the one case where the
19
+ * fallback is safe by construction.
20
+ */
21
+ declare function detectFallbackCodeLoss(conflict: Conflict, strategy: 'take-ours' | 'take-theirs'): CodeLossDetection | null;
22
+ //#endregion
23
+ export { CodeLossDetection, detectFallbackCodeLoss };
@@ -0,0 +1 @@
1
+ function e(e,t){let n=t===`take-theirs`?`ours`:`theirs`;return e.type===`delete-modify`?t===`take-theirs`&&e.deletedBy===`theirs`||t===`take-ours`&&e.deletedBy===`ours`?{discardedSide:n}:null:e.markers.length>0?{discardedSide:n}:null}export{e as detectFallbackCodeLoss};
@@ -1,2 +1,2 @@
1
- import{checkResidualMarkers as e}from"../ports/verify.js";import{buildSnapshots as t}from"../reference-integrity/snapshot.js";import{collectFileFindings as n,detect as r}from"../reference-integrity/detect.js";import{runRefinePass as i}from"./refine-pass.js";import{CONFIDENCE_CAPS as a}from"./resolve-conflict.js";async function o(t,r,o){let d=o.config?.fixLoopMaxIterations??3,p={parser:o.parser,validator:o.validator,signal:o.signal},m={content:t.content,findings:await(e=>n(t.filePath,r,e,p))(t.content)},h=s(m.findings);if(h.length===0)return l(o,t.filePath,0,0),c(t,m.findings.validator);if(o.budget?.exhausted())return o.onBudgetExhausted?.(),l(o,t.filePath,0,h.length),c(t,m.findings.validator);o.onProgress?.({type:`fix:started`,filePath:t.filePath,findingCount:h.length});let g=[],_=0,v=f(h);for(;_<d&&h.length>0;){if(o.budget?.exhausted()){o.onBudgetExhausted?.();break}_++;let a=await i(m.content,t.description,u(h),{model:o.model,signal:o.signal,temperature:o.config?.temperature,refineMaxSteps:o.config?.refineMaxSteps,onProgress:o.onProgress,filePath:t.filePath,budget:o.budget});if(a.edits.length===0)break;if(!e(t.filePath,a.content).valid){o.onProgress?.({type:`refine:discarded`,filePath:t.filePath,reason:`Fix edits introduced conflict markers.`});break}let c=await n(t.filePath,r,a.content,p),l=s(c);if(l.length>h.length){o.onProgress?.({type:`refine:discarded`,filePath:t.filePath,reason:`Fix edits raised the error count from ${h.length} to ${l.length}.`});break}m={content:a.content,findings:c},g.push(...a.edits),h=l;let d=f(h);if(d===v)break;v=d}l(o,t.filePath,_,h.length);let y=c({...t,content:m.content},m.findings.validator);return g.length===0?y:{...y,strategy:`ai`,edits:[...t.edits??[],...g],confidence:Math.min(y.confidence,a.refineApplied)}}function s(e){return[...e.parser,...e.validator].filter(e=>e.severity===`error`)}function c(e,t){if(t.length===0){if(!e.findings)return e;let{findings:t,...n}=e;return n}return{...e,findings:t}}function l(e,t,n,r){e.onProgress?.({type:`fix:completed`,filePath:t,iterations:n,remainingFindings:r})}function u(e){return e.slice(0,20).map(d)}function d(e){let t=e.location.symbol?`"${e.location.symbol}" (line ${e.location.line})`:`line ${e.location.line}`,n=e.message.replace(/\s+/g,` `).trim().slice(0,300);return`${e.category} at ${t}: ${n}`}function f(e){return e.map(e=>JSON.stringify([e.category,e.location.file,e.location.symbol??``])).sort().join(`
2
- `)}async function p(e,n,i){let a={git:i.git,parser:i.parser,signal:i.signal,excludePaths:i.excludePaths},o=await t(e,n,{git:i.git,signal:i.signal,excludePaths:i.excludePaths}),s=()=>i.onProgress?.({type:`fix:budget-exhausted`,spent:i.budget?.spent()??0,limit:i.budget?.limit()??0}),c={model:i.model,parser:i.parser,validator:i.validator,config:i.config,onProgress:i.onProgress,signal:i.signal,budget:i.budget,onBudgetExhausted:()=>i.budget?.notifyExhaustedOnce(s)};return await m(e,o,c),o?h(e,n,o,a,c,i):r(e,n,a)}async function m(e,t,n){for(let r=0;r<e.length;r++){let i=e[r];if(!(!i||i.strategy!==`ai`)){if(n.signal?.aborted)return;try{e[r]=await o(i,g(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 h(e,t,n,i,a,o){let s=o.config?.crossFileMaxIterations??2,c=()=>{a.onBudgetExhausted?.()},l=await r(e,t,i);for(let u=0;u<s&&!o.signal?.aborted;u++){if(o.budget?.exhausted()){c();break}let s=v(l.findings,e);if(s.size===0)break;let u=!1;for(let[t,r]of s){if(o.budget?.exhausted()){c();break}let i=e.findIndex(e=>e.filePath===t);if(i<0)continue;let s=e[i],l=s.content,d=await _(s,r,g(n,t),a);e[i]=d,d.content!==l&&(u=!0)}if(!u)break;l=await r(e,t,i)}return l}function g(e,t){return{ours:e?.ours.get(t),theirs:e?.theirs.get(t)}}async function _(t,r,o,s){let l=await i(t.content,t.description,u(r),{model:s.model,signal:s.signal,temperature:s.config?.temperature,refineMaxSteps:s.config?.refineMaxSteps,onProgress:s.onProgress,filePath:t.filePath,budget:s.budget});if(l.edits.length===0)return t;if(!e(t.filePath,l.content).valid)return s.onProgress?.({type:`refine:discarded`,filePath:t.filePath,reason:`Cross-file fix introduced conflict markers.`}),t;let d=await n(t.filePath,o,l.content,{parser:s.parser,validator:s.validator,signal:s.signal});return c({...t,content:l.content,strategy:`ai`,edits:[...t.edits??[],...l.edits],confidence:Math.min(t.confidence,a.refineApplied)},d.validator)}function v(e,t){let n=new Set(t.filter(e=>e.strategy===`ai`).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}export{o as runFixLoop,p as runFixPhase};
1
+ import{checkResidualMarkers as e}from"../ports/verify.js";import{buildSnapshots as t}from"../reference-integrity/snapshot.js";import{collectFileFindings as n,detect as r}from"../reference-integrity/detect.js";import{runRefinePass as i}from"./refine-pass.js";import{CONFIDENCE_CAPS as a}from"./resolve-conflict.js";async function o(t,r,o){let d=o.config?.fixLoopMaxIterations??3,f={parser:o.parser,validator:o.validator,signal:o.signal},p={content:t.content,findings:await(e=>n(t.filePath,r,e,f))(t.content)},g=s(p.findings);if(g.length===0)return l(o,t.filePath,0,0),c(t,p.findings.validator);if(o.budget?.exhausted())return o.onBudgetExhausted?.(),l(o,t.filePath,0,g.length),c(t,p.findings.validator);o.onProgress?.({type:`fix:started`,filePath:t.filePath,findingCount:g.length});let _=[],v=0,y=m(g);for(;v<d&&g.length>0;){if(o.budget?.exhausted()){o.onBudgetExhausted?.();break}v++;let a=await i(p.content,t.description,u(g),{model:o.model,signal:o.signal,temperature:o.config?.temperature,refineMaxSteps:o.config?.refineMaxSteps,onProgress:o.onProgress,filePath:t.filePath,budget:o.budget});if(a.edits.length===0)break;if(!e(t.filePath,a.content).valid){o.onProgress?.({type:`refine:discarded`,filePath:t.filePath,reason:`Fix edits introduced conflict markers.`});break}let c=await n(t.filePath,r,a.content,f),l=s(c);if(h(g,l)){o.onProgress?.({type:`refine:discarded`,filePath:t.filePath,reason:`Fix edits regressed the file (was ${g.length} error(s), now ${l.length}).`});break}p={content:a.content,findings:c},_.push(...a.edits),g=l;let d=m(g);if(d===y)break;y=d}l(o,t.filePath,v,g.length);let b=c({...t,content:p.content},p.findings.validator);return _.length===0?b:{...b,strategy:`ai`,edits:[...t.edits??[],..._],confidence:Math.min(b.confidence,a.refineApplied)}}function s(e){return[...e.parser,...e.validator].filter(e=>e.severity===`error`)}function c(e,t){if(t.length===0){if(!e.findings)return e;let{findings:t,...n}=e;return n}return{...e,findings:t}}function l(e,t,n,r){e.onProgress?.({type:`fix:completed`,filePath:t,iterations:n,remainingFindings:r})}function u(e){return e.slice(0,20).map(d)}function d(e){let t=e.location.symbol?f(e.location.symbol):void 0,n=t?`"${t}" (line ${e.location.line})`:`line ${e.location.line}`;return`${f(e.category)} at ${n}: ${f(e.message)}`}function f(e){return e.replace(/[\p{Cc}\p{Cf}]/gu,` `).replace(/\s+/g,` `).trim().slice(0,300)}function p(e){return JSON.stringify([e.category,e.location.file,e.location.symbol??``])}function m(e){return e.map(p).sort().join(`
2
+ `)}function h(e,t){if(t.length>e.length)return!0;let n=new Set(e.map(p));return t.some(e=>!n.has(p(e)))}async function g(e,n,i){let a=i.config?.maxDetectionScopeFiles,o={git:i.git,parser:i.parser,signal:i.signal,excludePaths:i.excludePaths,maxScopeFiles:a},s=await t(e,n,{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,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 _(e,s,l),s?v(e,n,s,o,l,i):r(e,n,o)}async function _(e,t,n){for(let r=0;r<e.length;r++){let i=e[r];if(i.strategy===`ai`){if(n.signal?.aborted)return;try{e[r]=await o(i,y(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 v(e,t,n,i,a,o){let s=o.config?.crossFileMaxIterations??2,c=await r(e,t,i);for(let l=0;l<s&&!o.signal?.aborted;l++){if(o.budget?.exhausted()){a.onBudgetExhausted?.();break}let s=x(c.findings,e);if(s.size===0)break;let l=!1;for(let[t,r]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 b(s,r,y(n,t),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 r(e,t,i)}return c}function y(e,t){return{ours:e?.ours.get(t),theirs:e?.theirs.get(t)}}async function b(t,r,o,l){let d={parser:l.parser,validator:l.validator,signal:l.signal},f=s(await n(t.filePath,o,t.content,d)),p=await i(t.content,t.description,u(r),{model:l.model,signal:l.signal,temperature:l.config?.temperature,refineMaxSteps:l.config?.refineMaxSteps,onProgress:l.onProgress,filePath:t.filePath,budget:l.budget});if(p.edits.length===0)return t;if(!e(t.filePath,p.content).valid)return l.onProgress?.({type:`refine:discarded`,filePath:t.filePath,reason:`Cross-file fix introduced conflict markers.`}),t;let m=await n(t.filePath,o,p.content,d);return h(f,s(m))?(l.onProgress?.({type:`refine:discarded`,filePath:t.filePath,reason:`Cross-file fix left the file with a new or additional error.`}),t):c({...t,content:p.content,strategy:`ai`,edits:[...t.edits??[],...p.edits],confidence:Math.min(t.confidence,a.refineApplied)},m.validator)}function x(e,t){let n=new Set(t.filter(e=>e.strategy===`ai`).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}export{o as runFixLoop,g as runFixPhase};
@@ -1 +1 @@
1
- function e(e){let t=0,n=!1,r=()=>e===void 0?1/0:e-t;return{wrap(e){return{generate:n=>(t++,e.generate(n))}},spent:()=>t,limit:()=>e??0,remaining:r,exhausted:()=>r()<=0,notifyExhaustedOnce(e){n||(n=!0,e())}}}export{e as createModelBudget};
1
+ function e(e){let t=Number.isFinite(e)?e:void 0,n=0,r=!1,i=()=>t===void 0?1/0:t-n;return{wrap(e){return{generate:t=>(n++,e.generate(t))}},spent:()=>n,limit:()=>t??0,remaining:i,exhausted:()=>i()<=0,notifyExhaustedOnce(e){r||(r=!0,e())}}}export{e as createModelBudget};
@@ -1 +1 @@
1
- import{AIError as e}from"../errors/ai.js";import{unmergedEntries as t}from"../git/port-dispatch.js";import{extractConflict as n}from"../extraction/extract.js";import{detect as r}from"../reference-integrity/detect.js";import{createResolution as i}from"../resolution/create-resolution.js";import{takeOurs as a,takeTheirs as o}from"../resolution/take-strategy.js";import{resolveConflict as s}from"./resolve-conflict.js";import{runFixPhase as c}from"./fix-loop.js";import{createModelBudget as l}from"./model-budget.js";import u from"picomatch";function d(e){let t=e.map(e=>{let t=(Array.isArray(e.match)?e.match:[e.match]).map(e=>u(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 f(e,s){let u=e.config?.rules??[],f=e.config?.defaultStrategy??`ai`,g=d(u),_=await t(e.git,{signal:e.signal}),v=l(e.config?.maxModelCalls),y=v.wrap(e.model),b={...s??{}},x=[],S=[],C=[];for(let t of _){if(e.signal?.aborted)break;let r=g(t.path)??f;if(r===`skip`){C.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){S.push({filePath:t.path,error:m(e)});continue}if(!s){C.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 p(s,b,{...e,model:y},S),c&&(b.previousResolutions=[...b.previousResolutions??[],c],x.push(c),e.onProgress?.({type:`resolution:applied`,filePath:c.filePath,strategy:c.strategy,confidence:c.confidence,description:c.description}))}let w=new Set([...C.map(e=>e.filePath),...S.map(e=>e.filePath)]),T={git:e.git,parser:e.parser,signal:e.signal,excludePaths:w},E;if(e.config?.fix&&e.parser&&!e.signal?.aborted)try{E=await c(x,b,{model:y,git:e.git,parser:e.parser,validator:e.validator,config:e.config,onProgress:e.onProgress,signal:e.signal,budget:v,excludePaths:w})}catch(t){if(e.signal?.aborted)throw t;e.onProgress?.({type:`fix:failed`,reason:m(t).message}),E=await r(x,b,T),E={...E,checks:{referenceIntegrity:`errored`,structural:`errored`}}}else E=await r(x,b,T);let D=x.flatMap(e=>e.findings??[]);return{resolutions:x,errors:S,skipped:C,detection:{...E,findings:h([...E.findings,...D])}}}async function p(t,n,r,i){try{return await s(t,n,{model:r.model,git:r.git,config:r.config,onProgress:r.onProgress,signal:r.signal,validator:r.validator})}catch(n){let s=m(n);if(r.signal?.aborted)throw s;let c=r.config?.fallbackStrategy;return c?(r.onProgress?.({type:`resolution:fallback`,filePath:t.filePath,error:s,cause:s instanceof e?s.code:void 0}),{...c===`take-ours`?a(t):o(t),confidence:0,fallback:!0}):(i.push({filePath:t.filePath,error:s}),r.onProgress?.({type:`resolution:failed`,filePath:t.filePath,error:s}),null)}}function m(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{f as resolveConflicts};
1
+ import{AIError as e}from"../errors/ai.js";import{ConflictError as t}from"../errors/conflict.js";import{unmergedEntries as n}from"../git/port-dispatch.js";import{extractConflict as r}from"../extraction/extract.js";import{detect as i}from"../reference-integrity/detect.js";import{createResolution as a}from"../resolution/create-resolution.js";import{detectFallbackCodeLoss as o}from"../resolution/detect-code-loss.js";import{takeOurs as s,takeTheirs as c}from"../resolution/take-strategy.js";import{resolveConflict as l}from"./resolve-conflict.js";import{runFixPhase as u}from"./fix-loop.js";import{createModelBudget as d}from"./model-budget.js";import f from"picomatch";function p(e){let t=e.map(e=>{let t=(Array.isArray(e.match)?e.match:[e.match]).map(e=>f(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 m(e,t){let o=e.config?.rules??[],l=e.config?.defaultStrategy??`ai`,f=p(o),m=await n(e.git,{signal:e.signal}),v=d(e.config?.maxModelCalls),y=v.wrap(e.model),b={...t??{}},x=[],S=[],C=[];for(let t of m){if(e.signal?.aborted)break;let n=f(t.path)??l;if(n===`skip`){C.push({filePath:t.path,reason:`excluded`}),e.onProgress?.({type:`conflict:excluded`,filePath:t.path});continue}let i;try{i=await r(t.path,{git:e.git,signal:e.signal},t.reason)}catch(e){S.push({filePath:t.path,error:g(e)});continue}if(!i){C.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:i.filePath,conflictType:i.type,markerCount:i.markers.length});let o=null;o=n===`take-ours`?s(i):n===`take-theirs`?c(i):n===`deleted`?a(i,[],{confidence:1,fileStrategy:`deleted`}):await h(i,b,{...e,model:y},S),o&&(b.previousResolutions=[...b.previousResolutions??[],o],x.push(o),e.onProgress?.({type:`resolution:applied`,filePath:o.filePath,strategy:o.strategy,confidence:o.confidence,description:o.description}))}let w=new Set([...C.map(e=>e.filePath),...S.map(e=>e.filePath)]),T={git:e.git,parser:e.parser,signal:e.signal,excludePaths:w,maxScopeFiles:e.config?.maxDetectionScopeFiles},E;if(e.signal?.aborted)E={findings:[],checks:{referenceIntegrity:`errored`,structural:`errored`}};else if(e.config?.fix&&e.parser)try{E=await u(x,b,{model:y,git:e.git,parser:e.parser,validator:e.validator,config:e.config,onProgress:e.onProgress,signal:e.signal,budget:v,excludePaths:w})}catch(t){if(e.signal?.aborted)throw t;e.onProgress?.({type:`fix:failed`,reason:g(t).message}),E=await i(x,b,T),E={...E,checks:{referenceIntegrity:`errored`,structural:`errored`}}}else E=await i(x,b,T);let D=x.flatMap(e=>e.findings??[]);return{resolutions:x,errors:S,skipped:C,detection:{...E,findings:_([...E.findings,...D])}}}async function h(n,r,i,a){try{return await l(n,r,{model:i.model,git:i.git,config:i.config,onProgress:i.onProgress,signal:i.signal,validator:i.validator})}catch(r){let l=g(r);if(i.signal?.aborted)throw l;let u=i.config?.fallbackStrategy;if(!u)return a.push({filePath:n.filePath,error:l}),i.onProgress?.({type:`resolution:failed`,filePath:n.filePath,error:l}),null;let d=i.config?.allowLossyFallback?null:o(n,u);if(d){let e=new t(`CODE_LOSS_RISK`,`AI resolution failed and the '${u}' fallback would replace ${n.filePath} wholesale, discarding the ${d.discardedSide} side and anything git auto-merged outside the conflict markers. Stopping for manual resolution.`,{cause:l});return a.push({filePath:n.filePath,error:e}),i.onProgress?.({type:`resolution:failed`,filePath:n.filePath,error:e}),null}return i.onProgress?.({type:`resolution:fallback`,filePath:n.filePath,error:l,cause:l instanceof e?l.code:void 0}),{...u===`take-ours`?s(n):c(n),confidence:0,fallback:!0}}}function g(e){return e instanceof Error?e:Error(String(e))}function _(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{m 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},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()})}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},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};
@@ -23,6 +23,9 @@ interface StepConfig extends ResolverConfig {
23
23
  defaultStrategy?: 'ai' | 'take-ours' | 'take-theirs' | 'skip' | 'deleted';
24
24
  rules?: FileRule[];
25
25
  fallbackStrategy?: 'take-ours' | 'take-theirs';
26
+ /** Escape hatch: apply the wholesale `fallbackStrategy` even when it would discard already-integrated
27
+ * work. Off by default so a lossy fallback stops for manual resolution instead of completing silently. */
28
+ allowLossyFallback?: boolean;
26
29
  /** Opt in to the fix phase (default `false` → report-only). When enabled, an AI-resolved file carrying
27
30
  * error-severity per-file findings is routed back through refine to attempt a fix; what it cannot fix
28
31
  * remains in the findings. Requires `ResolverDeps.parser` — without a parser the fix phase is a no-op
@@ -36,6 +39,9 @@ interface StepConfig extends ResolverConfig {
36
39
  /** Max cross-file fix rounds (route project-level breaks to their use-site file → re-detect the whole
37
40
  * set) when `fix` is on. Default 2. */
38
41
  crossFileMaxIterations?: number;
42
+ /** Override the reference-integrity scope cap (default 400 files). Beyond the cap the scope is
43
+ * `truncated`, which holds the verdict off `clean`; raise it so a large merge stays auto-applyable. */
44
+ maxDetectionScopeFiles?: number;
39
45
  /** Global cap on `model.generate` calls for the whole operation (resolve + refine + fix). Default
40
46
  * `undefined` = unlimited. Primarily a backstop for the fix phase's multiplicative worst case
41
47
  * (files × fixLoopMaxIterations + crossFileMaxIterations × routable calls). When spent, the fix
@@ -2,13 +2,16 @@ import { ResolutionStrategy } from "./resolution.js";
2
2
  import { AiTokenUsage } from "@gitkraken/shared-tools";
3
3
 
4
4
  //#region src/types/summary.d.ts
5
- /** A file flagged for human review. `reason` distinguishes a low file-confidence flag from a
6
- * detection finding; `confidence` is absent for files flagged purely by a finding (e.g. an
7
- * auto-merged file with no resolution of its own). */
5
+ /** A file flagged for human review. `reason` distinguishes a low file-confidence flag, a detection
6
+ * finding, and a file the tool left unmerged. `confidence` is absent for files flagged purely by a
7
+ * finding (e.g. an auto-merged file with no resolution of its own) or as unresolved. */
8
8
  interface ReviewItem {
9
9
  filePath: string;
10
10
  confidence?: number;
11
- reason: 'low-confidence' | 'finding';
11
+ /** `unresolved` — an unmerged entry the tool did not resolve (no conflict markers to act on: a binary
12
+ * add/add, an unparseable delete-modify). It is still unmerged on disk, so the operation is not
13
+ * clean; a `skip`-rule exclusion is a deliberate human choice and is not flagged. */
14
+ reason: 'low-confidence' | 'finding' | 'unresolved';
12
15
  /** Categories of the findings on this file, of any severity (error and warning alike); present when
13
16
  * `reason` is `'finding'`. */
14
17
  findingCategories?: string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitkraken/conflict-tools",
3
- "version": "0.2.4-beta.23",
3
+ "version": "0.2.4-beta.25",
4
4
  "description": "AI-powered git conflict resolution.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Axosoft, LLC dba GitKraken",