@gitkraken/conflict-tools 0.2.4-beta.9 → 0.4.0
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 +55 -7
- package/README.md +308 -59
- package/dist/errors/conflict.d.ts +1 -1
- package/dist/extraction/extract.d.ts +15 -1
- package/dist/extraction/extract.js +1 -1
- package/dist/extraction/parser.js +5 -5
- package/dist/git/port-dispatch.d.ts +14 -2
- package/dist/git/port-dispatch.js +1 -3
- package/dist/index.d.ts +6 -6
- package/dist/index.js +1 -1
- package/dist/ports/git.d.ts +20 -1
- package/dist/ports/parser.d.ts +27 -6
- package/dist/ports/validator.d.ts +29 -0
- package/dist/ports/validator.js +1 -0
- package/dist/reference-integrity/detect.d.ts +39 -4
- package/dist/reference-integrity/detect.js +2 -2
- package/dist/reference-integrity/snapshot.d.ts +28 -6
- package/dist/reference-integrity/snapshot.js +1 -1
- package/dist/resolution/detect-code-loss.d.ts +23 -0
- package/dist/resolution/detect-code-loss.js +1 -0
- package/dist/resolver/apply-resolutions.js +1 -1
- package/dist/resolver/deps.d.ts +1 -1
- package/dist/resolver/fix-loop.d.ts +67 -0
- package/dist/resolver/fix-loop.js +2 -0
- package/dist/resolver/mechanical.d.ts +25 -0
- package/dist/resolver/mechanical.js +1 -0
- package/dist/resolver/model-budget.d.ts +26 -0
- package/dist/resolver/model-budget.js +1 -0
- package/dist/resolver/parse-response.d.ts +9 -1
- package/dist/resolver/parse-response.js +1 -1
- package/dist/resolver/prompt.d.ts +13 -2
- package/dist/resolver/prompt.js +27 -7
- package/dist/resolver/refine-pass.d.ts +8 -0
- package/dist/resolver/refine-pass.js +1 -1
- package/dist/resolver/rename-choice.d.ts +15 -0
- package/dist/resolver/rename-choice.js +1 -0
- package/dist/resolver/rename-group.d.ts +22 -0
- package/dist/resolver/rename-group.js +1 -0
- package/dist/resolver/resolve-conflict.d.ts +7 -1
- package/dist/resolver/resolve-conflict.js +1 -1
- package/dist/resolver/resolve-conflicts.js +1 -1
- package/dist/resolver/summarize-operation.js +1 -1
- package/dist/resolver/validate.js +1 -1
- package/dist/types/events.d.ts +30 -3
- package/dist/types/findings.d.ts +25 -15
- package/dist/types/resolution.d.ts +17 -1
- package/dist/types/step.d.ts +47 -5
- package/dist/types/summary.d.ts +17 -10
- package/dist/validation/findings.d.ts +9 -0
- package/dist/validation/findings.js +1 -0
- package/dist/validation/markers.d.ts +11 -0
- package/dist/{ports/verify.js → validation/markers.js} +1 -1
- package/package.json +1 -1
- package/dist/ports/verify.d.ts +0 -30
package/CHANGELOG.md
CHANGED
|
@@ -7,23 +7,71 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.0] - 2026-09-22
|
|
11
|
+
|
|
10
12
|
### Added
|
|
11
13
|
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
+
- Rename/rename(1to2) is resolved end to end: the model chooses which of the two renamed paths survives, and the whole three-path set becomes **one** `Resolution` on that path carrying the new optional `Resolution.retiredPaths` — the discarded destination and the base path. `applyResolutions` removes every retired path with `removeFile` and never stages it, so after applying, the index holds no unmerged entry for the set and the operation can be committed; previously both names were written and staged and the stage-1 base entry was left behind, so git refused the commit. `strategy` stays orthogonal to the name decision: `'ai'` when the model merged the markers into the survivor, `'mechanical'` when neither side edited the content and git had already settled it (so a renamed symlink's target is never rewritten). The content is resolved once, on the survivor, instead of twice on identical blobs. A group is only formed from an unambiguous set — exactly one stage-1-only entry, exactly two lone-side entries at matching modes, neither a gitlink, and both destinations holding identical content — and only when the effective strategy is `'ai'` for all three paths, so a rule with a non-`ai` strategy, or a non-`ai` `defaultStrategy`, still wins. An explicit `{ strategy: 'ai' }` rule does not withhold the group: it asks for AI resolution, which is what the name decision is, and it already defers to the stage signature everywhere else. The content check is what makes the set unambiguous rather than merely possible: a stage-1-only entry is equally git's both-deleted signature, so an unrelated base beside two unrelated one-sided adds at the same mode would otherwise form a group and retire a correctly merged file. On by default; the name call goes through the same wrapped model, so it counts toward `maxModelCalls`. A consumer that calls `applyResolutions` needs no change; a consumer with its own apply loop that ignores `retiredPaths` leaves those paths unmerged, which git refuses to commit — a safe failure, not a silent loss.
|
|
15
|
+
- `'rename-choice'` skip reason — a rename/rename set the model was asked about and could not settle (it threw, returned nothing, or never named one of the two candidates) yields no resolution at all and lands all three paths in `StepResult.skipped` with `entryReason` and `stages` intact. It is the "tried and could not settle" signal, distinct from a set no group formed over, where the base path is settled as a deletion on its own and only the surviving name stays open. A user abort is rethrown rather than swallowed.
|
|
16
|
+
- `'mechanical'` resolution strategy — an unmerged path git already decided for itself (a directory rename that lands a file under the new name, for example) is resolved without a model call and without reading the file: exactly one index stage present and it being stage 2 or 3 means there is no base and one side's content, so the result is already in the working tree. The resolution carries `confidence: 1` and an empty `content`, and `applyResolutions` gains a `case 'mechanical'` that only stages the path — never `writeFile`/`checkoutFile`/`removeFile`, because writing would follow a symlink and corrupt its target or hit `EISDIR` on a submodule. A whole-set guard withholds the class from every entry when the set also holds a stage-1-only entry: that is the signature of a rename/rename(1to2), whose two destinations each look mechanical on their own and would otherwise both be staged, committing the file twice. Reference-integrity detection keeps a mechanical path in scope but out of the result overlay, reading the merged file from the working tree so the parser sees that (not the empty `content`) — on both the with-refs path and the no-refs structural-only path.
|
|
17
|
+
- `UnmergedEntry.stages` and the `UnmergedStages` type — which of index stages 1, 2 and 3 hold a path, and at what mode. Filled by the `unmergedEntries` exec fallback from the `--porcelain=v2` `u` record's mode fields at no extra cost; when a host injects its own `unmergedEntries` op that reports no signature, it is derived for the whole set in a single `git ls-files -u -z` call through `exec`. With neither the signature nor `exec` available, behaviour is exactly as before. The stage signature is what classifies a tree-level conflict, rather than the XY status code, which flips with which side is HEAD.
|
|
18
|
+
- `SkippedEntry` type for `StepResult.skipped` — a skip now carries `entryReason` (the git unmerged reason) and `stages` alongside `reason`, so a tree-level skip is distinguishable from an unexplained one. New typed `reason` value `'submodule'` (a gitlink stage, which has no content to resolve). `reason` stays a plain string, so existing consumers keep compiling.
|
|
19
|
+
- New `SkippedEntry.reason` value `'unmergeable'` — a marker-free unmerged entry whose index holds both stage 2 and stage 3 (real content on each side, not just an auto-merge) and that git did not merge is now a typed skip instead of the generic `no-markers`: either the working-tree content carries a NUL byte, or `git check-attr merge` reports an instruction not to text-merge the path — `unset`, `binary`, or a custom driver's name, as opposed to the values naming git's usual 3-way text merge (`unspecified`, `set`, and the built-in `text` driver). Under a `-merge` or `merge=binary` gitattribute git puts the ours version in the working tree, leaves the entry unmerged and writes no markers anywhere, so content alone can never see it; the attribute is also what tells such an entry apart from a text conflict a person merged by hand and left unstaged, which carries the same stages and no markers and stays `no-markers`. The lookup is one batched `git check-attr -z --stdin merge` per `resolveConflicts` call, made only once a marker-free entry needs it, so an ordinary marker-bearing merge pays no extra git call; with no `exec` on the port, or when that call fails, classification falls back to the NUL-byte test alone. This is the first command the package feeds on standard input, so an `exec` adapter that forwards only `args` and drops its `options` — which the README example used to show — now leaves git waiting on a pipe that never closes; the README example forwards `env`, `stdin` and `signal`, matching what the other packages already document. A consumer can now tell "a side must be chosen here" apart from "the resolver produced nothing and cannot say why".
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- A stage-1-only unmerged entry — the base path of a rename/rename(1to2) — is now resolved with the `deleted` strategy (`content: ''`, `confidence: 1`, a description naming the undecided rename), where it used to fail with an `ENOENT` in `errors` that read as a resolver malfunction. In every case reproduced against real git, both sides had already removed the path from their tree; git holds it unmerged only because the replacement name is undecided (tracked separately as #121), so its own deletion was never the open question. The classification is honoured whatever strategy the caller configured: with both sides gone there is no side left to take, so a rule or `defaultStrategy` of `take-ours`/`take-theirs`/`deleted` collapses to the same deletion instead of reaching a read that cannot succeed.
|
|
24
|
+
- A file rule or `defaultStrategy` of `take-ours`, `take-theirs`, or `deleted` is now honoured on a marker-free unmerged entry (in practice, a binary add/add) instead of being silently dropped into a `no-markers` skip. `extractConflict` used to run ahead of the rule check, so a caller's explicit `{ match: '*.png', strategy: 'take-theirs' }` never reached `checkoutFile` — this is what actually kept a sync aborting on a binary conflict the caller had already told it how to resolve. The new `'unmergeable'` skip reason (above) now applies only when the effective strategy is the default `ai`. A take whose named side has no stage in the index resolves as `deleted` rather than as that take: a one-sided tree-level entry — a directory-rename destination, say — holds only the present side, and `git checkout --ours` on a path with no stage 2 fails outright, which would abort the whole operation. Deleting is the answer `takeOurs`/`takeTheirs` already give a conflict pointed at the side that does not have the file.
|
|
25
|
+
- Reference-integrity detection no longer overlays the empty `content` of a `take-ours`/`take-theirs` produced from a marker-free entry onto the parser's view, which would have shown the parser an emptied file and reported every symbol in it as dropped. Such a take is now sourced from the parent side it names: `applyResolutions` checks that side out only after detection has run, so on a marker-free add/add the working tree still holds ours and a `take-theirs` would otherwise have been checked against the very side it discards. The working tree remains the fallback when that parent is unreadable, and is still the only source on the no-refs path, which has no parent snapshots to prefer. A mechanically resolved symlink is kept out of detection scope entirely, since its content is a path: reading through it would hand the parser whatever the link points at — a file the merge never touched, possibly outside the repository. The two kinds of take are told apart by an empty `content` together with absent `chunks` — neither signal alone is sound, because a whole-file conflict whose chosen side is empty also computes `content: ''`, while a consumer hand-building a resolution for the exported `detect()` can carry real content with no `chunks` at all.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- Prompt builders no longer interpolate a host-supplied value into an XML attribute unescaped. Ref labels, marker side labels and file paths went in raw, and the `related-definitions` path went through the body escaper, which leaves a quote alone. Git accepts `"`, `'`, `<` and `>` in a branch name, so an ordinary repository can carry one: such a value closed the attribute and wrote tags of its own into the prompt. Attribute values now go through an escaper covering `&`, `<`, `>`, `"` and `'`; a value holding none of them renders exactly as before.
|
|
30
|
+
- `parseConflictMarkers` no longer misses a conflict block whose marker run is longer than 7 characters. Git emits longer runs — `conflict-marker-size + 1` for a rename/rename(1to2) (8 by default), and longer still under a `conflict-marker-size` gitattribute — and the parser required a marker to be exactly 7, so a file full of genuine markers parsed as zero and reached the consumer as a `no-markers` skip. Acting on that signal would commit literal `<<<<<<<<` runs. The run length is now per block, taken from the block's opening `<<<` run (minimum 7), and every subsequent marker of that block must match it exactly — so a `==========` divider inside a 7-character block, and the longer run a criss-cross merge nests in the base section, both stay content as before.
|
|
31
|
+
- `unmergedEntries` no longer corrupts a path holding non-ASCII bytes. Its exec fallback ran `git status --porcelain=v2` without `-z`, so git C-quoted such a path and returned the literal `"caf\303\251.txt"`, which no later read or stage could act on. It now runs `-c core.quotepath=false … --porcelain=v2 -z` and splits on NUL, matching what `changedFiles` already did.
|
|
32
|
+
|
|
33
|
+
## [0.3.0] - 2026-09-09
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- `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`.
|
|
38
|
+
- `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.
|
|
39
|
+
- `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.
|
|
40
|
+
- `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).
|
|
41
|
+
- `ParsedFile.supported` — separates "the backend has no grammar for this file type" (a coverage gap, skip it) from "the backend handles this type but the content did not parse" (a finding, report it in `syntaxErrors`). `language: 'unsupported'` previously meant both, so a merged result the parser choked on contributed zero findings and still read as clean.
|
|
42
|
+
- Cross-file fixes in the fix phase (opt-in `StepConfig.fix`). After per-file convergence, a project-tier broken reference (an import/cross-file use whose target stopped resolving) is routed to the refine of its **use-site** file: the finding becomes a fix instruction, and the whole set is re-detected, up to `crossFileMaxIterations` rounds (default 2). The refine is seeded with the **defining file's current definitions** (names, kind, and line — a token-cheap list, never the file's text) so it can recognize a renamed, moved, or re-signatured symbol behind the broken use from the use site alone, even though the refine tool cannot read other files. Each cross-file fix is a single refine pass plus a mandatory per-file re-validation; the final whole-set detection also re-checks per-file for files a cross-file fix touched. **Routing:** a use-site file is routed when it has a write channel — an AI resolution, or an AI-unanimous whole-side take (`take-ours`/`take-theirs`), which relabels to `ai` when the fix edit lands so the edited content is actually written. A deliberate rule/default take (`deliberateTake`) and an emergency `fallback` take are **not** routed — the discard was intentional or a last resort — and an untouched auto-merged file (no resolution to carry the edit) is reported but not auto-fixed yet (needs a patches channel). The definition side, if broken, arrives as its own finding on its own file. Report-only (fix off) is unchanged.
|
|
43
|
+
- `Finding.definitionFile` — for a cross-file `broken-reference`, the file the parser reported as the pre-merge target of the broken use. Present only when the backend fills reference `target`s; absent otherwise (the fix phase then routes the use-site file without the defining-file hint). The cross-file refine reads it to seed the refine with that file's current definitions.
|
|
44
|
+
- `Resolution.deliberateTake` — marks a `take-ours`/`take-theirs` produced by a rule or the default strategy (a deliberate whole-side choice) rather than by an AI resolution that happened to pick one side unanimously. Lets the fix phase route AI-unanimous takes for cross-file fixing while leaving deliberate discards — and emergency `fallback` takes — untouched.
|
|
45
|
+
- `Resolution.fallback` — marks the emergency whole-side take the resolver applies when every other path failed, so an accidental wholesale loss is identified by an explicit flag rather than by `confidence === 0` (a legal model output). It drives the `dropped-side` finding and keeps the emergency take out of cross-file fix routing.
|
|
46
|
+
- Per-file fix loop (opt-in via `StepConfig.fix`, default `false`). When enabled, an AI-resolved (`strategy: 'ai'`) text file that carries error-severity per-file findings is routed back through refine: the findings become fix instructions, the file is re-analyzed after each edit, and the loop repeats until the file is clean, a round makes no progress, or `fixLoopMaxIterations` (default 3) is reached. Whatever it cannot fix stays in the findings, so the verdict reflects the post-fix state. The no-markers invariant holds (a fix that reintroduces markers is discarded). New progress events `fix:started` / `fix:completed`. Report-only (fix off) is unchanged. Deferred for the opt-in fix path: rebase per-step-ref/transient discipline, parser/validator call caching, and a full finding-lifecycle model (double-counting is handled here by fingerprint dedup when folding findings into the report). `fix:completed` is emitted even for a file with no findings to fix (`iterations: 0`, `remainingFindings: 0`), so "the fix loop evaluated this file and it was clean" stays distinguishable from "the fix loop never reached this file". Without `refs` the per-file stage still 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.
|
|
47
|
+
- 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. Detection itself is report-only; fixing is opt-in via the fix phase (above) and off by default. `ParserPort.analyze` is called serially — never for the two parents and the result concurrently — so a single-instance backend (a language server) is never asked to hold three overlays of the same paths at once. `DetectionReport.scope` reports `inScope`, `readable` (parents actually loaded), `incomingDiscovered`, and `truncated`; when nothing in scope could be read, `referenceIntegrity` reports `skipped-unreadable-source` rather than empty findings that would read as clean.
|
|
48
|
+
- 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 the resolver's emergency fallback took one side wholesale and discarded a substantial diff from the other; a deliberate rule/default take is not flagged.
|
|
14
49
|
- `ResolutionContext.detectionRefs` — optional parent tips for reference-integrity detection's before/after comparison, separate from `refs` (which drives the resolver's three-way diff). Detection uses `detectionRefs ?? refs`. Needed when the resolver's refs are the in-flight per-step state (e.g. a rebase) while detection needs the two stable pre-merge branch tips; in a merge they coincide and it can be left unset.
|
|
15
50
|
- Binding-aware broken-reference detection. `ParsedFile` gains an optional `bindings` (names bound at any scope, module-level and nested), and reference-integrity now treats a name as "resolvable before the merge" if a parent resolved it as a reference **or** defined/bound it. This catches a reference broken by removing a nested binding (e.g. a `const` used by a closure elsewhere in the same file): the dangling use was never a resolved reference — because it was locally bound — so the before/after reference diff alone could not see the regression. Backends that only surface top-level `definitions` can omit `bindings`; they simply do not gain the nested-binding case.
|
|
16
|
-
- Operation-level assessment: `buildOperationDigest`/`summarizeOperation` now fold `StepResult.detection` into the rollup. A file with
|
|
51
|
+
- 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. A check counts as complete when it ran, including when some scope files were an unsupported language (they carry no references to break); it is incomplete when the check could not run at all — no parser, no refs, an unreadable parent snapshot, a thrown parser — or when the scope was not fully covered (`scope.truncated`, or an incoming set the git port could not enumerate).
|
|
52
|
+
- `detectFallbackCodeLoss(conflict, strategy)` and the `CodeLossDetection` type (carrying `discardedSide`), exported from the package root.
|
|
53
|
+
- `StepConfig.allowLossyFallback?: boolean` escape hatch.
|
|
54
|
+
- `ConflictError` code `CODE_LOSS_RISK`.
|
|
17
55
|
|
|
18
56
|
### Changed
|
|
19
57
|
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
- **Breaking**:
|
|
58
|
+
- **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.
|
|
59
|
+
- 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'`. 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.
|
|
60
|
+
- **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. Only validator findings are persisted on `Resolution.findings`; parser findings come solely from `detect()`, the authoritative whole-set pass.
|
|
61
|
+
- **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`.
|
|
62
|
+
|
|
63
|
+
### Removed
|
|
64
|
+
|
|
65
|
+
- **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.
|
|
66
|
+
- **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.
|
|
67
|
+
- **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`.
|
|
23
68
|
|
|
24
69
|
### Fixed
|
|
25
70
|
|
|
26
|
-
-
|
|
71
|
+
- 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).
|
|
72
|
+
- A throwing consumer validator no longer destroys a good resolution. The post-refine `validator.validate()` call is now guarded: a validator error is reported via a new `validation:failed` progress event and the resolution is kept intact, instead of bubbling to `resolveOne` and degrading a successful AI resolve into a whole-file take-side fallback (confidence 0) — the exact dropped-side loss the tool guards against. The validator stays advisory.
|
|
73
|
+
- A user abort mid-resolve now cancels the operation instead of degrading into a take-side fallback. `resolveOne` rethrows when `signal.aborted` (covering aborts raised from the resolve loop, refine, or the validator), bypassing the emergency fallback rather than turning a cancellation into a whole-file take-one-side resolution.
|
|
74
|
+
- The refine pass no longer floods its bounded step budget when the model spirals. After a run of consecutive `edit_file_lines` apply errors (a model repeatedly proposing ops that never apply), the pass bails early — keeping whatever edits landed — and emits a new `refine:bailed` progress event, instead of executing failing tool calls until the step budget is exhausted.
|
|
27
75
|
|
|
28
76
|
## [0.2.3] - 2026-07-06
|
|
29
77
|
|