@gitkraken/conflict-tools 0.2.4-beta.20 → 0.2.4-beta.22
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 +35 -8
- package/README.md +23 -11
- package/dist/git/port-dispatch.js +1 -2
- package/dist/index.d.ts +2 -2
- package/dist/ports/model.d.ts +0 -7
- package/dist/ports/parser.d.ts +25 -4
- package/dist/ports/verify.d.ts +16 -8
- package/dist/ports/verify.js +2 -2
- package/dist/reference-integrity/detect.d.ts +22 -4
- package/dist/reference-integrity/detect.js +2 -2
- package/dist/reference-integrity/snapshot.d.ts +8 -2
- package/dist/reference-integrity/snapshot.js +1 -1
- package/dist/resolver/fix-loop.d.ts +8 -5
- package/dist/resolver/fix-loop.js +2 -2
- package/dist/resolver/model-budget.d.ts +6 -0
- package/dist/resolver/model-budget.js +1 -1
- package/dist/resolver/refine-pass.d.ts +4 -0
- package/dist/resolver/refine-pass.js +1 -1
- 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/types/events.d.ts +10 -3
- package/dist/types/findings.d.ts +20 -15
- package/dist/types/resolution.d.ts +5 -0
- package/dist/types/step.d.ts +12 -1
- package/dist/types/summary.d.ts +10 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -9,11 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
9
9
|
|
|
10
10
|
### Added
|
|
11
11
|
|
|
12
|
-
- `
|
|
13
|
-
- `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`.
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
- `
|
|
12
|
+
- `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
|
+
- `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
|
+
- `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).
|
|
15
|
+
- `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.
|
|
16
|
+
- 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). 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. **Limitation:** only use-site files that have an AI resolution are routed — a cross-file break landing on an 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.
|
|
17
|
+
- 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, cross-file fixes, and a full finding-lifecycle model (double-counting is handled here by fingerprint dedup when folding findings into the report).
|
|
18
|
+
- `collectFileFindings` — a per-file detection unit (structural + intra-file broken references + consumer validator issues, all `tier: 'file'`) over already-loaded parent snapshots; the fix loop re-runs it after each edit. It returns the parser and validator halves separately: a parser finding computed over a *single-file* overlay is provisional (a symbol defined in another file is unresolved there by construction), so only the validator half is persisted on `Resolution.findings` and `detect()` remains the authoritative source for the rest.
|
|
17
19
|
- 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.
|
|
18
20
|
- 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.
|
|
19
21
|
- `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.
|
|
@@ -23,19 +25,44 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
23
25
|
### Changed
|
|
24
26
|
|
|
25
27
|
- 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.
|
|
26
|
-
- **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
|
|
28
|
+
- **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.
|
|
27
29
|
- **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`.
|
|
28
|
-
-
|
|
30
|
+
- **Breaking**: the `fix:completed` event field `remaining` is renamed `remainingFindings`. `remaining` collided with `ModelBudget.remaining()` (model calls left) inside the same subsystem, two lines from `fix:budget-exhausted`'s `spent`/`limit`.
|
|
31
|
+
- The per-file fix loop now emits `fix:completed` (iterations 0, remainingFindings 0) even when a file has no findings to fix, so a consumer can distinguish "the fix loop evaluated this file and it was clean" from "the fix loop never reached this file". Previously a clean file returned silently, leaving the two cases indistinguishable in the event stream.
|
|
32
|
+
|
|
33
|
+
### Removed
|
|
34
|
+
|
|
35
|
+
- **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
|
+
- **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
|
+
- **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.
|
|
29
39
|
|
|
30
40
|
### Fixed
|
|
31
41
|
|
|
42
|
+
- 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
|
+
- `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
|
+
- `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.
|
|
45
|
+
- The fix loop can no longer ship a file worse than the one it was handed. A round that landed edits was committed on the strength of the marker check alone, and the no-progress guard compared finding-set *identity*, so a round that grew the error set read as progress — the worse content was kept, the good pre-fix content discarded, and confidence capped as though the file had improved. A round that raises the error count is now discarded and the better content stands.
|
|
46
|
+
- The fix phase isolates failures per file, not per phase. A parser or validator throw on one file propagated to the phase-level catch, skipping every remaining file and forcing both checks to `errored` for the whole operation. Each file's fix is now guarded: on a non-abort throw the pre-fix resolution stands, `fix:failed` carries that `filePath`, and the phase continues.
|
|
47
|
+
- `fix:completed` no longer under-reports `iterations`. The counter lived in the loop's update expression, so every early exit (no edits, markers reintroduced, no progress) skipped counting the round that had just run and reported `iterations: 0` — the value that is supposed to mean "evaluated, spent no model call".
|
|
48
|
+
- `maxModelCalls` is enforced per model call. It was checked only before entering a refine pass, and a pass makes up to `refineMaxSteps` calls, so the cap could be overshot by `refineMaxSteps - 1` per pass. The refine pass now checks the budget before every step.
|
|
49
|
+
- An abort between files no longer discards the partial result when `fix` is on. The resolve loop breaks on abort so the caller keeps what was computed; the fix phase then received the dead signal, handed it to the parser, and rethrew — throwing that work away, while report-only mode returned partials in the identical scenario.
|
|
50
|
+
- Per-file parser findings no longer reach the report unreconciled. `collectFileFindings` analyzes a *single-file* overlay, where a symbol defined in another file cannot resolve by construction, so an ordinary "extract to module" merge produced a `broken-reference` the authoritative whole-set pass did not — burning a model call trying to fix it and flipping the verdict to `blocked`. Only validator findings are persisted per resolution now; parser findings come solely from `detect()`.
|
|
51
|
+
- Detection no longer swallows aborts. `detect()`'s catch and the snapshot reads reported a cancelled operation as `errored` (or, on a partial abort, as an ordinary result over a truncated before/after picture, which could reach `clean`). All three now rethrow when the signal is aborted, matching the discipline the resolve path already follows.
|
|
52
|
+
- `detect()` no longer analyzes the two parents and the result concurrently. The three overlays cover the same paths with different content, which is exactly the race `collectFileFindings` serializes to avoid on a single-instance language-server backend. The calls are serialized and the guarantee is now stated on `ParserPort.analyze`.
|
|
53
|
+
- The `changedFiles` exec fallback no longer loses paths to git's quoting. With the default `core.quotepath`, any path holding non-ASCII characters came back C-quoted and wrapped in quotes; that literal became a scope key no `showFile` could read, so the file silently dropped out of analysis while still counting as in scope. The fallback now runs with `core.quotepath=false` and `-z`, splitting on NUL, and passes `--end-of-options` so a ref beginning with `-` cannot be read as a flag.
|
|
54
|
+
- `dropped-side` no longer keys on `confidence === 0`. That was a convention set 200 lines away in the fallback path and absent from the type system, while confidence 0 is a legal model output — so an AI resolution that took one side and honestly reported no confidence was flagged as an accidental wholesale loss. `Resolution.fallback` now marks the emergency take explicitly.
|
|
55
|
+
- 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
|
+
- 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
|
+
- `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
|
+
|
|
32
59
|
- 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.
|
|
33
60
|
- `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.
|
|
34
61
|
- 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).
|
|
35
62
|
- 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.
|
|
36
63
|
- 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.
|
|
37
64
|
- 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.
|
|
38
|
-
- Reference-integrity detection no longer reads as "clean" when the parent snapshots could not be loaded. A git port that returns empty content for an unreadable parent (e.g. a `showFile` that swallows read errors into `""` instead of throwing) previously produced empty findings with `referenceIntegrity: 'ran'`, masking a total read failure as a clean result. `buildSnapshots` now ignores empty parent content; `DetectionReport.scope` reports `readable` (parents actually loaded) alongside `
|
|
65
|
+
- Reference-integrity detection no longer reads as "clean" when the parent snapshots could not be loaded. A git port that returns empty content for an unreadable parent (e.g. a `showFile` that swallows read errors into `""` instead of throwing) previously produced empty findings with `referenceIntegrity: 'ran'`, masking a total read failure as a clean result. `buildSnapshots` now ignores empty parent content; `DetectionReport.scope` reports `readable` (parents actually loaded) alongside `inScope` (files in scope; renamed from `analyzed`, which described a count taken before anything was parsed); and `referenceIntegrity` reports a new `'skipped-unreadable-source'` status when nothing in scope could be read — which flips `OperationDigest.detectionComplete` to `false` and the verdict to `needs-review` instead of `clean`.
|
|
39
66
|
|
|
40
67
|
## [0.2.3] - 2026-07-06
|
|
41
68
|
|
package/README.md
CHANGED
|
@@ -102,14 +102,16 @@ const parser: ParserPort = {
|
|
|
102
102
|
Three 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
|
-
- **Language-agnostic.** A file the backend
|
|
106
|
-
- **Precision-
|
|
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.
|
|
106
|
+
- **Precision-declaring.** A tree-sitter backend and a language-server backend differ in how completely they resolve references, and the port asks them to say so via `capabilities`. A backend without `crossFileResolution` reports every import as unresolved on both parents *and* the result, so no cross-file break can ever surface — detection reports `partial-no-cross-file-resolution` instead of letting that empty result pass as a clean cross-file check.
|
|
107
|
+
- **Never called concurrently.** The package awaits each `analyze` before issuing the next, so a single-instance backend is never asked to hold two versions of the same path at once. Implementations need no internal locking.
|
|
107
108
|
|
|
108
109
|
`ParsedFile`:
|
|
109
110
|
|
|
110
111
|
| Field | Meaning |
|
|
111
112
|
|---|---|
|
|
112
|
-
| `
|
|
113
|
+
| `supported` | `false` when the backend has no grammar for this file type at all — the detector skips it |
|
|
114
|
+
| `language` | Backend's language id; meaningful only when `supported` |
|
|
113
115
|
| `definitions` | `SymbolDefinition[]` — top-level defs (`name`, `kind?`, 1-indexed `line`) |
|
|
114
116
|
| `references` | `SymbolReference[]` — `name`, `line`, `column?`, `resolved` (did it resolve within the overlay), `target?` |
|
|
115
117
|
| `bindings?` | Optional names bound at *any* scope (module-level and nested). Populating it lets detection catch a reference broken by removing a nested binding. A backend that only surfaces top-level `definitions` may omit it |
|
|
@@ -119,7 +121,9 @@ Pass via `deps.parser`. When absent, detection is skipped and the report says so
|
|
|
119
121
|
|
|
120
122
|
### `Validator` (optional)
|
|
121
123
|
|
|
122
|
-
The consumer's validation port. Plug in any checks — syntax, real import resolution, types, lint, project rules. It runs **post-refine over an overlay**
|
|
124
|
+
The consumer's validation port. Plug in any checks — syntax, real import resolution, types, lint, project rules. It runs **post-refine over an overlay** and returns `ValidatorFinding[]`.
|
|
125
|
+
|
|
126
|
+
The overlay is currently **always a single file**: the package validates one file at a time, right after that file's refine. `SourceOverlay` is the parameter type so a whole-set call can be added later without a breaking change, but no such call site exists yet — do not write a validator that only works across files.
|
|
123
127
|
|
|
124
128
|
```typescript
|
|
125
129
|
import type { Validator, ValidatorFinding } from '@gitkraken/conflict-tools';
|
|
@@ -258,7 +262,9 @@ const step = await resolveConflicts(
|
|
|
258
262
|
// step.resolutions[] — successful resolutions
|
|
259
263
|
// step.errors[] — { filePath, error } for files that failed (no fallback or AI exhausted)
|
|
260
264
|
// step.skipped?[] — { filePath, reason } for files that were excluded or had no markers
|
|
261
|
-
// step.detection
|
|
265
|
+
// step.detection — DetectionReport over the whole touched set; ALWAYS present. Read `checks`
|
|
266
|
+
// (or digest.detectionComplete) to tell whether detection actually ran —
|
|
267
|
+
// without a parser it is a present, empty report with `skipped-no-parser`.
|
|
262
268
|
|
|
263
269
|
// Gate on the verdict, not on raw confidence — see below.
|
|
264
270
|
const { digest } = await summarizeOperation(step);
|
|
@@ -272,6 +278,7 @@ if (digest.verdict === 'clean') {
|
|
|
272
278
|
|
|
273
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.
|
|
274
280
|
- `defaultStrategy` — strategy for files not matching any rule. Defaults to `'ai'`.
|
|
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.
|
|
275
282
|
- `fallbackStrategy` — when AI fails for a file, fall back to `'take-ours'` or `'take-theirs'` instead of recording an error.
|
|
276
283
|
|
|
277
284
|
Available strategies for rules and `defaultStrategy`:
|
|
@@ -301,7 +308,7 @@ const report = await detect(step.resolutions, { refs }, { git, parser });
|
|
|
301
308
|
`DetectDeps` is `{ git; parser?; signal? }`. It flags:
|
|
302
309
|
|
|
303
310
|
- references that resolved in a pre-merge parent (ours/theirs) but no longer resolve in the merged result (`broken-reference`, with `provenance`),
|
|
304
|
-
- duplicate definitions and syntax errors
|
|
311
|
+
- 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,
|
|
305
312
|
- the resolver's emergency fallback taking one side wholesale and discarding a substantial diff (`dropped-side`).
|
|
306
313
|
|
|
307
314
|
The scope includes the resolved files *and* the merge's auto-merged/incoming set (discovered from `refs` via `changedFiles`/`mergeBase`, or overridden by `context.touchedFiles`), so a dangling reference in a cleanly-merged file is still caught.
|
|
@@ -313,7 +320,7 @@ interface DetectionReport {
|
|
|
313
320
|
findings: Finding[];
|
|
314
321
|
checks: { referenceIntegrity: CheckStatus; structural: CheckStatus };
|
|
315
322
|
/** Honest coverage — present only when the reference check ran. */
|
|
316
|
-
scope?: {
|
|
323
|
+
scope?: { inScope: number; readable: number; truncated: boolean; incomingDiscovered: boolean };
|
|
317
324
|
}
|
|
318
325
|
```
|
|
319
326
|
|
|
@@ -345,7 +352,8 @@ interface DetectionReport {
|
|
|
345
352
|
| `skipped-no-parser` | No `ParserPort` supplied |
|
|
346
353
|
| `skipped-no-refs` | No refs, so the before/after comparison was impossible (structural checks still ran) |
|
|
347
354
|
| `skipped-unreadable-source` | Refs were present but the parent snapshots came back empty (unreadable source) |
|
|
348
|
-
| `partial-unsupported-language` | The check ran, but some files in scope were
|
|
355
|
+
| `partial-unsupported-language` | The check ran, but some files in scope were a file type the backend has no grammar for |
|
|
356
|
+
| `partial-no-cross-file-resolution` | The backend declared it does not resolve references across files, so cross-file breaks were structurally out of reach |
|
|
349
357
|
| `errored` | The parser was present but threw |
|
|
350
358
|
|
|
351
359
|
### `summarizeOperation` / `buildOperationDigest`
|
|
@@ -383,9 +391,13 @@ When enabled:
|
|
|
383
391
|
- **Per-file convergence** — an AI-resolved (`strategy: 'ai'`) text file carrying error-severity per-file findings is routed back through refine; the file is re-analyzed after each edit and the loop repeats until it is clean, a round makes no progress, or `fixLoopMaxIterations` (default 3) is reached.
|
|
384
392
|
- **Cross-file routing** — a `tier: 'project'` broken reference (an import/cross-file use whose target stopped resolving) is routed to the refine of its *use-site* file, and the whole set is re-detected, up to `crossFileMaxIterations` (default 2) rounds. Only use-site files that have an AI resolution are routed; a break landing on an auto-merged file (no resolution to carry the edit) is reported but not auto-fixed.
|
|
385
393
|
|
|
386
|
-
Whatever the fix phase cannot fix stays in the findings, so the verdict always reflects the post-fix state.
|
|
394
|
+
Whatever the fix phase cannot fix stays in the findings, so the verdict always reflects the post-fix state. Two invariants hold throughout: a fix that reintroduces conflict markers is discarded rather than written, and a round that leaves *more* error findings than it started with is discarded too — the better content is kept, so the phase can never ship a file worse than the one it was handed.
|
|
395
|
+
|
|
396
|
+
Failures are isolated per file: a parser, validator, or model throw on one file leaves that file at its pre-fix resolution, emits `fix:failed` with its `filePath`, and the phase continues with the rest. Only a user abort propagates.
|
|
397
|
+
|
|
398
|
+
Without `ResolutionContext.refs` (or `detectionRefs`) the per-file stage still runs on what needs no parent content — syntax errors and validator findings — while cross-file routing is skipped, since it is driven by the whole-set before/after report.
|
|
387
399
|
|
|
388
|
-
`
|
|
400
|
+
`StepConfig` fix-phase fields:
|
|
389
401
|
|
|
390
402
|
| Field | Default | Effect |
|
|
391
403
|
|---|---|---|
|
|
@@ -483,7 +495,7 @@ score(resolution, goldenFile);
|
|
|
483
495
|
| `refine:discarded` | A refine/fix edit batch was rejected because it reintroduced conflict markers (`reason`) |
|
|
484
496
|
| `refine:touched-user-region` | Refine modified a user-resolved marker region (`markerIndexes`) |
|
|
485
497
|
| `fix:started` | The fix loop began fixing a file (`findingCount`) |
|
|
486
|
-
| `fix:completed` | The fix loop finished evaluating a file (`iterations`, `
|
|
498
|
+
| `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" |
|
|
487
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` |
|
|
488
500
|
| `fix:budget-exhausted` | The global `maxModelCalls` budget was spent (`spent`, `limit`); the fix phase stopped and left the remainder in findings |
|
|
489
501
|
| `validation:failed` | The consumer validator threw; reported and swallowed, resolution kept (`reason`) |
|
|
@@ -1,3 +1,2 @@
|
|
|
1
1
|
import{ConflictGitPortMissingOpError as e}from"../ports/git.js";import{sliceLines as t,truncateLines as n,truncateMatches as r}from"./truncation.js";const i={DD:`both-deleted`,AU:`added-by-us`,UD:`deleted-by-them`,UA:`added-by-them`,DU:`deleted-by-us`,AA:`both-added`,UU:`both-modified`};function a(e){return i[e]??e}function o(e){return`Search capped at ${e} matches. There may be more — narrow with a more specific pattern or path.`}function s(t,n){throw new e(t,n)}function c(e,t){return e!=null&&t!=null?`startLine=${e}, endLine=${t}`:e==null?`endLine=${t}`:`startLine=${e}`}async function l(e,t,n){return e.readFile?e.readFile(t,n):s(`readFile`)}async function u(e,r,i,a){let o;if(e.showFile)o=await e.showFile(r,i,a);else if(e.exec){let n=t(await e.exec([`show`,`${r}:${i}`],{signal:a?.signal}),a?.startLine,a?.endLine);if(!n.ok)return n.reason===`invalid-range`?`[Invalid range: ${c(n.startLine,n.endLine)} (startLine must be <= endLine).]`:`[Range out of bounds: file has ${n.total} lines, requested ${c(n.startLine,n.endLine)}.]`;o=n.content}else return s(`showFile`);return n(o,1e3,`Output capped at 1000 lines. Use startLine/endLine to read other regions of the file.`)}async function d(e,t){if(e.unmergedEntries)return e.unmergedEntries(t);if(e.exec){let n=await e.exec([`status`,`--porcelain=v2`],{signal:t?.signal}),r=[];for(let e of n.split(`
|
|
2
|
-
`)){if(!e.startsWith(`u `))continue;let t=e.split(` `),n=t[1]??``,i=t.slice(10).join(` `);r.push({path:i,reason:a(n)})}return r}return s(`unmergedEntries`)}async function f(e,t,n,r){return e.mergeBase?e.mergeBase(t,n,r):e.exec?(await e.exec([`merge-base`,`--`,t,n],{signal:r?.signal})).trim():s(`mergeBase`)}async function p(e,t,r){let i=r?.startLine,a=r?.endLine,o=Number.isFinite(i),l=Number.isFinite(a);if(o&&l&&i>a)return`[Invalid range: ${c(i,a)} (startLine must be <= endLine).]`;let u;if(e.blame)u=await e.blame(t,r);else if(e.exec){let n=[`blame`,`--porcelain`],s=o?Math.max(1,i):void 0,c=l?Math.max(1,a):void 0;s!=null&&c!=null?n.push(`-L`,`${s},${c}`):s==null?c!=null&&n.push(`-L`,`1,${c}`):n.push(`-L`,`${s},`),r?.ref&&n.push(r.ref),n.push(`--`,t),u=await e.exec(n,{signal:r?.signal})}else return s(`blame`);return n(u,1e3,`Output capped at 1000 lines. Use startLine/endLine to inspect other regions.`)}async function m(e,t,n){let i=n?.maxResults,a=i!=null&&Number.isFinite(i)?Math.min(Math.max(1,i),100):100,c;if(e.grep)c=await e.grep(t,n);else if(e.exec){let r=[`grep`,`-n`,`--`,t];n?.ref&&r.push(n.ref),c=await e.exec(r,{signal:n?.signal})}else return s(`grep`);return r(c,a,o(a))}async function h(e,t,n,r){if(e.diff)return e.diff(t,n,r);if(e.exec){let i=[`diff`,t,n];return r?.path&&i.push(`--`,r.path),e.exec(i,{signal:r?.signal})}return s(`diff`)}async function g(e,t,n,r){return e.changedFiles?e.changedFiles(t,n,r):e.exec?(await e.exec([
|
|
3
|
-
`).map(e=>e.trim()).filter(e=>e.length>0):s(`changedFiles`)}async function _(e,t){let r=t?.maxCount,i=r!=null&&Number.isFinite(r)?Math.min(Math.max(1,r),100):100,a;if(e.log)a=await e.log({...t,maxCount:i});else if(e.exec){let n=[`log`,`--oneline`,`-n`,String(i)];t?.ref&&n.push(t.ref),t?.path&&n.push(`--`,t.path),a=await e.exec(n,{signal:t?.signal})}else return s(`log`);return n(a,100,`Output capped at 100 commits. Use path to scope to a file or refine the ref range.`)}async function v(e,t,r){let i;if(e.show)i=await e.show(t,r);else if(e.exec){let n=[`show`,t];r?.path&&n.push(`--`,r.path),i=await e.exec(n,{signal:r?.signal})}else return s(`show`);return n(i,500,`Output capped at 500 lines. Use show_file_at_ref with startLine/endLine to read specific regions.`)}async function y(e,t,n,r){return e.writeFile?e.writeFile(t,n,r):s(`writeFile`)}async function b(e,t,n){if(e.stageFiles)return e.stageFiles(t,n);if(e.exec){await e.exec([`add`,`--`,...t],{signal:n?.signal});return}return s(`stageFiles`)}async function x(e,t,n,r){if(e.checkoutFile)return e.checkoutFile(t,n,r);if(e.exec){await e.exec([`checkout`,`--${n}`,`--`,t],{signal:r?.signal});return}return s(`checkoutFile`)}async function S(e,t,n){if(e.removeFile)return e.removeFile(t,n);if(e.exec){await e.exec([`rm`,`--`,t],{signal:n?.signal});return}return s(`removeFile`)}export{p as blame,g as changedFiles,x as checkoutFile,h as diff,m as grep,_ as log,f as mergeBase,l as readFile,S as removeFile,v as show,u as showFile,b as stageFiles,d as unmergedEntries,y as writeFile};
|
|
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
|
@@ -9,7 +9,7 @@ import { extractConflict } from "./extraction/extract.js";
|
|
|
9
9
|
import { checkoutFile, removeFile, show } from "./git/port-dispatch.js";
|
|
10
10
|
import { ToolCall, ToolDefinition, ToolResult } from "./types/tool.js";
|
|
11
11
|
import { ConflictModelMessage, ConflictModelParams, ConflictModelPort, ConflictModelResult, ResolverConfig } from "./ports/model.js";
|
|
12
|
-
import { ParsedFile, ParserPort, SourceOverlay, SymbolDefinition, SymbolReference, SyntaxIssue } from "./ports/parser.js";
|
|
12
|
+
import { ParsedFile, ParserCapabilities, ParserPort, SourceOverlay, SymbolDefinition, SymbolReference, SyntaxIssue } from "./ports/parser.js";
|
|
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";
|
|
@@ -21,4 +21,4 @@ import { FileRule, StepConfig, StepResult } from "./types/step.js";
|
|
|
21
21
|
import { resolveConflicts } from "./resolver/resolve-conflicts.js";
|
|
22
22
|
import { OperationDigest, OperationSummary, OperationVerdict, ReviewItem } from "./types/summary.js";
|
|
23
23
|
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 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 };
|
|
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 };
|
package/dist/ports/model.d.ts
CHANGED
|
@@ -35,13 +35,6 @@ interface ResolverConfig {
|
|
|
35
35
|
maxSteps?: number;
|
|
36
36
|
temperature?: number;
|
|
37
37
|
refineMaxSteps?: number;
|
|
38
|
-
/** Global cap on `model.generate` calls for the whole operation (resolve + refine + fix). Default
|
|
39
|
-
* `undefined` = unlimited. Primarily a backstop for the fix phase's multiplicative worst case
|
|
40
|
-
* (files × fixLoopMaxIterations + crossFileMaxIterations × routable calls). When spent, the fix
|
|
41
|
-
* phase stops attempting further fixes and leaves the remainder in findings — it never aborts the
|
|
42
|
-
* operation. The resolve loop is not gated by this (it is already bounded by `maxSteps`); its calls
|
|
43
|
-
* still count toward the cap. */
|
|
44
|
-
maxModelCalls?: number;
|
|
45
38
|
}
|
|
46
39
|
//#endregion
|
|
47
40
|
export { ConflictModelMessage, ConflictModelParams, ConflictModelPort, ConflictModelResult, ResolverConfig };
|
package/dist/ports/parser.d.ts
CHANGED
|
@@ -26,9 +26,15 @@ interface SyntaxIssue {
|
|
|
26
26
|
line: number;
|
|
27
27
|
message: string;
|
|
28
28
|
}
|
|
29
|
-
/** Per-file parse result.
|
|
30
|
-
* detector then skips it rather than emitting false findings. */
|
|
29
|
+
/** Per-file parse result. */
|
|
31
30
|
interface ParsedFile {
|
|
31
|
+
/** `false` when the backend has no grammar for this file type at all (a lockfile, an image, a
|
|
32
|
+
* language it does not implement). The detector skips the file and reports reduced coverage rather
|
|
33
|
+
* than emitting false findings. A file the backend *does* handle but whose content failed to parse
|
|
34
|
+
* is `supported: true` with the failure in `syntaxErrors` — that is a finding, not a coverage gap,
|
|
35
|
+
* and conflating the two lets a broken merge result read as clean. */
|
|
36
|
+
supported: boolean;
|
|
37
|
+
/** Backend's language id. Free-form; only meaningful when `supported`. */
|
|
32
38
|
language: string;
|
|
33
39
|
definitions: SymbolDefinition[];
|
|
34
40
|
references: SymbolReference[];
|
|
@@ -40,18 +46,33 @@ interface ParsedFile {
|
|
|
40
46
|
bindings?: SymbolDefinition[];
|
|
41
47
|
syntaxErrors: SyntaxIssue[];
|
|
42
48
|
}
|
|
49
|
+
/** What a backend can actually do, so the detector reports the coverage it really achieved. */
|
|
50
|
+
interface ParserCapabilities {
|
|
51
|
+
/** Whether the backend resolves references across files (imports and their targets). A backend that
|
|
52
|
+
* cannot — a plain tree-sitter parser, which sees one file at a time — reports every cross-file use
|
|
53
|
+
* as unresolved on both parents *and* the result, so no before/after transition exists and no
|
|
54
|
+
* cross-file break can ever be found. Declaring `false` makes that limit visible in the report
|
|
55
|
+
* instead of letting an empty findings list read as a clean cross-file check. */
|
|
56
|
+
crossFileResolution: boolean;
|
|
57
|
+
}
|
|
43
58
|
/**
|
|
44
59
|
* Parsing / symbol-resolution capability supplied per surface — merge-mate via tree-sitter,
|
|
45
60
|
* GitLens/GKD via a language server. The detector stays free of any parser dependency; the port
|
|
46
|
-
* abstracts the precision difference between backends.
|
|
61
|
+
* abstracts the precision difference between backends and asks them to declare it.
|
|
47
62
|
*/
|
|
48
63
|
interface ParserPort {
|
|
64
|
+
/** What this backend can resolve. Omitted means the conservative reading: no cross-file resolution. */
|
|
65
|
+
capabilities?: ParserCapabilities;
|
|
49
66
|
/**
|
|
50
67
|
* Parse and resolve every file in `sources`, treating those contents as authoritative for their
|
|
51
68
|
* paths. Returns one {@link ParsedFile} per input path. Works only from the provided content — it
|
|
52
69
|
* never reads the filesystem itself. `signal`, when provided, should abort an in-flight analysis.
|
|
70
|
+
*
|
|
71
|
+
* Calls are never overlapped: the package awaits each `analyze` before issuing the next, so a
|
|
72
|
+
* single-instance backend (a language server holding one document per path) is not asked to hold
|
|
73
|
+
* two versions of the same path at once. Implementations need no internal locking.
|
|
53
74
|
*/
|
|
54
75
|
analyze(sources: SourceOverlay, signal?: AbortSignal): Promise<ReadonlyMap<string, ParsedFile>>;
|
|
55
76
|
}
|
|
56
77
|
//#endregion
|
|
57
|
-
export { ParsedFile, ParserPort, SourceOverlay, SymbolDefinition, SymbolReference, SyntaxIssue };
|
|
78
|
+
export { ParsedFile, ParserCapabilities, ParserPort, SourceOverlay, SymbolDefinition, SymbolReference, SyntaxIssue };
|
package/dist/ports/verify.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Finding, FindingCategory } from "../types/findings.js";
|
|
1
2
|
import { SourceOverlay } from "./parser.js";
|
|
2
3
|
import { Issue } from "../types/issue.js";
|
|
3
4
|
|
|
@@ -7,8 +8,9 @@ interface VerificationResult {
|
|
|
7
8
|
issues?: Issue[];
|
|
8
9
|
}
|
|
9
10
|
interface ValidatorFinding {
|
|
10
|
-
/**
|
|
11
|
-
|
|
11
|
+
/** Consumer's own category (e.g. `'type-error'`, `'lint:no-console'`). The package's own
|
|
12
|
+
* {@link FindingCategory} values are accepted too, and kept discoverable, but nothing requires them. */
|
|
13
|
+
category: FindingCategory | (string & {});
|
|
12
14
|
severity: 'error' | 'warning';
|
|
13
15
|
file: string;
|
|
14
16
|
/** 1-indexed. */
|
|
@@ -16,15 +18,21 @@ interface ValidatorFinding {
|
|
|
16
18
|
message: string;
|
|
17
19
|
}
|
|
18
20
|
/**
|
|
19
|
-
* The consumer's validation port
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
21
|
+
* The consumer's validation port. Plug in any checks — syntax, real import resolution, types, lint,
|
|
22
|
+
* project rules. **Non-blocking**: it runs post-refine and its findings are advisory (folded into the
|
|
23
|
+
* operation's findings and verdict), never a re-resolve. The only blocking gate is residual conflict
|
|
24
|
+
* markers, which the resolver checks built-in and language-agnostic — that is not this port's job.
|
|
25
|
+
*
|
|
26
|
+
* `sources` is currently always a single-file overlay: the package validates one file at a time, after
|
|
27
|
+
* that file's refine. The parameter is an overlay so a whole-set call can be added without a breaking
|
|
28
|
+
* change, but no such call site exists yet — do not write a validator that only works across files.
|
|
24
29
|
*/
|
|
25
30
|
interface Validator {
|
|
26
31
|
validate(sources: SourceOverlay, signal?: AbortSignal): Promise<ValidatorFinding[]>;
|
|
27
32
|
}
|
|
33
|
+
/** Lifts a consumer finding into the package's `Finding` shape. Validators run per file, so everything
|
|
34
|
+
* they report is `tier: 'file'`. */
|
|
35
|
+
declare function toFileFinding(finding: ValidatorFinding): Finding;
|
|
28
36
|
declare function checkResidualMarkers(_filePath: string, content: string): VerificationResult;
|
|
29
37
|
//#endregion
|
|
30
|
-
export { Validator, ValidatorFinding, VerificationResult, checkResidualMarkers };
|
|
38
|
+
export { Validator, ValidatorFinding, VerificationResult, checkResidualMarkers, toFileFinding };
|
package/dist/ports/verify.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const
|
|
2
|
-
`),i=[];for(let
|
|
1
|
+
function e(e){return{category:e.category,severity:e.severity,tier:`file`,location:{file:e.file,line:e.line??1},message:e.message}}const t=[/^<{7}(\s|$)/,/^\|{7}(\s|$)/,/^={7}(\s|$)/,/^>{7}(\s|$)/];function n(e,n){let r=n.split(`
|
|
2
|
+
`),i=[];for(let e=0;e<r.length;e++){let n=r[e]??``;for(let r of t)if(r.test(n)){i.push(e+1);break}}return i.length===0?{valid:!0}:{valid:!1,issues:[{severity:`error`,message:`Residual conflict markers found at line(s) ${i.join(`, `)}.`}]}}export{n as checkResidualMarkers,e as toFileFinding};
|
|
@@ -10,12 +10,21 @@ interface DetectDeps {
|
|
|
10
10
|
/** Parsing/symbol-resolution capability. When absent, detection is skipped and reported as such. */
|
|
11
11
|
parser?: ParserPort;
|
|
12
12
|
signal?: AbortSignal;
|
|
13
|
+
/** Paths to keep out of scope — the caller's skipped and failed files, which still carry conflict
|
|
14
|
+
* markers on disk. See {@link import('./snapshot.js').SnapshotDeps.excludePaths}. */
|
|
15
|
+
excludePaths?: ReadonlySet<string>;
|
|
16
|
+
}
|
|
17
|
+
/** Parent contents for one file, named so the two sides cannot be transposed at a call site. */
|
|
18
|
+
interface ParentContents {
|
|
19
|
+
ours?: string;
|
|
20
|
+
theirs?: string;
|
|
13
21
|
}
|
|
14
22
|
/**
|
|
15
23
|
* Deterministic post-resolution detection: flags references that resolved in a pre-merge parent
|
|
16
24
|
* (ours/theirs) but no longer resolve in the merged result, plus duplicate definitions and syntax
|
|
17
|
-
* errors
|
|
18
|
-
*
|
|
25
|
+
* errors across the analyzed scope (the resolved files and the merge's auto-merged/incoming set).
|
|
26
|
+
* Report-only — it never mutates state or fails the operation. When no parser is supplied, or refs are
|
|
27
|
+
* missing, the report says so instead of implying a clean result.
|
|
19
28
|
*/
|
|
20
29
|
declare function detect(resolutions: Resolution[], context: ResolutionContext, deps: DetectDeps): Promise<DetectionReport>;
|
|
21
30
|
interface FileFindingsDeps {
|
|
@@ -23,6 +32,15 @@ interface FileFindingsDeps {
|
|
|
23
32
|
validator?: Validator;
|
|
24
33
|
signal?: AbortSignal;
|
|
25
34
|
}
|
|
35
|
+
/** Per-file findings split by who produced them. They are kept apart because only the validator half is
|
|
36
|
+
* the file's own to keep: parser findings computed over a *single-file* overlay are provisional — a
|
|
37
|
+
* symbol defined in another file is unresolved there by construction — so the authoritative version is
|
|
38
|
+
* whatever `detect()` says over the whole set. The fix loop uses both to converge and persists only
|
|
39
|
+
* `validator`. */
|
|
40
|
+
interface FileFindings {
|
|
41
|
+
parser: Finding[];
|
|
42
|
+
validator: Finding[];
|
|
43
|
+
}
|
|
26
44
|
/**
|
|
27
45
|
* Per-file findings for one file's current content, compared against its pre-merge parents: structural
|
|
28
46
|
* (syntax/duplicate) + intra-file broken references (a local definition/binding the merge dropped while
|
|
@@ -31,6 +49,6 @@ interface FileFindingsDeps {
|
|
|
31
49
|
* authoritative whole-set report. This is the lightweight unit the per-file fix loop re-runs after each
|
|
32
50
|
* edit, over already-loaded parent snapshots (no git reads, no incoming-set discovery).
|
|
33
51
|
*/
|
|
34
|
-
declare function collectFileFindings(file: string,
|
|
52
|
+
declare function collectFileFindings(file: string, parents: ParentContents, resultContent: string, deps: FileFindingsDeps): Promise<FileFindings>;
|
|
35
53
|
//#endregion
|
|
36
|
-
export { DetectDeps, FileFindingsDeps, collectFileFindings, detect };
|
|
54
|
+
export { DetectDeps, FileFindings, FileFindingsDeps, ParentContents, collectFileFindings, detect };
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{
|
|
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(`
|
|
2
2
|
`)),r=0;for(let t of e.split(`
|
|
3
|
-
`))t.trim()!==``&&!n.has(t)&&r++;return r}function
|
|
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};
|
|
@@ -7,17 +7,23 @@ interface DetectionSnapshots {
|
|
|
7
7
|
ours: SourceOverlay;
|
|
8
8
|
theirs: SourceOverlay;
|
|
9
9
|
result: SourceOverlay;
|
|
10
|
-
/** Number of files
|
|
11
|
-
|
|
10
|
+
/** Number of files taken into scope (after the cap). */
|
|
11
|
+
inScope: number;
|
|
12
12
|
/** How many scope files had loadable parent content on `ours` or `theirs`. When this is 0 the
|
|
13
13
|
* before/after comparison had nothing to read — a total read failure, not a clean result. */
|
|
14
14
|
readable: number;
|
|
15
15
|
/** Whether the scope exceeded the cap and was truncated. */
|
|
16
16
|
truncated: boolean;
|
|
17
|
+
/** Whether the incoming/auto-merged set could be discovered (or was supplied by the consumer). */
|
|
18
|
+
incomingDiscovered: boolean;
|
|
17
19
|
}
|
|
18
20
|
interface SnapshotDeps {
|
|
19
21
|
git: ConflictGitPort;
|
|
20
22
|
signal?: AbortSignal;
|
|
23
|
+
/** Paths to keep out of scope entirely. The caller passes the files it skipped or failed on: those
|
|
24
|
+
* still carry conflict markers in the working tree, so parsing them would report the marker text as
|
|
25
|
+
* syntax errors and blame the merge for a file nobody resolved. */
|
|
26
|
+
excludePaths?: ReadonlySet<string>;
|
|
21
27
|
}
|
|
22
28
|
/**
|
|
23
29
|
* Builds the file-content overlays the detector compares: both pre-merge parents (read from git
|
|
@@ -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
|
|
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};
|
|
@@ -4,6 +4,7 @@ import { Resolution, ResolutionContext } from "../types/resolution.js";
|
|
|
4
4
|
import { ConflictModelPort } from "../ports/model.js";
|
|
5
5
|
import { ParserPort } from "../ports/parser.js";
|
|
6
6
|
import { Validator } from "../ports/verify.js";
|
|
7
|
+
import { ParentContents } from "../reference-integrity/detect.js";
|
|
7
8
|
import { ConflictProgressEvent } from "../types/events.js";
|
|
8
9
|
import { StepConfig } from "../types/step.js";
|
|
9
10
|
import { ModelBudget } from "./model-budget.js";
|
|
@@ -19,21 +20,21 @@ interface FixLoopDeps {
|
|
|
19
20
|
/** Global model-call budget for the operation. When exhausted, the loop stops before any further
|
|
20
21
|
* model call and leaves the remaining findings unfixed. Absent = unlimited. */
|
|
21
22
|
budget?: ModelBudget;
|
|
22
|
-
/** Invoked
|
|
23
|
-
*
|
|
23
|
+
/** Invoked when the budget stops work. Deduped by {@link ModelBudget}, so a caller may wire this
|
|
24
|
+
* straight to a progress emit. */
|
|
24
25
|
onBudgetExhausted?: () => void;
|
|
25
26
|
}
|
|
26
27
|
/**
|
|
27
28
|
* Per-file convergence loop (opt-in `config.fix`). Re-detects the file's per-file findings, feeds the
|
|
28
29
|
* error-severity ones back through refine as fix instructions, and repeats until the file is clean, a
|
|
29
|
-
* round makes no progress, or the iteration budget runs out. What it cannot fix
|
|
30
|
-
*
|
|
30
|
+
* round makes no progress or makes things worse, or the iteration budget runs out. What it cannot fix
|
|
31
|
+
* stays in the report — the verdict is computed from that remainder, never from a stale pre-fix set.
|
|
31
32
|
*
|
|
32
33
|
* Only intra-file findings are addressed here (from {@link collectFileFindings}); cross-file references
|
|
33
34
|
* are a separate set-level stage. `dropped-side` is not produced per-file and so is never routed. The
|
|
34
35
|
* caller should invoke this only for AI-resolved (`strategy: 'ai'`) text files.
|
|
35
36
|
*/
|
|
36
|
-
declare function runFixLoop(resolution: Resolution,
|
|
37
|
+
declare function runFixLoop(resolution: Resolution, parents: ParentContents, deps: FixLoopDeps): Promise<Resolution>;
|
|
37
38
|
interface FixPhaseDeps {
|
|
38
39
|
model: ConflictModelPort;
|
|
39
40
|
git: ConflictGitPort;
|
|
@@ -45,6 +46,8 @@ interface FixPhaseDeps {
|
|
|
45
46
|
/** Global model-call budget for the operation. When exhausted, both fix stages stop before any
|
|
46
47
|
* further model call, leaving the remainder in the report. Absent = unlimited. */
|
|
47
48
|
budget?: ModelBudget;
|
|
49
|
+
/** Paths kept out of the detection scope — the caller's skipped and failed files. */
|
|
50
|
+
excludePaths?: ReadonlySet<string>;
|
|
48
51
|
}
|
|
49
52
|
/**
|
|
50
53
|
* The fix phase (opt-in). Stage 2: per-file convergence over each AI resolution ({@link runFixLoop}).
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{
|
|
2
|
-
`)}async function
|
|
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};
|
|
@@ -11,9 +11,15 @@ interface ModelBudget {
|
|
|
11
11
|
* the real model unchanged. Multiple wraps share one counter. */
|
|
12
12
|
wrap(model: ConflictModelPort): ConflictModelPort;
|
|
13
13
|
spent(): number;
|
|
14
|
+
/** The configured cap. 0 when the budget is unlimited — check {@link ModelBudget.exhausted} rather
|
|
15
|
+
* than comparing against this. */
|
|
16
|
+
limit(): number;
|
|
14
17
|
/** Calls left before the cap. `Infinity` when the budget is unlimited (`max` undefined). */
|
|
15
18
|
remaining(): number;
|
|
16
19
|
exhausted(): boolean;
|
|
20
|
+
/** Runs `notify` the first time it is called on this budget, and never again. The budget owns the
|
|
21
|
+
* once-only invariant so no caller can get the dedupe wrong across the two fix stages. */
|
|
22
|
+
notifyExhaustedOnce(notify: () => void): void;
|
|
17
23
|
}
|
|
18
24
|
declare function createModelBudget(max?: number): ModelBudget;
|
|
19
25
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
function e(e){let t=0,n=()=>e===void 0?1/0:e-t;return{wrap(e){return{generate:n=>(t++,e.generate(n))}},spent:()=>t,remaining:
|
|
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,6 +1,7 @@
|
|
|
1
1
|
import { IntraFileEditOp } from "../types/hints.js";
|
|
2
2
|
import { ConflictModelPort } from "../ports/model.js";
|
|
3
3
|
import { ConflictProgressEvent } from "../types/events.js";
|
|
4
|
+
import { ModelBudget } from "./model-budget.js";
|
|
4
5
|
import { AiTokenUsage } from "@gitkraken/shared-tools";
|
|
5
6
|
|
|
6
7
|
//#region src/resolver/refine-pass.d.ts
|
|
@@ -11,6 +12,9 @@ interface RefinePassDeps {
|
|
|
11
12
|
refineMaxSteps?: number;
|
|
12
13
|
onProgress?: (event: ConflictProgressEvent) => void;
|
|
13
14
|
filePath: string;
|
|
15
|
+
/** Operation-wide model-call budget. Checked before every step so the cap holds per call, not merely
|
|
16
|
+
* per pass — a pass entered with one call left would otherwise spend up to `refineMaxSteps`. */
|
|
17
|
+
budget?: ModelBudget;
|
|
14
18
|
}
|
|
15
19
|
interface RefinePassResult {
|
|
16
20
|
edits: IntraFileEditOp[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{sumUsages as e}from"../resolution/metrics.js";import{buildRefinePrompt as t}from"./prompt.js";import{createRefineTools as n}from"./refine-tools.js";async function r(r,i,a,o){let s=o.refineMaxSteps??5,{system:c,userMessage:l}=t(r,i,a),{definitions:u,execute:d}=n(),f=[{role:`user`,content:l}],p=r,m=[],h=0,g=0,_=[];for(let
|
|
1
|
+
import{sumUsages as e}from"../resolution/metrics.js";import{buildRefinePrompt as t}from"./prompt.js";import{createRefineTools as n}from"./refine-tools.js";async function r(r,i,a,o){let s=o.refineMaxSteps??5,{system:c,userMessage:l}=t(r,i,a),{definitions:u,execute:d}=n(),f=[{role:`user`,content:l}],p=r,m=[],h=0,g=0,_=[],v=t=>({edits:m,verifies:h,content:p,description:t,usage:e(_)});for(let e=0;e<s;e++){if(o.budget?.exhausted())return v(``);let t=e+1,n=await o.model.generate({system:c,messages:[...f],tools:u,temperature:o.temperature??0,signal:o.signal});if(n.usage&&_.push(n.usage),n.toolCalls&&n.toolCalls.length>0){f.push({role:`assistant`,content:n.text,toolCalls:n.toolCalls});for(let e of n.toolCalls){let n=e.args.ops,r=Array.isArray(n)?n:[];o.onProgress?.({type:`refine:tool-call`,filePath:o.filePath,stepNumber:t,ops:r});let{content:i,edits:a,verifiedCount:s,feedback:c,isError:l}=d(e,p);if(l?g++:(p=i,m.push(...a),h+=s,g=0),o.onProgress?.({type:`refine:tool-result`,filePath:o.filePath,stepNumber:t,appliedCount:a.length,isError:l,feedback:c}),f.push({role:`tool`,toolCallId:e.id,toolName:e.name,content:c}),g>=3)return o.onProgress?.({type:`refine:bailed`,filePath:o.filePath,reason:`Stopped after ${g} consecutive apply errors.`}),v(``)}continue}let r=(n.text??``).trim();return o.onProgress?.({type:`refine:final-text`,filePath:o.filePath,text:r}),v(r)}return v(``)}export{r as runRefinePass};
|
|
@@ -3,6 +3,12 @@ import { Conflict } from "../types/conflict.js";
|
|
|
3
3
|
import { ResolverDeps } from "./deps.js";
|
|
4
4
|
|
|
5
5
|
//#region src/resolver/resolve-conflict.d.ts
|
|
6
|
+
/** Confidence ceilings after refine. Exported because the fix phase applies the same `refineApplied`
|
|
7
|
+
* cap — a fixed file is no longer a fully-confident auto-apply either. */
|
|
8
|
+
declare const CONFIDENCE_CAPS: {
|
|
9
|
+
readonly refineNoAction: 0.6;
|
|
10
|
+
readonly refineApplied: 0.9;
|
|
11
|
+
};
|
|
6
12
|
declare function resolveConflict(conflict: Conflict, context: ResolutionContext, deps: ResolverDeps): Promise<Resolution>;
|
|
7
13
|
//#endregion
|
|
8
|
-
export { resolveConflict };
|
|
14
|
+
export { CONFIDENCE_CAPS, resolveConflict };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{checkResidualMarkers as e,toFileFinding as t}from"../ports/verify.js";import{resolveChunkContent as n}from"../resolution/chunk-apply.js";import{createResolution as r}from"../resolution/create-resolution.js";import{mergeUsages as i}from"../resolution/metrics.js";import{runResolverLoop as a}from"./loop.js";import{runRefinePass as o}from"./refine-pass.js";import{buildThreeWayDiff as s}from"./three-way-diff.js";const c={refineNoAction:.6,refineApplied:.9};async function l(l,u,d){let f=u.threeWayDiff||!u.refs?u:{...u,threeWayDiff:await s(l.filePath,u.refs,{git:d.git,signal:d.signal})},{chunks:p,strategy:m,confidence:h,description:g,metrics:_,followUpInstructions:v}=await a(l,f,{model:d.model,git:d.git,config:d.config,onProgress:d.onProgress,signal:d.signal}),y=r(l,p,{confidence:h,description:g,metrics:_,...m?{fileStrategy:m}:{},...f.threeWayDiff?{threeWayDiff:f.threeWayDiff}:{}}),b=v&&v.length>0;if(l.type===`text`&&b){d.onProgress?.({type:`refine:started`,filePath:l.filePath});let t=0;try{let r=await o(y.content,y.description,v,{model:d.model,signal:d.signal,temperature:d.config?.temperature,refineMaxSteps:d.config?.refineMaxSteps,onProgress:d.onProgress,filePath:l.filePath});if(r.edits.length>0){let n=e(l.filePath,r.content);if(n.valid)y={...y,content:r.content,edits:r.edits,strategy:`ai`},t=r.edits.length;else{let e=(n.issues??[]).map(e=>e.message).join(` `);d.onProgress?.({type:`refine:discarded`,filePath:l.filePath,reason:`Edits introduced conflict markers: ${e}`})}}if(t>0){y={...y,confidence:Math.min(y.confidence,c.refineApplied)};let e=[];for(let t=0;t<l.markers.length;t++){let i=l.markers[t];if(!i||i.userResolution==null)continue;let a=n({markerIndex:t,...i.userResolution},i);a!==``&&(r.content.includes(a)||e.push(t))}e.length>0&&(d.onProgress?.({type:`refine:touched-user-region`,filePath:l.filePath,markerIndexes:e}),y={...y,note:`Refine modified user-resolved region(s): marker ${e.join(`, `)}.`})}else r.edits.length===0&&r.verifies>0||(y={...y,confidence:Math.min(y.confidence,c.refineNoAction)});if(r.usage){let e=i(y.metrics??{inputTokens:0,outputTokens:0},r.usage);y={...y,metrics:{...y.metrics,...e}}}d.onProgress?.({type:`refine:completed`,filePath:l.filePath,appliedCount:t})}catch(e){if(d.signal?.aborted)throw e;let t=e instanceof Error?e.message:String(e);d.onProgress?.({type:`refine:failed`,filePath:l.filePath,reason:t}),y={...y,confidence:Math.min(y.confidence,c.refineNoAction)}}}else d.onProgress?.({type:`refine:skipped`,filePath:l.filePath,reason:`no-follow-ups`});if(d.validator&&l.type===`text`)try{let e=await d.validator.validate(new Map([[l.filePath,y.content]]),d.signal);e.length>0&&(y={...y,findings:e.map(t)})}catch(e){if(d.signal?.aborted)throw e;let t=e instanceof Error?e.message:String(e);d.onProgress?.({type:`validation:failed`,filePath:l.filePath,reason:t})}return d.onProgress?.({type:`resolver:completed`,filePath:l.filePath,stepCount:_.stepCount??0}),y}export{c as CONFIDENCE_CAPS,l as resolveConflict};
|
|
@@ -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;if(e.config?.fix&&e.parser)try{
|
|
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 +1 @@
|
|
|
1
|
-
import{buildSummaryPrompt as e}from"./prompt.js";const t=.8;function n(e,n=t){let
|
|
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};
|
package/dist/types/events.d.ts
CHANGED
|
@@ -102,12 +102,19 @@ type ConflictProgressEvent = {
|
|
|
102
102
|
type: 'fix:started';
|
|
103
103
|
filePath: string;
|
|
104
104
|
findingCount: number;
|
|
105
|
-
}
|
|
105
|
+
}
|
|
106
|
+
/** `iterations` counts refine rounds actually run for this file (0 = evaluated, nothing spent).
|
|
107
|
+
* `remainingFindings` is the error findings still standing — named apart from the model-call budget's
|
|
108
|
+
* `remaining`, which the sibling `fix:budget-exhausted` reports as `spent`/`limit`. */
|
|
109
|
+
| {
|
|
106
110
|
type: 'fix:completed';
|
|
107
111
|
filePath: string;
|
|
108
112
|
iterations: number;
|
|
109
|
-
|
|
110
|
-
}
|
|
113
|
+
remainingFindings: number;
|
|
114
|
+
}
|
|
115
|
+
/** `filePath` is set when one file's fix threw and the phase carried on without it; absent when the
|
|
116
|
+
* whole phase failed. */
|
|
117
|
+
| {
|
|
111
118
|
type: 'fix:failed';
|
|
112
119
|
filePath?: string;
|
|
113
120
|
reason: string;
|
package/dist/types/findings.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
//#region src/types/findings.d.ts
|
|
2
|
-
/** The package's own detection categories. `Finding.category`
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
/** The package's own detection categories. `Finding.category` stays open so consumer validators can
|
|
3
|
+
* contribute their own kinds (e.g. `'type-error'`, `'lint:no-console'`); these are the well-known
|
|
4
|
+
* values the package itself emits. */
|
|
5
5
|
type FindingCategory = 'broken-reference' | 'duplicate-definition' | 'syntax-error' | 'dropped-side';
|
|
6
6
|
type FindingTier = 'file' | 'project';
|
|
7
7
|
interface Finding {
|
|
8
|
-
/** Finding kind.
|
|
9
|
-
*
|
|
10
|
-
category: string;
|
|
8
|
+
/** Finding kind. The package emits {@link FindingCategory} values; a consumer validator may use its
|
|
9
|
+
* own category strings, which is what the open half of the union allows. */
|
|
10
|
+
category: FindingCategory | (string & {});
|
|
11
11
|
severity: 'error' | 'warning';
|
|
12
12
|
tier: FindingTier;
|
|
13
13
|
location: {
|
|
@@ -24,23 +24,28 @@ interface Finding {
|
|
|
24
24
|
}
|
|
25
25
|
/** Why a check produced its result — lets a consumer tell "ran, clean" apart from "did not run".
|
|
26
26
|
* `errored` means the parser was present but threw; `partial-unsupported-language` means some files
|
|
27
|
-
* in scope could not be parsed; `
|
|
28
|
-
*
|
|
29
|
-
|
|
27
|
+
* in scope could not be parsed; `partial-no-cross-file-resolution` means the parser backend declared
|
|
28
|
+
* it does not resolve references across files, so cross-file breaks are structurally out of reach;
|
|
29
|
+
* `skipped-unreadable-source` means the parent snapshots came back empty (unreadable source), so the
|
|
30
|
+
* before/after comparison had nothing to compare against. */
|
|
31
|
+
type CheckStatus = 'ran' | 'skipped-no-parser' | 'skipped-no-refs' | 'skipped-unreadable-source' | 'partial-unsupported-language' | 'partial-no-cross-file-resolution' | 'errored';
|
|
30
32
|
interface DetectionReport {
|
|
31
33
|
findings: Finding[];
|
|
32
34
|
checks: {
|
|
33
35
|
referenceIntegrity: CheckStatus;
|
|
34
36
|
structural: CheckStatus;
|
|
35
37
|
};
|
|
36
|
-
/** Honest coverage signal so empty findings are not read as "clean"
|
|
37
|
-
*
|
|
38
|
-
* comparison had nothing to compare), and `truncated` whether the scope was capped. Present only
|
|
39
|
-
* when the reference check ran (refs available). */
|
|
38
|
+
/** Honest coverage signal so empty findings are not read as "clean". Present only when the reference
|
|
39
|
+
* check ran (refs available). */
|
|
40
40
|
scope?: {
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
/** How many files the detector took into scope, after the cap. */inScope: number;
|
|
42
|
+
/** How many scope files had loadable parent content. 0 means the before/after comparison had
|
|
43
|
+
* nothing to compare. */
|
|
44
|
+
readable: number; /** Whether the scope exceeded the cap and its tail was dropped. */
|
|
43
45
|
truncated: boolean;
|
|
46
|
+
/** Whether the merge's incoming/auto-merged set could be discovered. `false` means the git port
|
|
47
|
+
* could not answer, so scope fell back to the conflict set — a coverage gap, not an empty diff. */
|
|
48
|
+
incomingDiscovered: boolean;
|
|
44
49
|
};
|
|
45
50
|
}
|
|
46
51
|
//#endregion
|
|
@@ -26,6 +26,11 @@ interface Resolution {
|
|
|
26
26
|
confidence: number;
|
|
27
27
|
description: string;
|
|
28
28
|
note?: string;
|
|
29
|
+
/** Set when this resolution is the resolver's emergency fallback — a whole-side take after the AI
|
|
30
|
+
* path failed, not a decision. Distinguishes it from an identical-looking rule or default take, and
|
|
31
|
+
* from a genuine AI resolution that happened to pick one side; reference-integrity uses it to decide
|
|
32
|
+
* whether discarding the other side was intentional. */
|
|
33
|
+
fallback?: true;
|
|
29
34
|
/** Non-blocking per-file findings from the consumer's validator port (post-refine). Folded into
|
|
30
35
|
* the operation's detection findings, hence into the verdict. */
|
|
31
36
|
findings?: Finding[];
|
package/dist/types/step.d.ts
CHANGED
|
@@ -26,13 +26,24 @@ interface StepConfig extends ResolverConfig {
|
|
|
26
26
|
/** Opt in to the fix phase (default `false` → report-only). When enabled, an AI-resolved file carrying
|
|
27
27
|
* error-severity per-file findings is routed back through refine to attempt a fix; what it cannot fix
|
|
28
28
|
* remains in the findings. Requires `ResolverDeps.parser` — without a parser the fix phase is a no-op
|
|
29
|
-
* and the report-only detection path runs instead.
|
|
29
|
+
* and the report-only detection path runs instead. Without `ResolutionContext.refs` (or
|
|
30
|
+
* `detectionRefs`) the per-file stage still runs on what needs no parent content — syntax errors and
|
|
31
|
+
* validator findings — but cross-file routing is skipped, since it is driven by the whole-set
|
|
32
|
+
* before/after report. Off by default so the report-only path is unchanged. */
|
|
30
33
|
fix?: boolean;
|
|
31
34
|
/** Max per-file fix iterations (refine → re-detect the file) when `fix` is on. Default 3. */
|
|
32
35
|
fixLoopMaxIterations?: number;
|
|
33
36
|
/** Max cross-file fix rounds (route project-level breaks to their use-site file → re-detect the whole
|
|
34
37
|
* set) when `fix` is on. Default 2. */
|
|
35
38
|
crossFileMaxIterations?: number;
|
|
39
|
+
/** Global cap on `model.generate` calls for the whole operation (resolve + refine + fix). Default
|
|
40
|
+
* `undefined` = unlimited. Primarily a backstop for the fix phase's multiplicative worst case
|
|
41
|
+
* (files × fixLoopMaxIterations + crossFileMaxIterations × routable calls). When spent, the fix
|
|
42
|
+
* phase stops attempting further fixes and leaves the remainder in findings — it never aborts the
|
|
43
|
+
* operation. The resolve loop is not gated by this (it is already bounded by `maxSteps`); its calls
|
|
44
|
+
* still count toward the cap. Batch-only: the fix phase is what enforces it, so it has no effect on
|
|
45
|
+
* the single-file `resolveConflict`. */
|
|
46
|
+
maxModelCalls?: number;
|
|
36
47
|
}
|
|
37
48
|
//#endregion
|
|
38
49
|
export { FileRule, StepConfig, StepResult };
|
package/dist/types/summary.d.ts
CHANGED
|
@@ -9,7 +9,8 @@ interface ReviewItem {
|
|
|
9
9
|
filePath: string;
|
|
10
10
|
confidence?: number;
|
|
11
11
|
reason: 'low-confidence' | 'finding';
|
|
12
|
-
/** Categories of the
|
|
12
|
+
/** Categories of the findings on this file, of any severity (error and warning alike); present when
|
|
13
|
+
* `reason` is `'finding'`. */
|
|
13
14
|
findingCategories?: string[];
|
|
14
15
|
}
|
|
15
16
|
/** Suggested honest verdict for the operation. `blocked` when any error-severity finding exists;
|
|
@@ -27,16 +28,19 @@ interface OperationDigest {
|
|
|
27
28
|
byStrategy: Record<ResolutionStrategy, number>;
|
|
28
29
|
/** Lowest file confidence across all resolutions; null when there are none. */
|
|
29
30
|
minConfidence: number | null;
|
|
30
|
-
/** Files needing review — low-confidence resolutions and files
|
|
31
|
-
* first, then lowest confidence first. */
|
|
31
|
+
/** Files needing review — low-confidence resolutions and files carrying any finding (error or
|
|
32
|
+
* warning), findings first, then lowest confidence first. */
|
|
32
33
|
needsReview: ReviewItem[];
|
|
33
34
|
/** Files skipped by rule or because they had no markers. */
|
|
34
35
|
skipped: number;
|
|
35
36
|
/** Files that errored during resolution. */
|
|
36
37
|
errors: number;
|
|
37
|
-
/** Whether
|
|
38
|
-
*
|
|
39
|
-
*
|
|
38
|
+
/** Whether detection achieved full coverage: both checks ran (a check that ran with some
|
|
39
|
+
* unsupported-language files in scope still counts — non-code files carry no references to break)
|
|
40
|
+
* *and* nothing was left unexamined (scope not truncated, incoming set discoverable). `false` when
|
|
41
|
+
* no detection was attached, a check was skipped/errored, the backend cannot resolve cross-file
|
|
42
|
+
* references, or part of the scope was never looked at — so empty findings must NOT be read as
|
|
43
|
+
* "clean". */
|
|
40
44
|
detectionComplete: boolean;
|
|
41
45
|
/** Total detection findings across the operation. */
|
|
42
46
|
findingCount: number;
|