@gitkraken/conflict-tools 0.7.1-beta.11 → 0.7.1-beta.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -9
- package/dist/index.d.ts +1 -1
- package/dist/ports/validator.d.ts +22 -6
- package/dist/reference-integrity/detect.d.ts +2 -10
- package/dist/reference-integrity/detect.js +1 -1
- package/dist/reference-integrity/validate-set.d.ts +24 -0
- package/dist/reference-integrity/validate-set.js +1 -0
- package/dist/resolver/fix-loop.d.ts +1 -1
- package/dist/resolver/fix-loop.js +2 -2
- 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 +12 -1
- package/dist/types/findings.d.ts +7 -1
- package/dist/types/summary.d.ts +6 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -131,15 +131,21 @@ Pass via `deps.parser`. When absent, detection is skipped and the report says so
|
|
|
131
131
|
|
|
132
132
|
### `Validator` (optional)
|
|
133
133
|
|
|
134
|
-
The consumer's validation port. Plug in any checks — syntax, real import resolution, types, lint, project rules. It
|
|
134
|
+
The consumer's validation port. Plug in any checks — syntax, real import resolution, types, lint, project rules. It is called with an overlay and a `call` context, and returns `ValidatorFinding[]` or `{ status: 'not-run', reason }`.
|
|
135
135
|
|
|
136
|
-
|
|
136
|
+
`call` says what the overlay holds:
|
|
137
|
+
|
|
138
|
+
- **Per-file calls** (`scope: 'file'`) hold one file, right after that file's refine. `role` is `result` for the resolved content, and `ours` / `theirs` when the package builds a baseline from the pre-merge parents (findings the parents already had are subtracted). A validator that cannot check one file in isolation (a compile check that needs its siblings) returns `{ status: 'not-run', reason }` for these. A per-file `not-run` adds no findings and is reported as a `validation:not-run` event.
|
|
139
|
+
- **The whole-set call** (`scope: 'set'`, `role: 'result'`) runs **once per operation**, after every file of the step is resolved, and after the fix phase when it runs. The overlay holds every resolved file's final content: a resolution's own content, or for a whole-side take the taken side's content when the detection snapshots hold it (otherwise the file is left out). `call.deleted` lists the paths the step deletes, including retired rename paths; they are absent from the overlay, so treat them as removed. There is no parent baseline: errors the merged set has are the merge's to answer for. The call is skipped when there is nothing to validate.
|
|
140
|
+
|
|
141
|
+
The whole-set outcome is reported on `DetectionReport.checks.validator` (`ran`, `skipped-no-validator`, `not-run`, `errored`) with `validatorReason`. A whole-set `not-run` or throw never fails the operation, but it makes `detectionComplete` false, so the verdict cannot read `clean`.
|
|
137
142
|
|
|
138
143
|
```typescript
|
|
139
144
|
import type { Validator, ValidatorFinding } from '@gitkraken/conflict-tools';
|
|
140
145
|
|
|
141
146
|
const validator: Validator = {
|
|
142
|
-
async validate(sources) {
|
|
147
|
+
async validate(sources, signal, call) {
|
|
148
|
+
if (call.scope === 'file') return { status: 'not-run', reason: 'needs the whole set' };
|
|
143
149
|
const findings: ValidatorFinding[] = [];
|
|
144
150
|
for (const [file, content] of sources) {
|
|
145
151
|
for (const err of await typecheck(file, content)) {
|
|
@@ -151,7 +157,7 @@ const validator: Validator = {
|
|
|
151
157
|
};
|
|
152
158
|
```
|
|
153
159
|
|
|
154
|
-
**Advisory, never blocking.** Its findings fold into the operation's detection findings and the verdict; a validator finding never triggers a re-resolve, and a *throwing* validator never destroys the resolution — it is kept intact and the failure is reported. On the resolve/report path that report is a `validation:failed` event; inside the opt-in fix phase a validator throw folds into the phase's per-file isolation and surfaces as `fix:failed` (like any per-file throw there), so telemetry that must catch every validator failure should watch both. `ValidatorFinding.category` is an open string (e.g. `'type-error'`, `'lint:no-console'`) so consumer categories flow through alongside the package's own `FindingCategory` values.
|
|
160
|
+
**Advisory, never blocking.** Its findings fold into the operation's detection findings and the verdict; a validator finding never triggers a re-resolve, and a *throwing* validator never destroys the resolution — it is kept intact and the failure is reported. On the resolve/report path that report is a `validation:failed` event (`scope` says whether it was a per-file or the whole-set call); inside the opt-in fix phase a validator throw folds into the phase's per-file isolation and surfaces as `fix:failed` (like any per-file throw there), so telemetry that must catch every validator failure should watch both. `ValidatorFinding.category` is an open string (e.g. `'type-error'`, `'lint:no-console'`) so consumer categories flow through alongside the package's own `FindingCategory` values.
|
|
155
161
|
|
|
156
162
|
The one blocking gate — **residual conflict markers** — is *not* this port's job. It is checked built-in and language-agnostic inside the resolve loop. (This was the old `defaultVerifier`; it is now internal. `defaultVerifier`, `ResolutionVerifier`, and `VerificationResult` are no longer exported.)
|
|
157
163
|
|
|
@@ -190,7 +196,7 @@ import { resolveConflict } from '@gitkraken/conflict-tools';
|
|
|
190
196
|
const resolution = await resolveConflict(conflict, context, {
|
|
191
197
|
model,
|
|
192
198
|
git,
|
|
193
|
-
validator, // optional — advisory
|
|
199
|
+
validator, // optional — advisory checks
|
|
194
200
|
config: { maxSteps: 15, temperature: 0, refineMaxSteps: 5, modelCallTimeoutMs: 300_000 },
|
|
195
201
|
onProgress: (event) => console.log(event),
|
|
196
202
|
});
|
|
@@ -271,7 +277,7 @@ const step = await resolveConflicts(
|
|
|
271
277
|
model,
|
|
272
278
|
git,
|
|
273
279
|
parser, // optional — supply to enable set-level detection (stage 3)
|
|
274
|
-
validator, // optional — advisory
|
|
280
|
+
validator, // optional — advisory checks
|
|
275
281
|
config: {
|
|
276
282
|
rules: [
|
|
277
283
|
{ match: ['*.lock', 'pnpm-lock.yaml'], strategy: 'take-theirs' },
|
|
@@ -429,7 +435,9 @@ The scope includes the resolved files *and* the merge's auto-merged/incoming set
|
|
|
429
435
|
```typescript
|
|
430
436
|
interface DetectionReport {
|
|
431
437
|
findings: Finding[];
|
|
432
|
-
checks: { referenceIntegrity: CheckStatus; structural: CheckStatus };
|
|
438
|
+
checks: { referenceIntegrity: CheckStatus; structural: CheckStatus; validator: ValidatorCheckStatus };
|
|
439
|
+
/** Why the whole-set validator did not run or threw. */
|
|
440
|
+
validatorReason?: string;
|
|
433
441
|
/** Honest coverage — present only when the reference check ran. */
|
|
434
442
|
scope?: { inScope: number; readable: number; truncated: boolean; incomingDiscovered: boolean };
|
|
435
443
|
}
|
|
@@ -479,7 +487,8 @@ import { buildOperationDigest } from '@gitkraken/conflict-tools';
|
|
|
479
487
|
|
|
480
488
|
const digest = buildOperationDigest(step);
|
|
481
489
|
// digest.verdict — 'clean' | 'needs-review' | 'blocked'
|
|
482
|
-
// digest.detectionComplete — did every detection check run to completion
|
|
490
|
+
// digest.detectionComplete — did every detection check run to completion (a supplied validator that did not run counts as incomplete)
|
|
491
|
+
// digest.validation — ValidatorCheckStatus of the whole-set validator call; digest.validationReason says why when it did not run
|
|
483
492
|
// digest.findingCount — total detection findings
|
|
484
493
|
// digest.needsReview — ReviewItem[] (findings first, then lowest confidence first)
|
|
485
494
|
// digest.minConfidence — lowest file confidence, or null
|
|
@@ -505,6 +514,7 @@ When enabled:
|
|
|
505
514
|
- **Duplicate blocks** — a block of four or more significant lines that the merged file holds more often than either parent is reported as a `duplicate-block` warning whenever `refs` are set, with or without the fix phase. The fix phase still routes it through refine to keep one copy, and a leftover one gives `needs-review`, not `blocked`. This detection is language-agnostic, so it needs no `ParserPort`, but it needs at least one parent and does not see a block neither parent holds. Without a `ParserPort` the fix phase still runs the duplicate-block and validator checks, and cross-file routing is skipped.
|
|
506
515
|
- **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.
|
|
507
516
|
- **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. The refine is seeded with the defining file's current definitions (names, kind, line — a token-cheap list, not its text) so it can spot a renamed or moved symbol behind the broken use, even though the refine tool cannot read other files. A file is routed when it has a write channel: an AI resolution, or an AI-unanimous whole-side take (relabelled to `ai` when the fix edit lands). A deliberate rule/default take and an emergency fallback take are not routed, and a break landing on an untouched auto-merged file (no resolution to carry the edit) is reported but not auto-fixed.
|
|
517
|
+
- **Whole-set validator routing** — after the cross-file stage, the error findings of the validator's whole-set call (`scope: 'set'`) on files with a write channel (same rule as above) are routed to those files' refine and the set is validated again, up to `crossFileMaxIterations` (default 2) rounds. The stage ends when no routable error remains, a round lands no edit, the validator reports `not-run` or `errored`, the budget is spent, or the signal aborts; the last outcome is what the report carries, and findings on files without a write channel stay in it. When any round landed an edit, the parser report is refreshed with one more detection so `checks` and parser findings describe the final content.
|
|
508
518
|
|
|
509
519
|
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.
|
|
510
520
|
|
|
@@ -617,7 +627,9 @@ score(resolution, goldenFile);
|
|
|
617
627
|
| `fix:completed` | The fix loop finished evaluating a file (`iterations`, `remainingFindings`) — emitted even for a clean file (iterations 0, remainingFindings 0). `iterations: 0` means "evaluated, spent no model call" |
|
|
618
628
|
| `fix:failed` | A non-abort error in the opt-in fix phase (`filePath?`, `reason`). With `filePath`: that one file threw and was left at its pre-fix resolution while the phase carried on — detection still runs. Without `filePath`: the whole phase threw before it could report, so both checks are forced to `errored` |
|
|
619
629
|
| `fix:budget-exhausted` | The global `maxModelCalls` budget was spent (`spent`, `limit`); the fix phase stopped and left the remainder in findings |
|
|
620
|
-
| `validation
|
|
630
|
+
| `fix:set-validation-round` | A round of whole-set validator routing started (`round`, 1-based; `findingCount` of the set call; `routedFiles`) — emitted once per round that routes at least one file |
|
|
631
|
+
| `validation:failed` | The consumer validator threw; reported and swallowed, resolution kept (`scope`: `file` or `set`, `filePath?` — absent for the set call, `reason`) |
|
|
632
|
+
| `validation:not-run` | The consumer validator answered `not-run` (`scope`, `filePath?` — absent for the set call, `reason`) |
|
|
621
633
|
|
|
622
634
|
Note: `refine:tool-result.appliedCount` reports edits applied to an in-progress buffer per turn. The final disposition is signaled by `refine:completed` / `refine:discarded` / `refine:failed` — only edits that survive residual-marker validation appear in `Resolution.edits`.
|
|
623
635
|
|
package/dist/index.d.ts
CHANGED
|
@@ -11,12 +11,12 @@ import { ToolCall, ToolDefinition, ToolResult } from "./types/tool.js";
|
|
|
11
11
|
import { ConflictModelMessage, ConflictModelParams, ConflictModelPort, ConflictModelResult, ResolverConfig } from "./ports/model.js";
|
|
12
12
|
import { ParsedFile, ParserCapabilities, ParserPort, SourceOverlay, SymbolDefinition, SymbolReference, SyntaxIssue } from "./ports/parser.js";
|
|
13
13
|
import { Validator, ValidatorFinding } from "./ports/validator.js";
|
|
14
|
+
import { ConflictProgressEvent } from "./types/events.js";
|
|
14
15
|
import { DetectDeps, detect } from "./reference-integrity/detect.js";
|
|
15
16
|
import { createResolution } from "./resolution/create-resolution.js";
|
|
16
17
|
import { CodeLossDetection, detectFallbackCodeLoss } from "./resolution/detect-code-loss.js";
|
|
17
18
|
import { takeOurs, takeTheirs } from "./resolution/take-strategy.js";
|
|
18
19
|
import { applyResolutions } from "./resolver/apply-resolutions.js";
|
|
19
|
-
import { ConflictProgressEvent } from "./types/events.js";
|
|
20
20
|
import { resolveConflict } from "./resolver/resolve-conflict.js";
|
|
21
21
|
import { FailedEntry, FailureReason, FileRule, SkippedEntry, StepConfig, StepResult } from "./types/step.js";
|
|
22
22
|
import { resolveConflicts } from "./resolver/resolve-conflicts.js";
|
|
@@ -12,18 +12,34 @@ interface ValidatorFinding {
|
|
|
12
12
|
line?: number;
|
|
13
13
|
message: string;
|
|
14
14
|
}
|
|
15
|
+
interface ValidationCall {
|
|
16
|
+
/** `file`: one file's content, for a per-file check or a parent baseline. `set`: every resolved file of the step, once, after all files are resolved. */
|
|
17
|
+
scope: 'file' | 'set';
|
|
18
|
+
/** Which content a `file` call holds. Always `result` for `set`. */
|
|
19
|
+
role: 'result' | 'ours' | 'theirs';
|
|
20
|
+
/** `set` only: paths the step deletes. They are absent from `sources`; treat them as removed. */
|
|
21
|
+
deleted?: readonly string[];
|
|
22
|
+
}
|
|
23
|
+
interface ValidatorNotRun {
|
|
24
|
+
status: 'not-run';
|
|
25
|
+
/** Short, human-readable: "siblings still conflicted", "unsupported language", "go.mod conflicted". */
|
|
26
|
+
reason: string;
|
|
27
|
+
}
|
|
28
|
+
type ValidatorOutcome = ValidatorFinding[] | ValidatorNotRun;
|
|
15
29
|
/**
|
|
16
30
|
* The consumer's validation port. Plug in any checks — syntax, real import resolution, types, lint,
|
|
17
31
|
* project rules. It runs after refine; the opt-in fix loop may route new error-severity findings back
|
|
18
32
|
* through refine. Residual conflict markers are the resolver's blocking gate and are checked separately.
|
|
19
33
|
*
|
|
20
|
-
* `
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
34
|
+
* `call` says what `sources` holds. Per-file calls (`scope: 'file'`) hold one file; `role` says which
|
|
35
|
+
* version: the `result`, or a pre-merge parent (`ours`/`theirs`) to build a baseline. The whole-set call
|
|
36
|
+
* (`scope: 'set'`) holds every resolved file of the step and runs once, after all files are resolved (and
|
|
37
|
+
* after the fix phase, when it runs); it has no baseline. Validate the content supplied rather than reading
|
|
38
|
+
* the working tree. A validator that cannot check a single file in isolation returns
|
|
39
|
+
* `{ status: 'not-run', reason }` for `file` calls and does its work on the `set` call.
|
|
24
40
|
*/
|
|
25
41
|
interface Validator {
|
|
26
|
-
validate(sources: SourceOverlay, signal
|
|
42
|
+
validate(sources: SourceOverlay, signal: AbortSignal | undefined, call: ValidationCall): Promise<ValidatorOutcome>;
|
|
27
43
|
}
|
|
28
44
|
//#endregion
|
|
29
|
-
export { Validator, ValidatorFinding };
|
|
45
|
+
export { ValidationCall, Validator, ValidatorFinding, ValidatorNotRun, ValidatorOutcome };
|
|
@@ -3,6 +3,7 @@ import { DetectionReport, Finding } from "../types/findings.js";
|
|
|
3
3
|
import { Resolution, ResolutionContext } from "../types/resolution.js";
|
|
4
4
|
import { ParserPort } from "../ports/parser.js";
|
|
5
5
|
import { Validator } from "../ports/validator.js";
|
|
6
|
+
import { ConflictProgressEvent } from "../types/events.js";
|
|
6
7
|
|
|
7
8
|
//#region src/reference-integrity/detect.d.ts
|
|
8
9
|
interface DetectDeps {
|
|
@@ -34,16 +35,7 @@ interface FileFindingsDeps {
|
|
|
34
35
|
parser?: ParserPort;
|
|
35
36
|
validator?: Validator;
|
|
36
37
|
signal?: AbortSignal;
|
|
37
|
-
|
|
38
|
-
/** Per-file findings split by who produced them. They are kept apart because only the `text` and
|
|
39
|
-
* `validator` halves are the file's own to keep: parser findings computed over a *single-file* overlay are
|
|
40
|
-
* provisional — a symbol defined in another file is unresolved there by construction — so the
|
|
41
|
-
* authoritative version is whatever `detect()` says over the whole set. The fix loop uses all of them to
|
|
42
|
-
* converge and persists `text` and `validator`. */
|
|
43
|
-
interface FileFindingsDeps {
|
|
44
|
-
parser?: ParserPort;
|
|
45
|
-
validator?: Validator;
|
|
46
|
-
signal?: AbortSignal;
|
|
38
|
+
onProgress?: (event: ConflictProgressEvent) => void;
|
|
47
39
|
}
|
|
48
40
|
/** Per-file findings split by who produced them. They are kept apart because only the `text` and
|
|
49
41
|
* `validator` halves are the file's own to keep: parser findings computed over a *single-file* overlay are provisional — a
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{duplicateBlockFindings as e}from"../resolution/duplicate-blocks.js";import{toFileFinding as t}from"../validation/findings.js";import{buildSnapshots as n,carriesNoOwnContent as r,safeReadFile as i}from"./snapshot.js";async function a(e,t,a){let s=a.parser;if(!s)return{findings:[],checks:{referenceIntegrity:`skipped-no-parser`,structural:`skipped-no-parser`}};try{let c=t.detectionRefs??t.refs?await n(e,t,a):null;if(c){let t=await s.analyze(c.ours,a.signal),n=await s.analyze(c.theirs,a.signal),r=await s.analyze(c.result,a.signal);return{findings:[...u(t,n,r).findings,...f(r,[t,n]).findings,...m(e,c)],checks:{referenceIntegrity:o(c,s,[t,n,r]),structural:g(r)?`partial-unsupported-language`:`ran`},scope:{inScope:c.inScope,readable:c.readable,truncated:c.truncated,incomingDiscovered:c.incomingDiscovered}}}let l=new Map;for(let t of e){let e=r(t)?await i(a.git,t.filePath,{signal:a.signal}):t.content;e!=null&&l.set(t.filePath,e)}let d=await s.analyze(l,a.signal);return{findings:f(d,[]).findings,checks:{referenceIntegrity:`skipped-no-refs`,structural:g(d)?`partial-unsupported-language`:`ran`}}}catch(e){if(a.signal?.aborted)throw e;return{findings:[],checks:{referenceIntegrity:`errored`,structural:`errored`}}}}function o(e,t,n){return e.inScope>0&&e.readable===0?`skipped-unreadable-source`:t.capabilities?.crossFileResolution?n.some(g)?`partial-unsupported-language`:`ran`:`partial-no-cross-file-resolution`}async function s(n,r,i,a){let o=a.parser,s=[],l=[];if(o){let e=async e=>e==null?new Map:o.analyze(new Map([[n,e]]),a.signal),t=await e(r.ours),c=await e(r.theirs),d=await o.analyze(new Map([[n,i]]),a.signal),p=u(t,c,d),m=f(d,[t,c]);s=[...p.findings.filter(e=>e.tier===`file`),...m.findings],l.push(...p.preExisting.filter(e=>e.tier===`file`),...m.preExisting)}let d=[],p=a.validator;if(p){let e=async
|
|
1
|
+
import{duplicateBlockFindings as e}from"../resolution/duplicate-blocks.js";import{toFileFinding as t}from"../validation/findings.js";import{buildSnapshots as n,carriesNoOwnContent as r,safeReadFile as i}from"./snapshot.js";async function a(e,t,a){let s=a.parser;if(!s)return{findings:[],checks:{referenceIntegrity:`skipped-no-parser`,structural:`skipped-no-parser`,validator:`skipped-no-validator`}};try{let c=t.detectionRefs??t.refs?await n(e,t,a):null;if(c){let t=await s.analyze(c.ours,a.signal),n=await s.analyze(c.theirs,a.signal),r=await s.analyze(c.result,a.signal);return{findings:[...u(t,n,r).findings,...f(r,[t,n]).findings,...m(e,c)],checks:{referenceIntegrity:o(c,s,[t,n,r]),structural:g(r)?`partial-unsupported-language`:`ran`,validator:`skipped-no-validator`},scope:{inScope:c.inScope,readable:c.readable,truncated:c.truncated,incomingDiscovered:c.incomingDiscovered}}}let l=new Map;for(let t of e){let e=r(t)?await i(a.git,t.filePath,{signal:a.signal}):t.content;e!=null&&l.set(t.filePath,e)}let d=await s.analyze(l,a.signal);return{findings:f(d,[]).findings,checks:{referenceIntegrity:`skipped-no-refs`,structural:g(d)?`partial-unsupported-language`:`ran`,validator:`skipped-no-validator`}}}catch(e){if(a.signal?.aborted)throw e;return{findings:[],checks:{referenceIntegrity:`errored`,structural:`errored`,validator:`skipped-no-validator`}}}}function o(e,t,n){return e.inScope>0&&e.readable===0?`skipped-unreadable-source`:t.capabilities?.crossFileResolution?n.some(g)?`partial-unsupported-language`:`ran`:`partial-no-cross-file-resolution`}async function s(n,r,i,a){let o=a.parser,s=[],l=[];if(o){let e=async e=>e==null?new Map:o.analyze(new Map([[n,e]]),a.signal),t=await e(r.ours),c=await e(r.theirs),d=await o.analyze(new Map([[n,i]]),a.signal),p=u(t,c,d),m=f(d,[t,c]);s=[...p.findings.filter(e=>e.tier===`file`),...m.findings],l.push(...p.preExisting.filter(e=>e.tier===`file`),...m.preExisting)}let d=[],p=a.validator;if(p){let e=async(e,r)=>{let i=await p.validate(new Map([[n,e]]),a.signal,{scope:`file`,role:r});return Array.isArray(i)?i.map(t):(a.onProgress?.({type:`validation:not-run`,filePath:n,scope:`file`,reason:i.reason}),[])},o=[];r.ours!=null&&o.push(await e(r.ours,`ours`)),r.theirs!=null&&o.push(await e(r.theirs,`theirs`));let s=c(await e(i,`result`),o,e=>JSON.stringify([e.category,e.message]));d=s.kept,l.push(...s.preExisting)}return{parser:s,validator:d,preExisting:l,text:e(n,i,r)}}function c(e,t,n){let r=new Map;for(let e of t){let t=new Map;for(let r of e){let e=n(r);t.set(e,(t.get(e)??0)+1)}for(let[e,n]of t)r.set(e,Math.max(r.get(e)??0,n))}let i=[],a=[];for(let t of[...e].sort((e,t)=>e.location.line-t.location.line)){let e=n(t),o=r.get(e)??0;o>0?(a.push(t),r.set(e,o-1)):i.push(t)}return{kept:i,preExisting:a}}function l(e){let t=new Set;for(let n of e.definitions)t.add(n.name);for(let n of e.bindings??[])t.add(n.name);return t}function u(e,t,n){let r=d(e,t),i=[],a=[];for(let[o,s]of n){let n=l(s);for(let c of s.references){if(c.resolved||n.has(c.name))continue;let s=r.get(_(o,c.name));s&&([e,t].some(e=>{let t=e.get(o);return t!=null&&!l(t).has(c.name)&&t.references.some(e=>e.name===c.name&&!e.resolved)})?a:i).push({category:`broken-reference`,severity:`error`,tier:s.crossFile?`project`:`file`,location:{file:o,line:c.line,symbol:c.name},provenance:{side:s.side,line:s.line},...s.crossFile&&s.targetFile?{definitionFile:s.targetFile}:{},message:`Reference "${c.name}" resolved before the merge (on ${s.side}) but no longer resolves.`})}}return{findings:i,preExisting:a}}function d(e,t){let n=new Map,r=(e,t,r,i,a,o)=>{let s=_(t,r);n.has(s)||n.set(s,{side:e,line:i,crossFile:a,targetFile:o})},i=[[`ours`,e],[`theirs`,t]];for(let[e,t]of i)for(let[n,i]of t){for(let t of i.definitions)r(e,n,t.name,t.line,!1);for(let t of i.bindings??[])r(e,n,t.name,t.line,!1)}for(let[e,t]of i)for(let[n,i]of t)for(let t of i.references)t.resolved&&r(e,n,t.name,t.line,t.target?t.target.file!==n:!0,t.target?.file);return n}function f(e,t){let n=p(t),r=new Set;for(let e of t)for(let t of e.keys())r.add(t);let i=[],a=[];for(let[o,s]of e){let e=r.has(o),l=new Map;for(let t of s.definitions){let r=l.get(t.name);if(r==null){l.set(t.name,t.line);continue}!e||n.has(_(o,t.name))||i.push({category:`duplicate-definition`,severity:`warning`,tier:`file`,location:{file:o,line:t.line,symbol:t.name},message:`Duplicate definition of "${t.name}" (also defined at line ${r}).`})}let u=e=>e.map(e=>({category:`syntax-error`,severity:`error`,tier:`file`,location:{file:o,line:e.line},message:e.message})),d=t.map(e=>e.get(o)?.syntaxErrors??[]),f=c(u(s.syntaxErrors),d.map(u),e=>e.message),p=d.some(e=>e.length>0);for(let e of f.kept)p&&(e.severity=`warning`),i.push(e);a.push(...f.preExisting)}return{findings:i,preExisting:a}}function p(e){let t=new Set;for(let n of e)for(let[e,r]of n){let n=new Set;for(let i of r.definitions){let r=_(e,i.name);n.has(r)&&t.add(r),n.add(r)}}return t}function m(e,t){let n=[];for(let r of e){if(!r.fallback||r.strategy!==`take-ours`&&r.strategy!==`take-theirs`)continue;let e=r.strategy===`take-ours`?`theirs`:`ours`,i=(e===`ours`?t.ours:t.theirs).get(r.filePath);if(i==null)continue;let a=h(i,r.content);a<20||n.push({category:`dropped-side`,severity:`error`,tier:`file`,location:{file:r.filePath,line:1},message:`Resolution took "${r.strategy}" wholesale, discarding ~${a} line(s) of the ${e} side.`})}return n}function h(e,t){let n=new Set(t.split(`
|
|
2
2
|
`)),r=0;for(let t of e.split(`
|
|
3
3
|
`))t.trim()!==``&&!n.has(t)&&r++;return r}function g(e){for(let t of e.values())if(!t.supported)return!0;return!1}function _(e,t){return`${e}\u0000${t}`}export{s as collectFileFindings,a as detect};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { DetectionReport, Finding, ValidatorCheckStatus } from "../types/findings.js";
|
|
2
|
+
import { Resolution } from "../types/resolution.js";
|
|
3
|
+
import { Validator } from "../ports/validator.js";
|
|
4
|
+
import { ConflictProgressEvent } from "../types/events.js";
|
|
5
|
+
import { DetectionSnapshots } from "./snapshot.js";
|
|
6
|
+
|
|
7
|
+
//#region src/reference-integrity/validate-set.d.ts
|
|
8
|
+
interface SetValidation {
|
|
9
|
+
findings: Finding[];
|
|
10
|
+
status: ValidatorCheckStatus;
|
|
11
|
+
reason?: string;
|
|
12
|
+
}
|
|
13
|
+
interface SetValidationDeps {
|
|
14
|
+
validator?: Validator;
|
|
15
|
+
signal?: AbortSignal;
|
|
16
|
+
onProgress?: (event: ConflictProgressEvent) => void;
|
|
17
|
+
}
|
|
18
|
+
/** The one whole-set validator call of an operation: every resolution's final content, and the paths the
|
|
19
|
+
* step deletes. A resolution carrying no content of its own contributes the taken side's content from the
|
|
20
|
+
* snapshots when they hold it, and is left out otherwise. Never throws except on an aborted signal. */
|
|
21
|
+
declare function validateResolvedSet(resolutions: Resolution[], snapshots: DetectionSnapshots | null, deps: SetValidationDeps): Promise<SetValidation>;
|
|
22
|
+
declare function withSetValidation(detection: DetectionReport, set: SetValidation): DetectionReport;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { SetValidation, SetValidationDeps, validateResolvedSet, withSetValidation };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{toFileFinding as e}from"../validation/findings.js";import{carriesNoOwnContent as t}from"./snapshot.js";async function n(n,r,i){let a=i.validator;if(!a)return{findings:[],status:`skipped-no-validator`};let o=new Map,s=[];for(let e of n){if(e.strategy===`deleted`){s.push(e.filePath,...e.retiredPaths??[]);continue}let n=t(e)?r?.result.get(e.filePath):e.content;n!=null&&o.set(e.filePath,n)}if(o.size===0&&s.length===0)return{findings:[],status:`ran`};try{let t=await a.validate(o,i.signal,{scope:`set`,role:`result`,deleted:s});return Array.isArray(t)?{findings:t.map(e),status:`ran`}:(i.onProgress?.({type:`validation:not-run`,scope:`set`,reason:t.reason}),{findings:[],status:`not-run`,reason:t.reason})}catch(e){if(i.signal?.aborted)throw e;let t=e instanceof Error?e.message:String(e);return i.onProgress?.({type:`validation:failed`,scope:`set`,reason:t}),{findings:[],status:`errored`,reason:t}}}function r(e,t){return{...e,findings:[...e.findings,...t.findings],checks:{...e.checks,validator:t.status},...t.reason?{validatorReason:t.reason}:{}}}export{n as validateResolvedSet,r as withSetValidation};
|
|
@@ -4,8 +4,8 @@ import { Resolution, ResolutionContext } from "../types/resolution.js";
|
|
|
4
4
|
import { ConflictModelPort } from "../ports/model.js";
|
|
5
5
|
import { ParserPort } from "../ports/parser.js";
|
|
6
6
|
import { Validator } from "../ports/validator.js";
|
|
7
|
-
import { ParentContents } from "../reference-integrity/detect.js";
|
|
8
7
|
import { ConflictProgressEvent } from "../types/events.js";
|
|
8
|
+
import { ParentContents } from "../reference-integrity/detect.js";
|
|
9
9
|
import { StepConfig } from "../types/step.js";
|
|
10
10
|
import { DetectionSnapshots } from "../reference-integrity/snapshot.js";
|
|
11
11
|
import { ModelBudget } from "./model-budget.js";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{buildSnapshots as e}from"../reference-integrity/snapshot.js";import{collectFileFindings as t,detect as n}from"../reference-integrity/detect.js";import{checkResidualMarkers as r}from"../validation/markers.js";import{matchFileGuidance as i}from"./file-guidance.js";import{runRefinePass as a}from"./refine-pass.js";import{CONFIDENCE_CAPS as o}from"./resolve-conflict.js";async function
|
|
2
|
-
`)}function
|
|
1
|
+
import{buildSnapshots as e}from"../reference-integrity/snapshot.js";import{collectFileFindings as t,detect as n}from"../reference-integrity/detect.js";import{checkResidualMarkers as r}from"../validation/markers.js";import{matchFileGuidance as i}from"./file-guidance.js";import{runRefinePass as a}from"./refine-pass.js";import{CONFIDENCE_CAPS as o}from"./resolve-conflict.js";import{validateResolvedSet as s,withSetValidation as c}from"../reference-integrity/validate-set.js";async function l(e,n,s){let c=s.config?.fixLoopMaxIterations??3,l={parser:s.parser,validator:s.validator,signal:s.signal,onProgress:s.onProgress},g={content:e.content,findings:await(r=>t(e.filePath,n,r,l))(e.content)};g.findings.preExisting.length>0&&s.onProgress?.({type:`findings:pre-existing`,filePath:e.filePath,findings:g.findings.preExisting});let _=f(e,g.findings),v=d(g.findings);if(v.length===0)return m(s,e.filePath,0,0),p(e,_,g.findings);if(s.budget?.exhausted())return s.onBudgetExhausted?.(),m(s,e.filePath,0,v.length),u(p(e,_,g.findings),v);s.onProgress?.({type:`fix:started`,filePath:e.filePath,findingCount:v.length});let x=[],S=0,C=y(v);for(;S<c&&v.length>0;){if(s.budget?.exhausted()){s.onBudgetExhausted?.();break}S++;let o=await a(g.content,e.description,h(v),{model:s.model,signal:s.signal,temperature:s.config?.temperature,refineMaxSteps:s.config?.refineMaxSteps,modelCallTimeoutMs:s.config?.modelCallTimeoutMs,onProgress:s.onProgress,filePath:e.filePath,fileGuidance:i(s.fileGuidance,[e.filePath,...e.retiredPaths??[]]),budget:s.budget});if(o.edits.length===0)break;if(!r(e.filePath,o.content).valid){s.onProgress?.({type:`refine:discarded`,filePath:e.filePath,reason:`Fix edits introduced conflict markers.`});break}let c=await t(e.filePath,n,o.content,l),u=d(c);if(b(g.findings,c)){s.onProgress?.({type:`refine:discarded`,filePath:e.filePath,reason:`Fix edits regressed the file (was ${v.length} error(s), now ${u.length}).`});break}g={content:o.content,findings:c},x.push(...o.edits),v=u;let f=y(v);if(f===C)break;C=f}m(s,e.filePath,S,v.length);let w=p({...e,content:g.content},_,g.findings),T=d(g.findings);return x.length===0?u(w,T):u({...w,strategy:`ai`,edits:[...e.edits??[],...x],confidence:Math.min(w.confidence,o.refineApplied)},T)}function u(e,t){let n=t.filter(e=>e.severity===`error`);if(n.length===0)return e;let r=[...n].sort((e,t)=>e.location.line-t.location.line).slice(0,3).map(e=>`${e.category} at line ${e.location.line}`).join(`, `),i=n.length>3?`, and ${n.length-3} more`:``;return{...e,confidence:Math.min(e.confidence,o.errorsRemain),description:`Unresolved: ${n.length} error(s) remain after the fix pass (${r}${i}); fix them by hand before committing. ${e.description}`}}function d(e){return[...e.parser,...e.validator,...e.text].filter(e=>e.severity===`error`||e.category===`duplicate-block`)}function f(e,t){let n=(e,t)=>e.category===t.category&&e.location.file===t.location.file&&e.location.line===t.location.line&&e.location.symbol===t.location.symbol&&e.message===t.message;return(e.findings??[]).filter(e=>e.category!==`duplicate-block`&&![...t.validator,...t.preExisting].some(t=>n(e,t)))}function p(e,t,n){let r=[...t,...n.text,...n.validator];if(r.length===0){if(!e.findings)return e;let{findings:t,...n}=e;return n}return{...e,findings:r}}function m(e,t,n,r){e.onProgress?.({type:`fix:completed`,filePath:t,iterations:n,remainingFindings:r})}function h(e){return e.slice(0,20).map(g)}function g(e){let t=e.location.symbol?_(e.location.symbol):void 0,n=t?`"${t}" (line ${e.location.line})`:`line ${e.location.line}`;return`${_(e.category)} at ${n}: ${_(e.message)}`}function _(e){return e.replace(/[\p{Cc}\p{Cf}]/gu,` `).replace(/\s+/g,` `).trim().slice(0,300)}function v(e){return JSON.stringify([e.category,e.location.file,e.location.symbol??``])}function y(e){return e.map(v).sort().join(`
|
|
2
|
+
`)}function b(e,t){let n=e=>[...e.parser,...e.validator,...e.text].filter(e=>e.severity===`error`||e.category===`syntax-error`||e.category===`duplicate-block`),r=n(e),i=n(t);if(i.length>r.length)return!0;let a=new Set(r.map(v));return i.some(e=>!a.has(v(e)))}async function x(t,r,i){let a=i.config?.maxDetectionScopeFiles,o={git:i.git,parser:i.parser,signal:i.signal,excludePaths:i.excludePaths,maxScopeFiles:a},s=await e(t,r,{git:i.git,signal:i.signal,excludePaths:i.excludePaths,maxScopeFiles:a}),l=()=>i.onProgress?.({type:`fix:budget-exhausted`,spent:i.budget?.spent()??0,limit:i.budget?.limit()??0}),u={model:i.model,fileGuidance:r.fileGuidance,parser:i.parser,validator:i.validator,config:i.config,onProgress:i.onProgress,signal:i.signal,budget:i.budget,onBudgetExhausted:()=>i.budget?.notifyExhaustedOnce(l)};await C(t,s,u);let d=s?await w(t,r,s,o,u,i):await n(t,r,o),{set:f,changed:p}=await T(t,s,u,i);return p&&(d=await n(t,r,o)),c(d,f)}function S(e){return e.strategy===`ai`?!0:e.strategy===`take-ours`||e.strategy===`take-theirs`?!e.fallback&&!e.deliberateTake:!1}async function C(e,t,n){for(let r=0;r<e.length;r++){let i=e[r];if(S(i)){if(n.signal?.aborted)return;try{e[r]=await l(i,E(t,i.filePath),n)}catch(e){if(n.signal?.aborted)throw e;n.onProgress?.({type:`fix:failed`,filePath:i.filePath,reason:e instanceof Error?e.message:String(e)})}}}}async function w(e,t,r,i,a,o){let s=o.config?.crossFileMaxIterations??2,c=await n(e,t,i);for(let l=0;l<s&&!o.signal?.aborted;l++){if(o.budget?.exhausted()){a.onBudgetExhausted?.();break}let s=O(c.findings,e,`project`);if(s.size===0)break;let l=!1;for(let[t,n]of s){if(o.budget?.exhausted()){a.onBudgetExhausted?.();break}let i=e.findIndex(e=>e.filePath===t);if(i<0)continue;let s=e[i],c=s.content;try{let o=await D(s,n,E(r,t),r.result,a);e[i]=o,o.content!==c&&(l=!0)}catch(e){if(o.signal?.aborted)throw e;o.onProgress?.({type:`fix:failed`,filePath:t,reason:e instanceof Error?e.message:String(e)})}}if(!l)break;c=await n(e,t,i)}return c}async function T(e,t,n,r){let i={validator:r.validator,signal:r.signal,onProgress:r.onProgress},a=r.config?.crossFileMaxIterations??2,o=await s(e,t,i),c=!1;for(let l=0;l<a&&!(o.status!==`ran`||r.signal?.aborted);l++){if(r.budget?.exhausted()){n.onBudgetExhausted?.();break}let a=O(o.findings,e);if(a.size===0)break;r.onProgress?.({type:`fix:set-validation-round`,round:l+1,findingCount:o.findings.length,routedFiles:a.size});let u=!1;for(let[i,o]of a){if(r.budget?.exhausted()){n.onBudgetExhausted?.();break}let a=e.findIndex(e=>e.filePath===i);if(a<0)continue;let s=e[a];try{let r=await D(s,o,E(t,i),t?.result??new Map,n);e[a]=r,r.content!==s.content&&(u=!0)}catch(e){if(r.signal?.aborted)throw e;r.onProgress?.({type:`fix:failed`,filePath:i,reason:e instanceof Error?e.message:String(e)})}}if(!u)break;c=!0,o=await s(e,t,i)}return{set:o,changed:c}}function E(e,t){return{ours:e?.ours.get(t),theirs:e?.theirs.get(t)}}async function D(e,n,s,c,l){let u={parser:l.parser,validator:l.validator,signal:l.signal,onProgress:l.onProgress},d=await t(e.filePath,s,e.content,u),m=l.parser?await k(n,c,l.parser,l.signal):void 0,g=await a(e.content,e.description,h(n),{model:l.model,signal:l.signal,temperature:l.config?.temperature,refineMaxSteps:l.config?.refineMaxSteps,modelCallTimeoutMs:l.config?.modelCallTimeoutMs,onProgress:l.onProgress,filePath:e.filePath,fileGuidance:i(l.fileGuidance,[e.filePath,...e.retiredPaths??[]]),budget:l.budget,relatedDefinitions:m});if(g.edits.length===0)return e;if(!r(e.filePath,g.content).valid)return l.onProgress?.({type:`refine:discarded`,filePath:e.filePath,reason:`Cross-file fix introduced conflict markers.`}),e;let _=await t(e.filePath,s,g.content,u);return b(d,_)?(l.onProgress?.({type:`refine:discarded`,filePath:e.filePath,reason:`Cross-file fix left the file with a new or additional error.`}),e):p({...e,content:g.content,strategy:`ai`,edits:[...e.edits??[],...g.edits],confidence:Math.min(e.confidence,o.refineApplied)},f(e,d),_)}function O(e,t,n){let r=new Set(t.filter(S).map(e=>e.filePath)),i=new Map;for(let t of e){if(n&&t.tier!==n||t.severity!==`error`||!r.has(t.location.file))continue;let e=i.get(t.location.file)??[];e.push(t),i.set(t.location.file,e)}return i}async function k(e,t,n,r){let i=new Set;for(let n of e){let e=n.definitionFile;e&&t.has(e)&&i.add(e)}if(i.size===0)return;let a=[];for(let e of i){let i=t.get(e);if(i==null)continue;let o=(await n.analyze(new Map([[e,i]]),r)).get(e);if(!o||o.definitions.length===0)continue;let s=o.definitions.slice(0,50).map(e=>{let t=_(e.name),n=e.kind?_(e.kind):void 0;return n?`${t} (${n}, line ${e.line})`:`${t} (line ${e.line})`});a.push({file:_(e),symbols:s})}return a.length>0?a:void 0}export{S as isFixable,E as parentsOf,l as runFixLoop,x as runFixPhase};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{ConflictGitPortMissingOpError as e}from"../ports/git.js";import{log as t,mergeBase as n}from"../git/port-dispatch.js";import{toFileFinding as r}from"../validation/findings.js";import{resolveChunkContent as i}from"../resolution/chunk-apply.js";import{createResolution as a}from"../resolution/create-resolution.js";import{mergeUsages as o}from"../resolution/metrics.js";import{checkResidualMarkers as s}from"../validation/markers.js";import{bracketImbalances as c,bracketInstruction as l,fileBracketFinding as u}from"./bracket-balance.js";import{matchFileGuidance as d}from"./file-guidance.js";import{runResolverLoop as f}from"./loop.js";import{lostAdditionFinding as p,lostAdditions as m,refineInstructions as h}from"./lost-additions.js";import{runRefinePass as g}from"./refine-pass.js";import{buildThreeWayDiff as _}from"./three-way-diff.js";const v={refineNoAction:.6,refineApplied:.9,errorsRemain:.5};async function y(e,t,n,y=[e.filePath]){let x={...t,fileGuidance:d(t.fileGuidance,y)},S=x;if(x.refs){let t=x.operation!==void 0&&x.upstreamCommits===void 0,r=x.threeWayDiff??await _(e.filePath,x.refs,{git:n.git,signal:n.signal}),i=t?await b(e.filePath,x.refs,x.operation===`merge`?x.refs.theirs:x.refs.ours,n):x.upstreamCommits;S={...x,threeWayDiff:r,...i===void 0?{}:{upstreamCommits:i}}}let{chunks:C,strategy:w,confidence:T,description:E,metrics:D,followUpInstructions:O}=await f(e,S,{model:n.model,git:n.git,config:n.config,onProgress:n.onProgress,signal:n.signal}),k=a(e,C,{confidence:T,description:E,metrics:D,...w?{fileStrategy:w}:{},...S.threeWayDiff?{threeWayDiff:S.threeWayDiff}:{}}),A=(t,r)=>{let i=S.threeWayDiff;if(e.type!==`text`||!i)return[];let a=[...m(i.oursDiff,`ours`,r),...m(i.theirsDiff,`theirs`,r)];for(let r of a)n.onProgress?.({type:`refine:lost-addition`,filePath:e.filePath,side:r.side,startLine:r.startLine,endLine:r.endLine,lines:r.lines.length,phase:t});return a},j=e.type===`text`?c(C,e):[];for(let t of j)n.onProgress?.({type:`refine:bracket-imbalance`,filePath:e.filePath,...t});let M=h(O,A(`before-refine`,k.content),j.map(e=>l(e,S.operation)));if(e.type===`text`&&M.length>0){n.onProgress?.({type:`refine:started`,filePath:e.filePath});let t=0;try{let r=await g(k.content,k.description,M,{model:n.model,signal:n.signal,temperature:n.config?.temperature,refineMaxSteps:n.config?.refineMaxSteps,modelCallTimeoutMs:n.config?.modelCallTimeoutMs,fileGuidance:S.fileGuidance,onProgress:n.onProgress,filePath:e.filePath});if(r.edits.length>0){let i=s(e.filePath,r.content);if(i.valid)k={...k,content:r.content,edits:r.edits,strategy:`ai`},t=r.edits.length;else{let t=(i.issues??[]).map(e=>e.message).join(` `);n.onProgress?.({type:`refine:discarded`,filePath:e.filePath,reason:`Edits introduced conflict markers: ${t}`})}}if(t>0){k={...k,confidence:Math.min(k.confidence,v.refineApplied)};let t=[];for(let n=0;n<e.markers.length;n++){let a=e.markers[n];if(!a||a.userResolution==null)continue;let o=i({markerIndex:n,...a.userResolution},a);o!==``&&(r.content.includes(o)||t.push(n))}t.length>0&&(n.onProgress?.({type:`refine:touched-user-region`,filePath:e.filePath,markerIndexes:t}),k={...k,note:`Refine modified user-resolved region(s): marker ${t.join(`, `)}.`})}else k={...k,confidence:Math.min(k.confidence,v.refineNoAction)};if(r.usage){let e=o(k.metrics??{inputTokens:0,outputTokens:0},r.usage);k={...k,metrics:{...k.metrics,...e}}}n.onProgress?.({type:`refine:completed`,filePath:e.filePath,appliedCount:t})}catch(t){if(n.signal?.aborted)throw t;let r=t instanceof Error?t.message:String(t);n.onProgress?.({type:`refine:failed`,filePath:e.filePath,reason:r}),k={...k,confidence:Math.min(k.confidence,v.refineNoAction)}}}else n.onProgress?.({type:`refine:skipped`,filePath:e.filePath,reason:`no-follow-ups`});let N=A(`after-refine`,k.content);N.length>0&&(k={...k,findings:[...k.findings??[],...N.map(t=>p(t,e.filePath))]});let P=e.type===`text`?u(e,k.content,S.operation,j):void 0;if(P&&(k={...k,findings:[...k.findings??[],P]}),n.validator&&e.type===`text`)try{let t=await n.validator.validate(new Map([[e.filePath,k.content]]),n.signal);t.length>0&&(k={...k,findings:[...k.findings??[],...t.map(r)]})}catch(t){if(n.signal?.aborted)throw t;let r=t instanceof Error?t.message:String(t);n.onProgress?.({type:`validation:failed`,filePath:e.filePath,reason:r})}return n.onProgress?.({type:`resolver:completed`,filePath:e.filePath,stepCount:D.stepCount??0}),k}async function b(r,i,a,o){try{let e=i.base??await n(o.git,i.ours,i.theirs,{signal:o.signal});return await t(o.git,{ref:`${e}..${a}`,path:r,signal:o.signal})}catch(t){if(o.signal?.aborted||!(t instanceof e))throw t;return}}export{v as CONFIDENCE_CAPS,y as resolveConflict};
|
|
1
|
+
import{ConflictGitPortMissingOpError as e}from"../ports/git.js";import{log as t,mergeBase as n}from"../git/port-dispatch.js";import{toFileFinding as r}from"../validation/findings.js";import{resolveChunkContent as i}from"../resolution/chunk-apply.js";import{createResolution as a}from"../resolution/create-resolution.js";import{mergeUsages as o}from"../resolution/metrics.js";import{checkResidualMarkers as s}from"../validation/markers.js";import{bracketImbalances as c,bracketInstruction as l,fileBracketFinding as u}from"./bracket-balance.js";import{matchFileGuidance as d}from"./file-guidance.js";import{runResolverLoop as f}from"./loop.js";import{lostAdditionFinding as p,lostAdditions as m,refineInstructions as h}from"./lost-additions.js";import{runRefinePass as g}from"./refine-pass.js";import{buildThreeWayDiff as _}from"./three-way-diff.js";const v={refineNoAction:.6,refineApplied:.9,errorsRemain:.5};async function y(e,t,n,y=[e.filePath]){let x={...t,fileGuidance:d(t.fileGuidance,y)},S=x;if(x.refs){let t=x.operation!==void 0&&x.upstreamCommits===void 0,r=x.threeWayDiff??await _(e.filePath,x.refs,{git:n.git,signal:n.signal}),i=t?await b(e.filePath,x.refs,x.operation===`merge`?x.refs.theirs:x.refs.ours,n):x.upstreamCommits;S={...x,threeWayDiff:r,...i===void 0?{}:{upstreamCommits:i}}}let{chunks:C,strategy:w,confidence:T,description:E,metrics:D,followUpInstructions:O}=await f(e,S,{model:n.model,git:n.git,config:n.config,onProgress:n.onProgress,signal:n.signal}),k=a(e,C,{confidence:T,description:E,metrics:D,...w?{fileStrategy:w}:{},...S.threeWayDiff?{threeWayDiff:S.threeWayDiff}:{}}),A=(t,r)=>{let i=S.threeWayDiff;if(e.type!==`text`||!i)return[];let a=[...m(i.oursDiff,`ours`,r),...m(i.theirsDiff,`theirs`,r)];for(let r of a)n.onProgress?.({type:`refine:lost-addition`,filePath:e.filePath,side:r.side,startLine:r.startLine,endLine:r.endLine,lines:r.lines.length,phase:t});return a},j=e.type===`text`?c(C,e):[];for(let t of j)n.onProgress?.({type:`refine:bracket-imbalance`,filePath:e.filePath,...t});let M=h(O,A(`before-refine`,k.content),j.map(e=>l(e,S.operation)));if(e.type===`text`&&M.length>0){n.onProgress?.({type:`refine:started`,filePath:e.filePath});let t=0;try{let r=await g(k.content,k.description,M,{model:n.model,signal:n.signal,temperature:n.config?.temperature,refineMaxSteps:n.config?.refineMaxSteps,modelCallTimeoutMs:n.config?.modelCallTimeoutMs,fileGuidance:S.fileGuidance,onProgress:n.onProgress,filePath:e.filePath});if(r.edits.length>0){let i=s(e.filePath,r.content);if(i.valid)k={...k,content:r.content,edits:r.edits,strategy:`ai`},t=r.edits.length;else{let t=(i.issues??[]).map(e=>e.message).join(` `);n.onProgress?.({type:`refine:discarded`,filePath:e.filePath,reason:`Edits introduced conflict markers: ${t}`})}}if(t>0){k={...k,confidence:Math.min(k.confidence,v.refineApplied)};let t=[];for(let n=0;n<e.markers.length;n++){let a=e.markers[n];if(!a||a.userResolution==null)continue;let o=i({markerIndex:n,...a.userResolution},a);o!==``&&(r.content.includes(o)||t.push(n))}t.length>0&&(n.onProgress?.({type:`refine:touched-user-region`,filePath:e.filePath,markerIndexes:t}),k={...k,note:`Refine modified user-resolved region(s): marker ${t.join(`, `)}.`})}else k={...k,confidence:Math.min(k.confidence,v.refineNoAction)};if(r.usage){let e=o(k.metrics??{inputTokens:0,outputTokens:0},r.usage);k={...k,metrics:{...k.metrics,...e}}}n.onProgress?.({type:`refine:completed`,filePath:e.filePath,appliedCount:t})}catch(t){if(n.signal?.aborted)throw t;let r=t instanceof Error?t.message:String(t);n.onProgress?.({type:`refine:failed`,filePath:e.filePath,reason:r}),k={...k,confidence:Math.min(k.confidence,v.refineNoAction)}}}else n.onProgress?.({type:`refine:skipped`,filePath:e.filePath,reason:`no-follow-ups`});let N=A(`after-refine`,k.content);N.length>0&&(k={...k,findings:[...k.findings??[],...N.map(t=>p(t,e.filePath))]});let P=e.type===`text`?u(e,k.content,S.operation,j):void 0;if(P&&(k={...k,findings:[...k.findings??[],P]}),n.validator&&e.type===`text`)try{let t=await n.validator.validate(new Map([[e.filePath,k.content]]),n.signal,{scope:`file`,role:`result`});Array.isArray(t)?t.length>0&&(k={...k,findings:[...k.findings??[],...t.map(r)]}):n.onProgress?.({type:`validation:not-run`,filePath:e.filePath,scope:`file`,reason:t.reason})}catch(t){if(n.signal?.aborted)throw t;let r=t instanceof Error?t.message:String(t);n.onProgress?.({type:`validation:failed`,filePath:e.filePath,scope:`file`,reason:r})}return n.onProgress?.({type:`resolver:completed`,filePath:e.filePath,stepCount:D.stepCount??0}),k}async function b(r,i,a,o){try{let e=i.base??await n(o.git,i.ours,i.theirs,{signal:o.signal});return await t(o.git,{ref:`${e}..${a}`,path:r,signal:o.signal})}catch(t){if(o.signal?.aborted||!(t instanceof e))throw t;return}}export{v as CONFIDENCE_CAPS,y as resolveConflict};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{AIError as e}from"../errors/ai.js";import{ConflictError as t}from"../errors/conflict.js";import{mergeAttributes as n,readFile as r,unmergedEntries as i,unmergedStages as a}from"../git/port-dispatch.js";import{extractConflict as o,extractConflictWithSource as s}from"../extraction/extract.js";import{duplicateBlockFindings as c}from"../resolution/duplicate-blocks.js";import{buildSnapshots as l}from"../reference-integrity/snapshot.js";import{detect as u}from"../reference-integrity/detect.js";import{createResolution as d}from"../resolution/create-resolution.js";import{detectFallbackCodeLoss as f}from"../resolution/detect-code-loss.js";import{takeOurs as p,takeTheirs as m}from"../resolution/take-strategy.js";import{buildPathMatcher as h,guidanceNamesFile as g,matchFileGuidance as _}from"./file-guidance.js";import{resolveConflict as v}from"./resolve-conflict.js";import{
|
|
1
|
+
import{AIError as e}from"../errors/ai.js";import{ConflictError as t}from"../errors/conflict.js";import{mergeAttributes as n,readFile as r,unmergedEntries as i,unmergedStages as a}from"../git/port-dispatch.js";import{extractConflict as o,extractConflictWithSource as s}from"../extraction/extract.js";import{duplicateBlockFindings as c}from"../resolution/duplicate-blocks.js";import{buildSnapshots as l}from"../reference-integrity/snapshot.js";import{detect as u}from"../reference-integrity/detect.js";import{createResolution as d}from"../resolution/create-resolution.js";import{detectFallbackCodeLoss as f}from"../resolution/detect-code-loss.js";import{takeOurs as p,takeTheirs as m}from"../resolution/take-strategy.js";import{buildPathMatcher as h,guidanceNamesFile as g,matchFileGuidance as _}from"./file-guidance.js";import{resolveConflict as v}from"./resolve-conflict.js";import{validateResolvedSet as y,withSetValidation as b}from"../reference-integrity/validate-set.js";import{classifiedResolution as x,deleteModifyResolution as S,noMarkerResolution as C}from"./entry-decision.js";import{isFixable as w,parentsOf as T,runFixPhase as E}from"./fix-loop.js";import{classifyUnmergedEntries as D,loneSideStageMode as O}from"./mechanical.js";import{createModelBudget as k}from"./model-budget.js";import{chooseSurvivingName as A}from"./rename-choice.js";import{findRenameRenameGroup as j}from"./rename-group.js";function M(e){let t=e.map(e=>({isMatch:h(e.match),strategy:e.strategy}));return e=>{for(let{isMatch:n,strategy:r}of t)if(n(e))return r}}async function N(e,t){let r=e.config?.rules??[],a=e.config?.defaultStrategy??`ai`,o=M(r),c=await F(await i(e.git,{signal:e.signal}),e),l=D(c),u=k(e.config?.maxModelCalls),f=u.wrap(e.model),h={...t??{}},_=[],v=[],y=[],b=new Set,w,T=()=>(w??=n(e.git,c.map(e=>e.path),{signal:e.signal}).catch(()=>new Map),w),E=j(c),A=E?[E.basePath,E.oursPath,E.theirsPath]:[],N=E!==null&&A.every(e=>(o(e)??a)===`ai`),B=new Set;if(E&&N&&!e.signal?.aborted&&await L(E,e)){await R(E,c,h,{...e,model:f},{resolutions:_,errors:v,skipped:y});for(let e of A)B.add(e)}for(let t of c){if(e.signal?.aborted)break;if(B.has(t.path))continue;let n=o(t.path)??a;if(n===`skip`){y.push({filePath:t.path,reason:`excluded`}),e.onProgress?.({type:`conflict:excluded`,filePath:t.path});continue}let r=l.get(t.path),i=x(t.path,r,n!==`ai`);if(i){P(i,_,e),i.strategy===`mechanical`&&t.stages!=null&&O(t.stages)===`120000`&&b.add(t.path);continue}if(n===`ai`&&r?.kind===`skip`){y.push({filePath:t.path,reason:r.reason,entryReason:t.reason,stages:t.stages}),e.onProgress?.({type:`conflict:skipped`,filePath:t.path,reason:r.reason,entryReason:t.reason});continue}let c,u;try{({conflict:c,rawContent:u}=await s(t.path,{git:e.git,signal:e.signal},t.reason))}catch(n){let r=U(n);v.push({filePath:t.path,error:r,reason:H(r,e.signal)});continue}if(!c){if(n===`take-ours`||n===`take-theirs`||n===`deleted`){let r=C(t.path,n,t.stages);P({...r,...r.strategy===`deleted`?{}:{deliberateTake:!0}},_,e);continue}let r=await V(t,u,T)?`unmergeable`:`no-markers`;y.push({filePath:t.path,reason:r,entryReason:t.reason,stages:t.stages}),e.onProgress?.({type:`conflict:skipped`,filePath:t.path,reason:r,entryReason:t.reason});continue}e.onProgress?.({type:`conflict:found`,filePath:c.filePath,conflictType:c.type,markerCount:c.markers.length});let w=null;w=n===`take-ours`?{...p(c),deliberateTake:!0}:n===`take-theirs`?{...m(c),deliberateTake:!0}:n===`deleted`?d(c,[],{confidence:1,fileStrategy:`deleted`}):c.type===`delete-modify`&&!g(c.filePath,h)?await S(c,h,e):await z(c,h,{...e,model:f},v),w&&(h.previousResolutions=[...h.previousResolutions??[],w],P(w,_,e))}return{resolutions:_,errors:v,skipped:y,detection:await I(_,h,y,v,b,u,{...e,model:f})}}function P(e,t,n){t.push(e),n.onProgress?.({type:`resolution:applied`,filePath:e.filePath,strategy:e.strategy,confidence:e.confidence,description:e.description})}async function F(e,t){if(!t.git.exec||!e.some(e=>e.stages===void 0))return e;let n=await a(t.git,{signal:t.signal});return e.map(e=>{if(e.stages)return e;let t=n.get(e.path);return t?{...e,stages:t}:e})}async function I(e,t,n,r,i,a,o){let s=new Set([...n.map(e=>e.filePath),...r.map(e=>e.filePath),...e.flatMap(e=>e.retiredPaths??[]),...i]),d={git:o.git,parser:o.parser,signal:o.signal,excludePaths:s,maxScopeFiles:o.config?.maxDetectionScopeFiles},f;if(o.signal?.aborted)f={findings:[],checks:{referenceIntegrity:`errored`,structural:`errored`,validator:`errored`}};else if(o.config?.fix)try{f=await E(e,t,{model:o.model,git:o.git,parser:o.parser,validator:o.validator,config:o.config,onProgress:o.onProgress,signal:o.signal,budget:a,excludePaths:s})}catch(n){if(o.signal?.aborted)throw n;o.onProgress?.({type:`fix:failed`,reason:U(n).message}),f=await u(e,t,d),f={...f,checks:{referenceIntegrity:`errored`,structural:`errored`,validator:o.validator?`errored`:`skipped-no-validator`}}}else{f=await u(e,t,d);let n=await l(e,t,d);for(let t of e.filter(w))f.findings.push(...c(t.filePath,t.content,T(n,t.filePath)));f=b(f,await y(e,n,{validator:o.validator,signal:o.signal,onProgress:o.onProgress}))}let p=e.flatMap(e=>e.findings??[]);return{...f,findings:W([...f.findings,...p])}}async function L(e,t){try{return await r(t.git,e.oursPath,{signal:t.signal})===await r(t.git,e.theirsPath,{signal:t.signal})}catch(e){if(t.signal?.aborted)throw U(e);return!1}}async function R(e,t,n,r,i){let a=e=>{for(let n of e){let e=t.find(e=>e.path===n);i.skipped.push({filePath:n,reason:`rename-choice`,entryReason:e?.reason,stages:e?.stages}),r.onProgress?.({type:`conflict:skipped`,filePath:n,reason:`rename-choice`,entryReason:e?.reason})}},s={...n,fileGuidance:_(n.fileGuidance,[e.basePath,e.oursPath,e.theirsPath])},c;try{c=await A(e,s,{model:r.model,config:r.config,signal:r.signal})}catch(t){if(r.signal?.aborted)throw U(t);a([e.basePath,e.oursPath,e.theirsPath]);return}let l=c.path,u=[l===e.oursPath?e.theirsPath:e.oursPath,e.basePath],d=t.find(e=>e.path===l),f;try{f=await o(l,{git:r.git,signal:r.signal},d?.reason)}catch(e){if(r.signal?.aborted)throw U(e);let t=U(e);i.errors.push({filePath:l,error:t,reason:H(t,r.signal)}),a(u);return}let p;if(!f)p={filePath:l,content:``,strategy:`mechanical`,confidence:c.confidence,description:c.description??`Both sides renamed ${e.basePath}; kept ${l}. Git settled the content itself, which is staged unchanged.`,retiredPaths:u};else{r.onProgress?.({type:`conflict:found`,filePath:f.filePath,conflictType:f.type,markerCount:f.markers.length});let t=await z(f,n,r,i.errors,[e.basePath,e.oursPath,e.theirsPath]);if(!t){a(u);return}let o=[t.note,c.description].filter(e=>!!e).join(` `);p={...t,confidence:Math.min(t.confidence,c.confidence),retiredPaths:u,...o?{note:o}:{}}}i.resolutions.push(p),n.previousResolutions=[...n.previousResolutions??[],p],r.onProgress?.({type:`resolution:applied`,filePath:p.filePath,strategy:p.strategy,confidence:p.confidence,description:p.description})}async function z(n,r,i,a,o){let s,c={generate:async e=>{try{return await i.model.generate(e)}catch(e){throw s=e,e}}};try{return await v(n,r,{model:c,git:i.git,config:i.config,onProgress:i.onProgress,signal:i.signal,validator:i.validator},o)}catch(r){let o=U(r);if(i.signal?.aborted)throw o;let c=r===s||o instanceof DOMException&&o.name===`TimeoutError`?`model-error`:o instanceof e&&o.code===`VALIDATION_EXHAUSTED`?`validation-exhausted`:`unknown`,l=i.config?.fallbackStrategy;if(!l)return a.push({filePath:n.filePath,error:o,reason:c}),i.onProgress?.({type:`resolution:failed`,filePath:n.filePath,error:o}),null;let u=i.config?.allowLossyFallback?null:f(n,l);if(u){let e=new t(`CODE_LOSS_RISK`,`AI resolution failed and the '${l}' fallback would take one side at every conflict marker in ${n.filePath} without a decision, discarding the ${u.discardedSide} side's conflicting changes. Stopping for manual resolution. Resolver failure: ${o.message}`,{cause:o});return a.push({filePath:n.filePath,error:e,reason:`code-loss-risk`,causeReason:c}),i.onProgress?.({type:`resolution:failed`,filePath:n.filePath,error:e}),null}return i.onProgress?.({type:`resolution:fallback`,filePath:n.filePath,error:o,cause:o instanceof e?o.code:void 0}),{...l===`take-ours`?p(n):m(n),confidence:0,fallback:!0}}}const B=new Set([`unspecified`,`set`,`text`]);async function V(e,t,n){if(e.stages?.stage2==null||e.stages?.stage3==null)return!1;if(t.includes(`\0`))return!0;let r=(await n()).get(e.path);return r!==void 0&&!B.has(r)}function H(e,n){return n?.aborted?`aborted`:e instanceof t&&e.code===`PARSE_FAILED`?`parse-failed`:`read-failed`}function U(e){return e instanceof Error?e:Error(String(e))}function W(e){let t=new Set,n=[];for(let r of e){let{category:e,location:i}=r,a=`${e}\u0000${i.file}\u0000${i.line}\u0000${i.symbol??``}`;t.has(a)||(t.add(a),n.push(r))}return n}export{N as resolveConflicts};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{buildSummaryPrompt as e}from"./prompt.js";const t=.8;function n(e,n=t){let s={ai:0,"take-ours":0,"take-theirs":0,deleted:0,skipped:0,mechanical:0},c=0,l=null,u=new Map;for(let t of e.resolutions)s[t.strategy]++,c+=t.chunks?.length??0,l=l===null?t.confidence:Math.min(l,t.confidence),t.confidence<n&&u.set(t.filePath,{filePath:t.filePath,confidence:t.confidence,reason:`low-confidence`});let d=e.detection,f=d?.findings??[],p=new Map,m=!1;for(let e of f){e.severity===`error`&&(m=!0);let t=p.get(e.location.file)??new Set;t.add(e.category),p.set(e.location.file,t)}for(let[e,t]of p){let n=u.get(e);u.set(e,{filePath:e,...n?.confidence==null?{}:{confidence:n.confidence},reason:`finding`,findingCategories:[...t].sort()})}for(let t of e.skipped??[])t.reason!==`excluded`&&(u.has(t.filePath)||u.set(t.filePath,{filePath:t.filePath,reason:`unresolved`}));let h=[...u.values()].sort(a),g=d!=null&&r(d.checks.referenceIntegrity)&&r(d.checks.structural)&&i(d.scope),_=o({hasErrorFinding:m,hasWarningFinding:f.some(e=>e.severity===`warning`),needsReview:h.length>0,detectionComplete:g,hasResolutionErrors:e.errors.length>0});return{files:e.resolutions.length,conflicts:c,byStrategy:s,minConfidence:l,needsReview:h,skipped:e.skipped?.length??0,errors:e.errors.length,detectionComplete:g,findingCount:f.length,verdict:_}}function r(e){return e===`ran`||e===`partial-unsupported-language`}function i(e){return e?!e.truncated&&e.incomingDiscovered:!0}function a(e,t){return e.reason===t.reason?(e.confidence??1)-(t.confidence??1):e.reason===`finding`?-1:1}function o(e){return e.hasErrorFinding?`blocked`:e.needsReview||!e.detectionComplete||e.hasResolutionErrors||e.hasWarningFinding?`needs-review`:`clean`}async function s(r,i={}){let a=n(r,i.reviewThreshold??t);if(!i.model)return{digest:a};let{system:o,userMessage:s}=e(r,a),c=await i.model.generate({system:o,messages:[{role:`user`,content:s}],temperature:i.temperature??0,signal:i.signal}),l=c.text?.trim();return{digest:a,...l?{prose:l}:{},...c.usage?{usage:c.usage}:{}}}export{n as buildOperationDigest,s as summarizeOperation};
|
|
1
|
+
import{buildSummaryPrompt as e}from"./prompt.js";const t=.8;function n(e,n=t){let s={ai:0,"take-ours":0,"take-theirs":0,deleted:0,skipped:0,mechanical:0},c=0,l=null,u=new Map;for(let t of e.resolutions)s[t.strategy]++,c+=t.chunks?.length??0,l=l===null?t.confidence:Math.min(l,t.confidence),t.confidence<n&&u.set(t.filePath,{filePath:t.filePath,confidence:t.confidence,reason:`low-confidence`});let d=e.detection,f=d?.findings??[],p=new Map,m=!1;for(let e of f){e.severity===`error`&&(m=!0);let t=p.get(e.location.file)??new Set;t.add(e.category),p.set(e.location.file,t)}for(let[e,t]of p){let n=u.get(e);u.set(e,{filePath:e,...n?.confidence==null?{}:{confidence:n.confidence},reason:`finding`,findingCategories:[...t].sort()})}for(let t of e.skipped??[])t.reason!==`excluded`&&(u.has(t.filePath)||u.set(t.filePath,{filePath:t.filePath,reason:`unresolved`}));let h=[...u.values()].sort(a),g=d!=null&&r(d.checks.referenceIntegrity)&&r(d.checks.structural)&&(d.checks.validator===`ran`||d.checks.validator===`skipped-no-validator`)&&i(d.scope),_=o({hasErrorFinding:m,hasWarningFinding:f.some(e=>e.severity===`warning`),needsReview:h.length>0,detectionComplete:g,hasResolutionErrors:e.errors.length>0});return{files:e.resolutions.length,conflicts:c,byStrategy:s,minConfidence:l,needsReview:h,skipped:e.skipped?.length??0,errors:e.errors.length,detectionComplete:g,validation:d?.checks.validator??`skipped-no-validator`,...d?.validatorReason?{validationReason:d.validatorReason}:{},findingCount:f.length,verdict:_}}function r(e){return e===`ran`||e===`partial-unsupported-language`}function i(e){return e?!e.truncated&&e.incomingDiscovered:!0}function a(e,t){return e.reason===t.reason?(e.confidence??1)-(t.confidence??1):e.reason===`finding`?-1:1}function o(e){return e.hasErrorFinding?`blocked`:e.needsReview||!e.detectionComplete||e.hasResolutionErrors||e.hasWarningFinding?`needs-review`:`clean`}async function s(r,i={}){let a=n(r,i.reviewThreshold??t);if(!i.model)return{digest:a};let{system:o,userMessage:s}=e(r,a),c=await i.model.generate({system:o,messages:[{role:`user`,content:s}],temperature:i.temperature??0,signal:i.signal}),l=c.text?.trim();return{digest:a,...l?{prose:l}:{},...c.usage?{usage:c.usage}:{}}}export{n as buildOperationDigest,s as summarizeOperation};
|
package/dist/types/events.d.ts
CHANGED
|
@@ -154,9 +154,20 @@ type ConflictProgressEvent = {
|
|
|
154
154
|
type: 'fix:budget-exhausted';
|
|
155
155
|
spent: number;
|
|
156
156
|
limit: number;
|
|
157
|
+
} | {
|
|
158
|
+
type: 'fix:set-validation-round';
|
|
159
|
+
round: number;
|
|
160
|
+
findingCount: number;
|
|
161
|
+
routedFiles: number;
|
|
157
162
|
} | {
|
|
158
163
|
type: 'validation:failed';
|
|
159
|
-
filePath
|
|
164
|
+
filePath?: string;
|
|
165
|
+
scope: 'file' | 'set';
|
|
166
|
+
reason: string;
|
|
167
|
+
} | {
|
|
168
|
+
type: 'validation:not-run';
|
|
169
|
+
filePath?: string;
|
|
170
|
+
scope: 'file' | 'set';
|
|
160
171
|
reason: string;
|
|
161
172
|
} | {
|
|
162
173
|
type: 'resolver:tool-result';
|
package/dist/types/findings.d.ts
CHANGED
|
@@ -34,12 +34,18 @@ interface Finding {
|
|
|
34
34
|
* `skipped-unreadable-source` means the parent snapshots came back empty (unreadable source), so the
|
|
35
35
|
* before/after comparison had nothing to compare against. */
|
|
36
36
|
type CheckStatus = 'ran' | 'skipped-no-parser' | 'skipped-no-refs' | 'skipped-unreadable-source' | 'partial-unsupported-language' | 'partial-no-cross-file-resolution' | 'errored';
|
|
37
|
+
/** Whether the consumer's validator ran on the whole resolved set. `not-run` means it answered that it
|
|
38
|
+
* did not; `errored` means it threw. */
|
|
39
|
+
type ValidatorCheckStatus = 'ran' | 'skipped-no-validator' | 'not-run' | 'errored';
|
|
37
40
|
interface DetectionReport {
|
|
38
41
|
findings: Finding[];
|
|
39
42
|
checks: {
|
|
40
43
|
referenceIntegrity: CheckStatus;
|
|
41
44
|
structural: CheckStatus;
|
|
45
|
+
validator: ValidatorCheckStatus;
|
|
42
46
|
};
|
|
47
|
+
/** Why the whole-set validator did not run or threw. Present with `checks.validator` `not-run` or `errored`. */
|
|
48
|
+
validatorReason?: string;
|
|
43
49
|
/** Honest coverage signal so empty findings are not read as "clean". Present only when the reference
|
|
44
50
|
* check ran (refs available). */
|
|
45
51
|
scope?: {
|
|
@@ -54,4 +60,4 @@ interface DetectionReport {
|
|
|
54
60
|
};
|
|
55
61
|
}
|
|
56
62
|
//#endregion
|
|
57
|
-
export { CheckStatus, DetectionReport, Finding, FindingCategory, FindingTier };
|
|
63
|
+
export { CheckStatus, DetectionReport, Finding, FindingCategory, FindingTier, ValidatorCheckStatus };
|
package/dist/types/summary.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ValidatorCheckStatus } from "./findings.js";
|
|
1
2
|
import { ResolutionStrategy } from "./resolution.js";
|
|
2
3
|
import { AiTokenUsage } from "@gitkraken/shared-tools";
|
|
3
4
|
|
|
@@ -45,6 +46,11 @@ interface OperationDigest {
|
|
|
45
46
|
* references, or part of the scope was never looked at — so empty findings must NOT be read as
|
|
46
47
|
* "clean". */
|
|
47
48
|
detectionComplete: boolean;
|
|
49
|
+
/** Whether the consumer's validator ran on the whole resolved set. `'skipped-no-validator'` when none
|
|
50
|
+
* was supplied or no detection was attached. */
|
|
51
|
+
validation: ValidatorCheckStatus;
|
|
52
|
+
/** Why the validator did not run or threw; see {@link validation}. */
|
|
53
|
+
validationReason?: string;
|
|
48
54
|
/** Total detection findings across the operation. */
|
|
49
55
|
findingCount: number;
|
|
50
56
|
/** Suggested honest verdict; see {@link OperationVerdict}. */
|