kaniscope 0.31.0 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/config.d.ts CHANGED
@@ -105,6 +105,8 @@ export interface ReviewConfig {
105
105
  prBody?: boolean;
106
106
  /** Cap on the description handed to the reviewer. A clipped one is marked truncated, so absence is not read as out-of-scope. Default: `12000`. (`PR_BODY_MAX_CHARS`) */
107
107
  prBodyMaxChars?: number;
108
+ /** Cap on the stated change intent handed to the reviewer on a local review. A clipped one is marked truncated, so absence is not read as out-of-scope. Default: `12000`. (`CHANGE_INTENT_MAX_CHARS`) */
109
+ changeIntentMaxChars?: number;
108
110
  /** Snap a finding that drifted just off a diff line onto the nearest diff line sharing its code symbol, instead of folding it into the summary. Default: `true`. (`REANCHOR_FINDINGS`) */
109
111
  reanchorFindings?: boolean;
110
112
  /** Re-review automatically when a PR gets new commits. Off by default: pushing is the inner loop, and every round costs a full review. Default: `false`. (`REVIEW_ON_UPDATE`) */
package/config.js CHANGED
@@ -53,6 +53,7 @@ const CONFIG_ENV = {
53
53
  prbotRunLog: ["PRBOT_RUN_LOG", "Path"],
54
54
  prBody: ["PR_BODY", "Bool"],
55
55
  prBodyMaxChars: ["PR_BODY_MAX_CHARS", "Int"],
56
+ changeIntentMaxChars: ["CHANGE_INTENT_MAX_CHARS", "Int"],
56
57
  reanchorFindings: ["REANCHOR_FINDINGS", "Bool"],
57
58
  reviewOnUpdate: ["REVIEW_ON_UPDATE", "Bool"],
58
59
  reviewSamples: ["REVIEW_SAMPLES", "Int"],
package/index.d.ts CHANGED
@@ -1,6 +1,17 @@
1
- import type { RunReviewOutput } from "./types";
1
+ import type {
2
+ RunReviewOutput,
3
+ FileReviewOutput,
4
+ EffectiveRules,
5
+ FindingsOutput,
6
+ ResolveOutput,
7
+ ExplainOutput,
8
+ ExplainInput,
9
+ } from "./types";
2
10
 
3
- export type { RunReviewOutput, Finding, InlineComment, Usage } from "./types";
11
+ // The WHOLE generated surface. A hand-kept list drifted from the Python
12
+ // client's — 22 names here against 10 there — so the same generated type was
13
+ // public in one language and private in the other. `export *` cannot fall behind.
14
+ export * from "./types";
4
15
 
5
16
  export type { ReviewConfig } from "./config";
6
17
 
@@ -49,6 +60,17 @@ export interface ReviewOptions extends SpawnOptions {
49
60
  repoRoot?: string;
50
61
  /** With {@link local}: what to call this change. Defaults to the branch name. */
51
62
  label?: string;
63
+ /**
64
+ * With {@link local}: what this change is MEANT to do — the task, or the
65
+ * instruction given to a coding agent. The reviewer checks the diff against
66
+ * it, the way it checks a PR against its description.
67
+ *
68
+ * Treated as untrusted data: fenced and labelled before it reaches the model,
69
+ * and unable to direct the review. Mutually exclusive with {@link intentFile}.
70
+ */
71
+ intent?: string;
72
+ /** With {@link local}: read {@link intent} from this file instead. */
73
+ intentFile?: string;
52
74
  /** Also write the result as JSON to this path, atomically. */
53
75
  jsonOut?: string;
54
76
  /**
@@ -71,8 +93,91 @@ export interface ReviewOptions extends SpawnOptions {
71
93
  */
72
94
  export function review(options?: ReviewOptions): Promise<RunReviewOutput>;
73
95
 
74
- /** The JSON Schema of a {@link review} result. Needs no key and no network. */
75
- export function schema(options?: SpawnOptions): Promise<Record<string, unknown>>;
96
+ /** Selects a checkout, or a pull request. Give one or the other, not both. */
97
+ export interface ScopeOptions extends SpawnOptions {
98
+ /** The checkout to resolve against. Defaults to the current directory. */
99
+ repoRoot?: string;
100
+ /** `github` | `gitlab` | `bitbucket`. With {@link repo} and {@link pr}. */
101
+ provider?: string;
102
+ repo?: string;
103
+ pr?: number;
104
+ }
105
+
106
+ export interface ReviewFileOptions extends ScopeOptions {
107
+ /** Repository-relative path of the file to review. Required. */
108
+ path: string;
109
+ }
110
+
111
+ /**
112
+ * The effective review rules: merged settings, which `.prbot.toml` was read,
113
+ * what it overrode, and the exact instructions injected into the system prompt.
114
+ *
115
+ * Makes no model call, so it needs no `OPENROUTER_API_KEY`. Never includes
116
+ * credentials — the result is built from an explicit allowlist of settings.
117
+ */
118
+ export function getRules(options?: ScopeOptions): Promise<EffectiveRules>;
119
+
120
+ /**
121
+ * Deep-review one complete file, in a checkout or at a PR head.
122
+ *
123
+ * Posts nothing. A path excluded by the repository's review filters comes back
124
+ * as an `excluded` outcome rather than an error, so it can be reported without
125
+ * being retried.
126
+ */
127
+ export function reviewFile(options: ReviewFileOptions): Promise<FileReviewOutput>;
128
+
129
+ /** PR coordinates. All three are required. */
130
+ export interface PrOptions extends SpawnOptions {
131
+ provider: string;
132
+ repo: string;
133
+ pr: number;
134
+ }
135
+
136
+ /**
137
+ * The findings currently on a pull request, with lifecycle state.
138
+ *
139
+ * Read-only. Check `outcome.status`: a provider that cannot track findings
140
+ * returns `unsupported` rather than an empty list, because "no open findings" is
141
+ * a conclusion and must never come from a question that was never asked.
142
+ */
143
+ export function getFindings(options: PrOptions): Promise<FindingsOutput>;
144
+
145
+ export interface ResolveOptions extends PrOptions {
146
+ /** Fingerprints to hand over. Omit for every active finding. */
147
+ fingerprints?: string[];
148
+ }
149
+
150
+ /**
151
+ * Package findings for your own edit loop.
152
+ *
153
+ * Changes nothing — no edits, no posts, no thread resolution. The name is the
154
+ * operation's, and the returned `disclaimer` says so in the payload.
155
+ */
156
+ export function resolveFindings(options: ResolveOptions): Promise<ResolveOutput>;
157
+
158
+ export interface ExplainOptions extends SpawnOptions {
159
+ /** The finding to investigate. */
160
+ finding: ExplainInput;
161
+ /** The checkout to read the file from. Defaults to the current directory. */
162
+ repoRoot?: string;
163
+ /** The commit the checkout is at, so a revision mismatch can be reported. */
164
+ headSha?: string;
165
+ }
166
+
167
+ /** Investigate one finding against a local checkout. Never posts. */
168
+ export function explainFinding(options: ExplainOptions): Promise<ExplainOutput>;
169
+
170
+ export interface SchemaOptions extends SpawnOptions {
171
+ /**
172
+ * Which operation's output schema to fetch — `review-file`, `get-rules`,
173
+ * `get-findings`, `resolve-findings`, `explain-finding`, `review-local`,
174
+ * `review-pr`. Omit for the review output's schema.
175
+ */
176
+ operation?: string;
177
+ }
178
+
179
+ /** The JSON Schema of an operation's result. Needs no key and no network. */
180
+ export function schema(options?: SchemaOptions): Promise<Record<string, unknown>>;
76
181
 
77
182
  /** The engine version this package's binary was built from. */
78
183
  export function version(options?: SpawnOptions): Promise<string>;
package/index.js CHANGED
@@ -24,6 +24,8 @@ const VALUE_FLAGS = {
24
24
  base: "--base",
25
25
  repoRoot: "--repo-root",
26
26
  label: "--label",
27
+ intent: "--intent",
28
+ intentFile: "--intent-file",
27
29
  jsonOut: "--json-out",
28
30
  };
29
31
 
@@ -218,11 +220,139 @@ async function review(options = {}) {
218
220
  /** The JSON Schema of a {@link review} result. Needs no key and no network. */
219
221
  async function schema(options = {}) {
220
222
  const bin = options.binary || binaryPath();
221
- const result = await run(bin, ["--schema"], options);
222
- if (result.code !== 0) throw new Error(`kaniscope --schema failed\n${tail(result.stderr)}`);
223
+ // `operation` selects a per-operation schema; without it, the review output's,
224
+ // which is what this function has always returned.
225
+ const args = options.operation ? ["schema", options.operation] : ["--schema"];
226
+ const result = await run(bin, args, options);
227
+ if (result.code !== 0) throw new Error(`kaniscope schema failed\n${tail(result.stderr)}`);
223
228
  return JSON.parse(result.stdout);
224
229
  }
225
230
 
231
+ /**
232
+ * Run one explicit toolbox operation and parse its single JSON document.
233
+ *
234
+ * Shared by the operations below so they cannot diverge in how they report a
235
+ * crash or a non-JSON stdout — the two failures a caller most needs told apart.
236
+ */
237
+ async function runOperation(op, args, options) {
238
+ const bin = options.binary || binaryPath();
239
+ const result = await run(bin, [op, ...args], options);
240
+ if (result.code !== 0) {
241
+ const how = result.signal ? `killed by ${result.signal}` : `exited ${result.code}`;
242
+ const err = new Error(`kaniscope ${op} ${how}\n${tail(result.stderr)}`);
243
+ err.exitCode = result.code;
244
+ err.signal = result.signal;
245
+ err.stderr = result.stderr;
246
+ throw err;
247
+ }
248
+ try {
249
+ return JSON.parse(result.stdout);
250
+ } catch (cause) {
251
+ const err = new Error(
252
+ `kaniscope ${op} exited 0 but stdout was not JSON — is KANISCOPE_BINARY_PATH ` +
253
+ `pointing at a different program?\n${tail(result.stdout, 5)}`
254
+ );
255
+ err.cause = cause;
256
+ throw err;
257
+ }
258
+ }
259
+
260
+ /**
261
+ * Scope flags shared by the operations that take a checkout OR a pull request.
262
+ *
263
+ * A PARTIAL scope is a caller error and is refused here. It used to stringify
264
+ * whatever was missing into the argv — `--provider undefined --repo o/r` — which
265
+ * the binary then rejected with a message about a provider named "undefined",
266
+ * pointing at the wrong thing entirely. Worse, before the CLI required the full
267
+ * trio it would have been read as a local request and answered for the wrong
268
+ * scope without any error at all.
269
+ */
270
+ function scopeArgs(options) {
271
+ const given = ["provider", "repo", "pr"].filter(
272
+ (k) => options[k] !== undefined && options[k] !== null
273
+ );
274
+ if (given.length === 0) {
275
+ return options.repoRoot ? ["--repo-root", String(options.repoRoot)] : [];
276
+ }
277
+ if (given.length !== 3) {
278
+ const missing = ["provider", "repo", "pr"].filter((k) => !given.includes(k));
279
+ throw new TypeError(
280
+ `kaniscope: a pull-request scope needs provider, repo and pr — missing ${missing.join(", ")}`
281
+ );
282
+ }
283
+ return ["--provider", String(options.provider), "--repo", String(options.repo), "--pr", String(options.pr)];
284
+ }
285
+
286
+ /**
287
+ * The effective review rules — merged settings, the repository's `.prbot.toml`,
288
+ * and the exact injected instructions.
289
+ *
290
+ * Makes no model call, so it needs no `OPENROUTER_API_KEY`. Credentials are
291
+ * never included in the result.
292
+ */
293
+ async function getRules(options = {}) {
294
+ return runOperation("get-rules", scopeArgs(options), options);
295
+ }
296
+
297
+ /** PR coordinates, required by the findings operations. */
298
+ function prArgs(options) {
299
+ for (const key of ["provider", "repo", "pr"]) {
300
+ if (options[key] === undefined || options[key] === null) {
301
+ throw new TypeError(`kaniscope: this operation needs \`${key}\``);
302
+ }
303
+ }
304
+ return ["--provider", String(options.provider), "--repo", String(options.repo), "--pr", String(options.pr)];
305
+ }
306
+
307
+ /**
308
+ * The findings currently on a pull request, with their lifecycle state.
309
+ *
310
+ * Read-only. Check `outcome.status` — a provider that cannot track findings
311
+ * returns `unsupported` rather than an empty list, because "no open findings" is
312
+ * a conclusion and must never come from a question that was never asked.
313
+ */
314
+ async function getFindings(options = {}) {
315
+ return runOperation("get-findings", prArgs(options), options);
316
+ }
317
+
318
+ /**
319
+ * Package findings for your own edit loop. Changes nothing: Kaniscope does not
320
+ * edit code, post, or resolve provider threads.
321
+ *
322
+ * Omit `fingerprints` to take every active finding.
323
+ */
324
+ async function resolveFindings(options = {}) {
325
+ const fps = (options.fingerprints || []).flatMap((fp) => ["--fingerprint", String(fp)]);
326
+ return runOperation("resolve-findings", [...prArgs(options), ...fps], options);
327
+ }
328
+
329
+ /**
330
+ * Investigate one finding against a local checkout. Never posts.
331
+ *
332
+ * The finding is passed as JSON on stdin rather than as a flag: a finding body
333
+ * is multi-line prose with quotes and backticks in it, which is exactly what an
334
+ * argv mangles.
335
+ */
336
+ async function explainFinding(options = {}) {
337
+ if (!options.finding) throw new TypeError("kaniscope: explainFinding needs a `finding`");
338
+ const args = ["--finding", "@-"];
339
+ if (options.repoRoot) args.push("--repo-root", String(options.repoRoot));
340
+ if (options.headSha) args.push("--head-sha", String(options.headSha));
341
+ // `diff` is how `run` decides to open the child's stdin and what to write
342
+ // there; the name is historical and the content here is the finding, not a
343
+ // diff. The caller never sets it — `@-` above is what reads it.
344
+ return runOperation("explain-finding", args, {
345
+ ...options,
346
+ diff: JSON.stringify(options.finding),
347
+ });
348
+ }
349
+
350
+ /** Deep-review one complete file, locally or at a PR head. Never posts. */
351
+ async function reviewFile(options = {}) {
352
+ if (!options.path) throw new TypeError("kaniscope: reviewFile needs a `path`");
353
+ return runOperation("review-file", ["--path", String(options.path), ...scopeArgs(options)], options);
354
+ }
355
+
226
356
  /** The engine version this package's binary was built from. */
227
357
  async function version(options = {}) {
228
358
  const bin = options.binary || binaryPath();
@@ -231,4 +361,14 @@ async function version(options = {}) {
231
361
  return result.stdout.trim().replace(/^kaniscope\s+/, "");
232
362
  }
233
363
 
234
- module.exports = { review, schema, version, binaryPath };
364
+ module.exports = {
365
+ review,
366
+ reviewFile,
367
+ getRules,
368
+ getFindings,
369
+ resolveFindings,
370
+ explainFinding,
371
+ schema,
372
+ version,
373
+ binaryPath,
374
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kaniscope",
3
- "version": "0.31.0",
3
+ "version": "0.32.0",
4
4
  "description": "AI pull-request reviewer — line-anchored inline comments plus a summary, for GitHub, GitLab and Bitbucket. Ships a native binary; no Rust toolchain required.",
5
5
  "keywords": [
6
6
  "code-review",
@@ -34,10 +34,10 @@
34
34
  "README.md"
35
35
  ],
36
36
  "optionalDependencies": {
37
- "@nhatvu148/kaniscope-darwin-arm64": "0.31.0",
38
- "@nhatvu148/kaniscope-darwin-x64": "0.31.0",
39
- "@nhatvu148/kaniscope-linux-arm64": "0.31.0",
40
- "@nhatvu148/kaniscope-linux-x64": "0.31.0",
41
- "@nhatvu148/kaniscope-win32-x64": "0.31.0"
37
+ "@nhatvu148/kaniscope-darwin-arm64": "0.32.0",
38
+ "@nhatvu148/kaniscope-darwin-x64": "0.32.0",
39
+ "@nhatvu148/kaniscope-linux-arm64": "0.32.0",
40
+ "@nhatvu148/kaniscope-linux-x64": "0.32.0",
41
+ "@nhatvu148/kaniscope-win32-x64": "0.32.0"
42
42
  }
43
43
  }
package/types.d.ts CHANGED
@@ -46,3 +46,312 @@ export interface Usage {
46
46
  total_tokens?: number | null;
47
47
  }
48
48
 
49
+ /** One file review. */
50
+ export interface FileReviewOutput {
51
+ outcome: FileReviewOutcome;
52
+ path: string;
53
+ source: FileSource;
54
+ /** The result as a comment body — what the PR command posts, and what a CLI caller prints. */
55
+ summaryMarkdown: string;
56
+ }
57
+
58
+ /** The file was read and reviewed. */
59
+ export interface FileReviewOutcomeReviewed {
60
+ /** After the confidence floor, severity sort and cap — the same policy a diff review applies, so a finding here means what it means there. */
61
+ findings: Finding[];
62
+ recommendation: string;
63
+ status: "reviewed";
64
+ summary: string;
65
+ }
66
+
67
+ /** The path is excluded by this repository's review file filters. */
68
+ export interface FileReviewOutcomeExcluded {
69
+ reason: string;
70
+ status: "excluded";
71
+ }
72
+
73
+ /** No such file at the resolved source. */
74
+ export interface FileReviewOutcomeNotFound {
75
+ reason: string;
76
+ status: "notFound";
77
+ }
78
+
79
+ /** What came of the request. */
80
+ export type FileReviewOutcome = FileReviewOutcomeReviewed | FileReviewOutcomeExcluded | FileReviewOutcomeNotFound;
81
+
82
+ /** A checkout on disk. */
83
+ export interface FileSourceLocal {
84
+ kind: "local";
85
+ repoRoot: string;
86
+ }
87
+
88
+ /** A host, at a specific ref — named, because "the file" is meaningless without saying which revision of it was read. */
89
+ export interface FileSourcePr {
90
+ gitRef: string;
91
+ kind: "pr";
92
+ pr: number;
93
+ provider: string;
94
+ repo: string;
95
+ }
96
+
97
+ /** Where the reviewed file was read from. */
98
+ export type FileSource = FileSourceLocal | FileSourcePr;
99
+
100
+ /** Everything that shapes one review, resolved and redacted. */
101
+ export interface EffectiveRules {
102
+ /** The exact text appended to the backend's system prompt — calibration rules, the suggestion rules when enabled, and the consumer's `EXTRA_PROMPT` with any repository `instructions` already merged in. */
103
+ injectedRules: string;
104
+ repoConfig: RepoConfigSource;
105
+ scope: RulesScope;
106
+ settings: ReviewSettings;
107
+ /** Anything that failed open on the way here. */
108
+ warnings: string[];
109
+ }
110
+
111
+ /** Per-repo review overrides parsed from a `.prbot.toml`. */
112
+ export interface RepoConfig {
113
+ agentic?: boolean | null;
114
+ /** Cap on the stated change intent handed to the reviewer on a local review, for this repo. */
115
+ change_intent_max_chars?: number | null;
116
+ /** Toggle fetching the head commit's CI results into the prompt for this repo. */
117
+ ci_status?: boolean | null;
118
+ /** Toggle the OSV.dev dependency vulnerability scan for this repo. */
119
+ cve_scan?: boolean | null;
120
+ /** Instructions shaping the `/describe` output specifically — a house PR description layout, release-notes sections, a contributor table. */
121
+ describe_instructions?: string | null;
122
+ exclude_globs?: string[] | null;
123
+ /** Toggle grouping related changed files (source + test, i18n siblings) when packing a large diff, for this repo. */
124
+ file_bundling?: boolean | null;
125
+ /** Let the agentic reviewer's `grep` return context lines around each match, for this repo. */
126
+ grep_context?: boolean | null;
127
+ include_globs?: string[] | null;
128
+ /** Extra review instructions in plain language, appended to the system prompt. */
129
+ instructions?: string | null;
130
+ max_findings?: number | null;
131
+ min_confidence?: number | null;
132
+ model?: string | null;
133
+ model_explore?: string | null;
134
+ /** Pass this repo's PR descriptions to the reviewer as a statement of intent to check the diff against. */
135
+ pr_body?: boolean | null;
136
+ /** Cap on the description handed to the reviewer, for this repo. */
137
+ pr_body_max_chars?: number | null;
138
+ /** Toggle re-anchoring a finding that drifted just off a diff line to the nearest matching diff line (else it folds to the summary), for this repo. */
139
+ reanchor_findings?: boolean | null;
140
+ self_critique?: boolean | null;
141
+ /** Toggle committable suggestion blocks on findings, for this repo. */
142
+ suggestions?: boolean | null;
143
+ /** Globs marking vendored third-party source (`thirdparty/**`, `vendor/**`, …). */
144
+ vendored?: string[] | null;
145
+ }
146
+
147
+ /** Nothing to read against — no checkout, or a PR with no ref to fetch from. */
148
+ export interface RepoConfigSourceUnavailable {
149
+ reason: string;
150
+ status: "unavailable";
151
+ }
152
+
153
+ /** Looked, and the repository ships no `.prbot.toml`. */
154
+ export interface RepoConfigSourceAbsent {
155
+ location: string;
156
+ status: "absent";
157
+ }
158
+
159
+ /** Read, parsed, and merged over the environment config. */
160
+ export interface RepoConfigSourceApplied {
161
+ location: string;
162
+ /** Exactly the keys the file set. */
163
+ overrides: RepoConfig;
164
+ status: "applied";
165
+ }
166
+
167
+ /** Read, but rejected — invalid TOML or an unknown key. */
168
+ export interface RepoConfigSourceInvalid {
169
+ error: string;
170
+ location: string;
171
+ status: "invalid";
172
+ }
173
+
174
+ /** Where the per-repo configuration came from, and what it said. */
175
+ export type RepoConfigSource = RepoConfigSourceUnavailable | RepoConfigSourceAbsent | RepoConfigSourceApplied | RepoConfigSourceInvalid;
176
+
177
+ /** The review-relevant configuration, after merging repo overrides. */
178
+ export interface ReviewSettings {
179
+ agentic: boolean;
180
+ blastMaxRefs: number;
181
+ blastMaxSymbols: number;
182
+ blastRadius: boolean;
183
+ changeIntentMaxChars: number;
184
+ ciStatus: boolean;
185
+ /** The signature appended to every comment this deployment posts. */
186
+ commentMarker: string;
187
+ complexityMetrics: boolean;
188
+ complexityMinCyclomatic: number;
189
+ cveMaxPackages: number;
190
+ cveScan: boolean;
191
+ diagram: boolean;
192
+ diagramMaxNodes: number;
193
+ excludeGlobs: string[];
194
+ fileBundling: boolean;
195
+ grepContext: boolean;
196
+ includeGlobs: string[];
197
+ maxDiffChars: number;
198
+ maxFindings: number;
199
+ maxHistoryChars: number;
200
+ maxTokens: number;
201
+ maxTurns: number;
202
+ minConfidence: number;
203
+ model: string;
204
+ modelExplore: string;
205
+ prBody: boolean;
206
+ prBodyMaxChars: number;
207
+ reanchorFindings: boolean;
208
+ reviewSamples: number;
209
+ sampleLineTolerance: number;
210
+ sampleMinAgreement: number;
211
+ selfCritique: boolean;
212
+ structuralContext: boolean;
213
+ structuralMaxFiles: number;
214
+ suggestions: boolean;
215
+ temperature: number;
216
+ vendoredGlobs: string[];
217
+ walkthrough: boolean;
218
+ walkthroughMaxSymbols: number;
219
+ }
220
+
221
+ /** A checkout on disk — the pre-PR path. */
222
+ export interface RulesScopeLocal {
223
+ kind: "local";
224
+ /** The directory whose `.prbot.toml` was consulted. */
225
+ repoRoot: string;
226
+ }
227
+
228
+ /** A pull request on a host. */
229
+ export interface RulesScopePr {
230
+ kind: "pr";
231
+ pr: number;
232
+ provider: string;
233
+ repo: string;
234
+ }
235
+
236
+ /** What the rules were resolved for. */
237
+ export type RulesScope = RulesScopeLocal | RulesScopePr;
238
+
239
+ /** The findings currently on one pull request. */
240
+ export interface FindingsOutput {
241
+ /** The PR's head as of this call, when the provider reported one. */
242
+ headSha?: string | null;
243
+ outcome: FindingsOutcome;
244
+ pr: number;
245
+ provider: string;
246
+ repo: string;
247
+ /** Present when at least one active finding was written against a commit other than the current head. */
248
+ revisionWarning?: string | null;
249
+ }
250
+
251
+ /** Where a finding is in its lifecycle. */
252
+ export type FindingState = "active" | "resolved" | "unparseable";
253
+
254
+ /** The provider tracks finding lifecycle and this is what it holds. */
255
+ export interface FindingsOutcomeListed {
256
+ /** Open findings — the ones worth acting on. */
257
+ active: OutstandingFinding[];
258
+ /** Findings the provider considers closed. */
259
+ resolved: OutstandingFinding[];
260
+ status: "listed";
261
+ /** Bot comments that carry no fingerprint and cannot be matched. */
262
+ unparseable: OutstandingFinding[];
263
+ }
264
+
265
+ /** This provider cannot answer the question, and why. */
266
+ export interface FindingsOutcomeUnsupported {
267
+ reason: string;
268
+ status: "unsupported";
269
+ }
270
+
271
+ /** Whether the provider could answer at all. */
272
+ export type FindingsOutcome = FindingsOutcomeListed | FindingsOutcomeUnsupported;
273
+
274
+ /** One finding as it currently exists on the pull request. */
275
+ export interface OutstandingFinding {
276
+ /** The comment body as posted, rendered markdown and all. */
277
+ body: string;
278
+ /** The provider's id for the comment, for a caller that wants to link to it. */
279
+ commentId: string;
280
+ /** The reconciliation fingerprint — stable across rewordings of the same finding, and the identity to pass back to `resolve-findings`. */
281
+ fingerprint?: string | null;
282
+ /** The line the provider currently tracks this thread at — which is not necessarily the line it was posted on, since a provider moves a thread as the file is edited beneath it. */
283
+ line?: number | null;
284
+ /** The commit this finding was first written against. */
285
+ originalCommit?: string | null;
286
+ path: string;
287
+ state: FindingState;
288
+ /** The provider's thread id, where it has threads. */
289
+ threadId?: string | null;
290
+ }
291
+
292
+ /** A bundle of findings handed over for investigation. */
293
+ export interface ResolveOutput {
294
+ /** Stated in the payload, not left to the caller's memory. */
295
+ disclaimer: string;
296
+ handoffs: FindingHandoff[];
297
+ headSha?: string | null;
298
+ pr: number;
299
+ provider: string;
300
+ repo: string;
301
+ /** Set when the provider cannot track findings at all, with the reason. */
302
+ unsupported?: string | null;
303
+ }
304
+
305
+ /** One finding, packaged for a coding agent to act on. */
306
+ export interface FindingHandoff {
307
+ action: HandoffAction;
308
+ /** Absent only for [`HandoffAction::NotFound`]. */
309
+ finding?: OutstandingFinding | null;
310
+ /** Why this action, in one sentence a caller can show a user. */
311
+ rationale: string;
312
+ /** The identity that was asked for — echoed back so a caller can match up a request that produced [`HandoffAction::NotFound`]. */
313
+ requested: string;
314
+ }
315
+
316
+ /** What a caller should do with one selected finding. */
317
+ export type HandoffAction = "investigate" | "reverifyAgainstHead" | "alreadyResolved" | "notFound" | "needsHumanJudgement";
318
+
319
+ /** One finding explained against a local checkout. */
320
+ export interface ExplainOutput {
321
+ explanation: Explanation;
322
+ /** The finding as given. */
323
+ finding: ExplainInput;
324
+ /** Set when the finding names a revision that is not the one that was read. */
325
+ revisionWarning?: string | null;
326
+ }
327
+
328
+ /** The finding to explain. */
329
+ export interface ExplainInput {
330
+ /** What the reviewer said. */
331
+ body: string;
332
+ file: string;
333
+ line?: number | null;
334
+ /** The commit the finding was written against, when known. */
335
+ originalCommit?: string | null;
336
+ severity?: string | null;
337
+ }
338
+
339
+ /** A structured explanation of one finding, checked against real code. */
340
+ export interface Explanation {
341
+ /** What goes wrong if the claim holds, in terms of behaviour rather than style. */
342
+ affectedBehavior: string;
343
+ /** What the finding asserts, restated plainly. */
344
+ claim: string;
345
+ /** What in the code supports or contradicts it — file, line, and what is actually there. */
346
+ evidence: string;
347
+ /** How a person could confirm or refute this — a test to write, a command to run, a line to read. */
348
+ suggestedVerification: string;
349
+ /** What the explanation could not settle. */
350
+ uncertainty: string;
351
+ /** Whether the investigation found the finding to hold. */
352
+ verdict: ExplanationVerdict;
353
+ }
354
+
355
+ /** What the investigation concluded. */
356
+ export type ExplanationVerdict = "holds" | "doesNotHold" | "inconclusive";
357
+