@orion-agents/orion-code 0.3.26 → 0.3.28

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.
@@ -0,0 +1,354 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_REVIEW_BUDGETS = void 0;
4
+ exports.createProcessGitRunner = createProcessGitRunner;
5
+ exports.computeReviewRulesDigest = computeReviewRulesDigest;
6
+ exports.buildReviewSystemPrompt = buildReviewSystemPrompt;
7
+ exports.runCodeReview = runCodeReview;
8
+ /**
9
+ * v0.3.28 — the bounded single-pass code-review runner (docs/plan/
10
+ * v0.3.28-plan.md S2). Wires the frozen PR0 contract and the PR1 snapshot
11
+ * reader into an executable review: a typed selector is resolved to a frozen
12
+ * Git snapshot, the snapshot material is assembled into a bounded prompt, one
13
+ * model completion is requested, and the output is validated against the
14
+ * contract before a digest-verifiable `CodeReviewResultV1` receipt is built.
15
+ *
16
+ * Read-only posture: the runner performs no writes inside the reviewed
17
+ * repository — git runs read-only with argument arrays, working files are read
18
+ * through the contained reader, and the model receives the frozen material
19
+ * inline instead of an open tool surface. `REVIEW_WORKFLOW_CONSTRAINTS` is
20
+ * bound into the receipt's rules digest so receipts record the policy that
21
+ * governed the run.
22
+ */
23
+ const child_process_1 = require("child_process");
24
+ const crypto_1 = require("crypto");
25
+ const crypto_2 = require("crypto");
26
+ const promises_1 = require("fs/promises");
27
+ const path_1 = require("path");
28
+ const contract_1 = require("./contract");
29
+ Object.defineProperty(exports, "DEFAULT_REVIEW_BUDGETS", { enumerable: true, get: function () { return contract_1.DEFAULT_REVIEW_BUDGETS; } });
30
+ const constraints_1 = require("./constraints");
31
+ const snapshot_1 = require("./snapshot");
32
+ function sha256(value) {
33
+ return (0, crypto_1.createHash)('sha256').update(value, 'utf8').digest('hex');
34
+ }
35
+ /** Production git runner: argument arrays, no shell, read-only profile is the
36
+ * caller's responsibility (the runner only issues read commands). */
37
+ function createProcessGitRunner(cwd) {
38
+ return async (args) => new Promise((resolvePromise, reject) => {
39
+ (0, child_process_1.execFile)('git', args, { cwd, maxBuffer: 64 * 1024 * 1024 }, (error, stdout, stderr) => {
40
+ if (error) {
41
+ reject(new Error(`git ${args.join(' ')} failed: ${stderr || error.message}`));
42
+ return;
43
+ }
44
+ resolvePromise(stdout);
45
+ });
46
+ });
47
+ }
48
+ /** Rules provenance: AGENTS.md contents from cwd up to the filesystem root
49
+ * (bounded to 16 levels), combined with the frozen read-only constraints
50
+ * digest. Changing the rules or the policy invalidates applicability. */
51
+ async function computeReviewRulesDigest(cwd) {
52
+ const chunks = [];
53
+ let current = (0, path_1.resolve)(cwd);
54
+ for (let depth = 0; depth < 16; depth += 1) {
55
+ try {
56
+ const content = await (0, promises_1.readFile)((0, path_1.join)(current, 'AGENTS.md'), 'utf8');
57
+ chunks.push(`${current}::${content}`);
58
+ }
59
+ catch {
60
+ // No AGENTS.md at this level — fine.
61
+ }
62
+ const parent = (0, path_1.dirname)(current);
63
+ if (parent === current)
64
+ break;
65
+ current = parent;
66
+ }
67
+ return sha256(`${constraints_1.REVIEW_WORKFLOW_CONSTRAINTS.constraintsDigest}\n${chunks.join('\n---\n')}`);
68
+ }
69
+ /** The frozen review instruction set assembled for the model. */
70
+ function buildReviewSystemPrompt(rulesDigest) {
71
+ return [
72
+ 'You are a strict code reviewer operating inside Orion Code.',
73
+ 'You receive a frozen snapshot of the changes under review.',
74
+ 'Report only defects introduced or worsened by these changes, with concrete evidence.',
75
+ 'Exclude pure style opinions, speculation without evidence, and duplicates.',
76
+ 'Never suggest running commands, editing files, or any other action: this review is read-only.',
77
+ 'Respond with a single JSON object of the shape {"findings":[...]} and nothing else.',
78
+ 'Each finding object needs exactly these fields:',
79
+ ' findingId: 8-64 hex characters, stable within this response;',
80
+ ' priority: "P0" (blocks usage, proven) | "P1" | "P2" | "P3";',
81
+ ' title: short imperative summary;',
82
+ ' trigger: the concrete condition under which the defect manifests;',
83
+ ' impact: what breaks and for whom;',
84
+ ' fileKey: copy the file key from the snapshot material verbatim;',
85
+ ' side: "old" for removed/base lines, "new" for added/current lines;',
86
+ ' startLine, endLine: 1-based inclusive line numbers in that side;',
87
+ ' evidence: the code excerpt or reasoning that proves the finding.',
88
+ `Active policy digest: ${constraints_1.REVIEW_WORKFLOW_CONSTRAINTS.constraintsDigest}.`,
89
+ `Rules digest: ${rulesDigest}.`,
90
+ ].join('\n');
91
+ }
92
+ /** Assembles the bounded user prompt material from the frozen snapshot plus
93
+ * the diff text captured inside the admission window. */
94
+ function buildSnapshotMaterial(snapshot, diffText, budgets) {
95
+ const sections = [
96
+ `Snapshot: ${snapshot.snapshotId} (kind=${snapshot.kind}`,
97
+ snapshot.kind === 'base' ? `base=${snapshot.baseOid} head=${snapshot.headOid})` : 'worktree scope)',
98
+ `Input snapshot digest: ${snapshot.inputSnapshotDigest}`,
99
+ 'Changed files (fileKey -> path):',
100
+ ...snapshot.files.map(file => {
101
+ const sides = file.sides.map(side => side.source).join('+') || 'no-content';
102
+ return ` ${file.fileKey} ${file.path}${file.oldPath ? ` (from ${file.oldPath})` : ''} [${sides}]`;
103
+ }),
104
+ snapshot.skipped.length > 0
105
+ ? `Skipped by the host: ${snapshot.skipped.map(skip => `${skip.path} (${skip.reason})`).join('; ')}`
106
+ : 'Skipped by the host: none',
107
+ 'Unified diff:',
108
+ diffText,
109
+ ];
110
+ const text = sections.join('\n');
111
+ if (Buffer.byteLength(text, 'utf8') <= budgets.maxBodyBytes) {
112
+ return { text, truncated: false };
113
+ }
114
+ const clipped = Buffer.alloc(budgets.maxBodyBytes);
115
+ const written = Buffer.from(text, 'utf8').subarray(0, budgets.maxBodyBytes);
116
+ clipped.set(written);
117
+ return { text: clipped.toString('utf8'), truncated: true };
118
+ }
119
+ function extractFindingsPayload(content) {
120
+ const trimmed = content.trim();
121
+ const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/);
122
+ const candidate = fenced ? fenced[1].trim() : trimmed;
123
+ const start = candidate.indexOf('{');
124
+ const end = candidate.lastIndexOf('}');
125
+ if (start < 0 || end <= start)
126
+ return null;
127
+ try {
128
+ const parsed = JSON.parse(candidate.slice(start, end + 1));
129
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
130
+ return null;
131
+ const record = parsed;
132
+ if (!Array.isArray(record.findings))
133
+ return null;
134
+ return { findings: record.findings };
135
+ }
136
+ catch {
137
+ return null;
138
+ }
139
+ }
140
+ function snapshotAuthorizesFile(snapshot, finding) {
141
+ const file = snapshot.files.find(candidate => candidate.fileKey === finding.fileKey);
142
+ if (!file)
143
+ return false;
144
+ if (finding.side === 'old' && !file.oldPath && file.hunks.every(hunk => hunk.oldLines === 0)) {
145
+ return false;
146
+ }
147
+ const relevant = file.hunks.filter(hunk => finding.side === 'old'
148
+ ? hunk.oldLines > 0 && finding.startLine <= hunk.oldStart + hunk.oldLines - 1 && finding.endLine >= hunk.oldStart
149
+ : hunk.newLines > 0 && finding.startLine <= hunk.newStart + hunk.newLines - 1 && finding.endLine >= hunk.newStart);
150
+ // A finding whose lines touch no diff hunk is not anchored in the frozen
151
+ // material; the model must cite the changes under review.
152
+ return relevant.length > 0;
153
+ }
154
+ /**
155
+ * Runs one bounded review. Never throws for expected review failures — the
156
+ * outcome is typed; unexpected internal errors surface as `model_error`.
157
+ */
158
+ async function runCodeReview(options) {
159
+ const now = options.now ?? Date.now;
160
+ const startedAt = now();
161
+ const budgets = (0, contract_1.resolveReviewBudgets)(options.budgets);
162
+ if (!budgets.ok)
163
+ return { ok: false, failure: { kind: 'budget', field: budgets.field } };
164
+ const aborted = () => options.abortSignal?.aborted === true;
165
+ if (aborted())
166
+ return { ok: false, failure: { kind: 'cancelled' } };
167
+ const runGit = options.runGit ?? createProcessGitRunner(options.cwd);
168
+ let snapshot;
169
+ let diffText;
170
+ try {
171
+ snapshot = await (0, snapshot_1.captureReviewSnapshot)({
172
+ root: options.cwd,
173
+ selector: options.selector,
174
+ budgets: budgets.budgets,
175
+ runGit,
176
+ readWorktreeFile: (0, snapshot_1.createContainedWorktreeReader)(options.cwd),
177
+ });
178
+ if (aborted())
179
+ return { ok: false, failure: { kind: 'cancelled' } };
180
+ // The exact same frozen diff arguments the snapshot used, against the
181
+ // OIDs the snapshot resolved; captured once inside the admission window
182
+ // and never re-read afterwards.
183
+ diffText = await runGit((0, snapshot_1.reviewUnifiedDiffArgs)(options.selector, {
184
+ baseOid: snapshot.baseOid,
185
+ headOid: snapshot.headOid,
186
+ }));
187
+ }
188
+ catch (error) {
189
+ const code = error instanceof snapshot_1.ReviewSnapshotError ? error.code : 'not_a_repository';
190
+ return {
191
+ ok: false,
192
+ failure: { kind: 'snapshot', code, message: error instanceof Error ? error.message : String(error) },
193
+ };
194
+ }
195
+ const rulesDigest = await computeReviewRulesDigest(options.cwd);
196
+ const limits = [];
197
+ const budgetExhausted = [];
198
+ if (snapshot.files.length === 0) {
199
+ limits.push('empty_scope_no_changes');
200
+ const endedAt = now();
201
+ const coverage = {
202
+ inspectedFiles: [],
203
+ inspectedRanges: [],
204
+ skipped: [],
205
+ truncated: false,
206
+ budgetExhausted: [],
207
+ };
208
+ const base = {
209
+ schemaVersion: 1,
210
+ reviewId: `cr-${(0, crypto_2.randomBytes)(8).toString('hex')}`,
211
+ workspaceIdentity: options.identity.workspaceIdentity,
212
+ sessionId: options.identity.sessionId,
213
+ threadId: options.identity.threadId,
214
+ turnId: options.identity.turnId,
215
+ requestedSelector: options.selector,
216
+ resolvedTarget: {
217
+ kind: snapshot.kind,
218
+ ...(snapshot.baseOid ? { baseOid: snapshot.baseOid } : {}),
219
+ ...(snapshot.headOid ? { headOid: snapshot.headOid } : {}),
220
+ snapshotId: snapshot.snapshotId,
221
+ },
222
+ inputSnapshotDigest: snapshot.inputSnapshotDigest,
223
+ rulesDigest,
224
+ state: 'completed',
225
+ conclusion: 'no_findings',
226
+ findings: [],
227
+ invalidFindingCount: 0,
228
+ coverage,
229
+ coverageAssertion: { complete: true },
230
+ limits,
231
+ model: { provider: 'none', model: 'none' },
232
+ usage: { inputTokens: 0, outputTokens: 0 },
233
+ startedAt,
234
+ endedAt,
235
+ };
236
+ return { ok: true, result: Object.freeze({ ...base, receiptDigest: (0, contract_1.computeReceiptDigest)(base) }) };
237
+ }
238
+ const material = buildSnapshotMaterial(snapshot, diffText, budgets.budgets);
239
+ if (material.truncated) {
240
+ budgetExhausted.push('maxBodyBytes');
241
+ limits.push('prompt_material_truncated');
242
+ }
243
+ if (aborted())
244
+ return { ok: false, failure: { kind: 'cancelled' } };
245
+ let completion;
246
+ try {
247
+ completion = await options.adapter.complete([
248
+ { role: 'system', content: buildReviewSystemPrompt(rulesDigest) },
249
+ { role: 'user', content: material.text },
250
+ ]);
251
+ }
252
+ catch (error) {
253
+ return {
254
+ ok: false,
255
+ failure: {
256
+ kind: 'model_error',
257
+ message: error instanceof Error ? error.message : String(error),
258
+ },
259
+ };
260
+ }
261
+ const durationExceeded = now() - startedAt > budgets.budgets.maxDurationMs;
262
+ if (durationExceeded)
263
+ budgetExhausted.push('maxDurationMs');
264
+ const payload = extractFindingsPayload(completion.content);
265
+ if (!payload) {
266
+ return {
267
+ ok: false,
268
+ failure: {
269
+ kind: 'unparsable_output',
270
+ message: 'The model response did not contain a {"findings":[...]} JSON object.',
271
+ },
272
+ };
273
+ }
274
+ const validFindings = [];
275
+ let invalidFindingCount = 0;
276
+ for (const raw of payload.findings) {
277
+ const validated = (0, contract_1.validateReviewFinding)(raw);
278
+ if (!validated.valid) {
279
+ invalidFindingCount += 1;
280
+ continue;
281
+ }
282
+ if (validFindings.length >= budgets.budgets.maxFindings) {
283
+ invalidFindingCount += 1;
284
+ budgetExhausted.push('maxFindings');
285
+ continue;
286
+ }
287
+ if (!snapshotAuthorizesFile(snapshot, validated.finding)) {
288
+ invalidFindingCount += 1;
289
+ continue;
290
+ }
291
+ validFindings.push(validated.finding);
292
+ }
293
+ const truncated = material.truncated || budgetExhausted.includes('maxFindings');
294
+ const coverage = {
295
+ inspectedFiles: snapshot.files.map(file => file.path),
296
+ inspectedRanges: [],
297
+ skipped: snapshot.skipped.map(skip => ({
298
+ fileKey: skip.path,
299
+ reason: skip.reason,
300
+ ...(skip.detail ? { detail: skip.detail } : {}),
301
+ })),
302
+ truncated,
303
+ budgetExhausted: [...new Set(budgetExhausted)],
304
+ };
305
+ const coverageComplete = snapshot.skipped.length === 0 && !truncated && !durationExceeded && !material.truncated;
306
+ let state = 'completed';
307
+ if (aborted())
308
+ return { ok: false, failure: { kind: 'cancelled' } };
309
+ if (truncated || durationExceeded || snapshot.skipped.length > 0)
310
+ state = 'partial';
311
+ const conclusion = (0, contract_1.resolveReviewConclusion)({
312
+ state,
313
+ validFindingCount: validFindings.length,
314
+ invalidFindingCount,
315
+ coverageComplete,
316
+ });
317
+ const base = {
318
+ schemaVersion: 1,
319
+ reviewId: `cr-${(0, crypto_2.randomBytes)(8).toString('hex')}`,
320
+ workspaceIdentity: options.identity.workspaceIdentity,
321
+ sessionId: options.identity.sessionId,
322
+ threadId: options.identity.threadId,
323
+ turnId: options.identity.turnId,
324
+ requestedSelector: options.selector,
325
+ resolvedTarget: {
326
+ kind: snapshot.kind,
327
+ ...(snapshot.baseOid ? { baseOid: snapshot.baseOid } : {}),
328
+ ...(snapshot.headOid ? { headOid: snapshot.headOid } : {}),
329
+ snapshotId: snapshot.snapshotId,
330
+ },
331
+ inputSnapshotDigest: snapshot.inputSnapshotDigest,
332
+ rulesDigest,
333
+ state,
334
+ conclusion,
335
+ findings: Object.freeze(validFindings),
336
+ invalidFindingCount,
337
+ coverage,
338
+ coverageAssertion: { complete: coverageComplete },
339
+ limits,
340
+ model: { provider: 'orion-llm', model: completion.model },
341
+ usage: {
342
+ inputTokens: completion.usage?.inputTokens ?? 0,
343
+ outputTokens: completion.usage?.outputTokens ?? 0,
344
+ },
345
+ startedAt,
346
+ endedAt: now(),
347
+ };
348
+ const result = Object.freeze({
349
+ ...base,
350
+ receiptDigest: (0, contract_1.computeReceiptDigest)(base),
351
+ });
352
+ return { ok: true, result };
353
+ }
354
+ //# sourceMappingURL=runner.js.map
@@ -0,0 +1,24 @@
1
+ /**
2
+ * v0.3.28 — typed `/review` selector parsing (docs/plan/v0.3.28-plan.md S1).
3
+ *
4
+ * Slash arguments are parsed once into the frozen `ReviewSelectorV1` contract
5
+ * from PR0. Unknown arguments produce a typed usage error instead of falling
6
+ * back to an ordinary chat turn (v0.3.27 plan §3.1: "不能当成分支、路径或任意
7
+ * 普通聊天悄悄继续").
8
+ */
9
+ import type { ReviewSelectorV1 } from './contract';
10
+ export type ReviewSelectorParseResult = {
11
+ readonly ok: true;
12
+ readonly selector: ReviewSelectorV1;
13
+ } | {
14
+ readonly ok: false;
15
+ readonly usage: string;
16
+ };
17
+ export declare const REVIEW_USAGE_TEXT: string;
18
+ /**
19
+ * Parses `/review` arguments. Tokens: `all` alias, `--base <ref>` (once),
20
+ * `--path <path>` (repeatable, also `--path=<path>`), a single bare relative
21
+ * path (back-compat path filter). Anything else is a usage error.
22
+ */
23
+ export declare function parseReviewSelector(rawArgs: string): ReviewSelectorParseResult;
24
+ //# sourceMappingURL=selector.d.ts.map
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.REVIEW_USAGE_TEXT = void 0;
4
+ exports.parseReviewSelector = parseReviewSelector;
5
+ exports.REVIEW_USAGE_TEXT = [
6
+ 'Usage: /review [all | --base <ref>] [--path <path>]...',
7
+ ' /review Review the current worktree (staged + unstaged + untracked)',
8
+ ' /review all Alias for the current worktree',
9
+ ' /review --base main Review committed changes merge-base(main, HEAD)..HEAD',
10
+ ' /review --path src/foo.ts Restrict the review to a path (repeatable)',
11
+ ].join('\n');
12
+ function ok(selector) {
13
+ return { ok: true, selector: Object.freeze({ ...selector }) };
14
+ }
15
+ function usageError(reason) {
16
+ return { ok: false, usage: `${reason}\n\n${exports.REVIEW_USAGE_TEXT}` };
17
+ }
18
+ function looksLikeRelativePath(value) {
19
+ return (value.length > 0 &&
20
+ !value.startsWith('/') &&
21
+ !value.startsWith('-') &&
22
+ !value.includes('\\') &&
23
+ value.split('/').every(segment => segment && segment !== '.' && segment !== '..'));
24
+ }
25
+ /**
26
+ * Parses `/review` arguments. Tokens: `all` alias, `--base <ref>` (once),
27
+ * `--path <path>` (repeatable, also `--path=<path>`), a single bare relative
28
+ * path (back-compat path filter). Anything else is a usage error.
29
+ */
30
+ function parseReviewSelector(rawArgs) {
31
+ const tokens = rawArgs.trim().split(/\s+/).filter(token => token.length > 0);
32
+ let base = null;
33
+ const paths = [];
34
+ for (let index = 0; index < tokens.length; index += 1) {
35
+ const token = tokens[index];
36
+ if (token === 'all') {
37
+ if (tokens.length !== 1) {
38
+ return usageError('`all` cannot be combined with other arguments.');
39
+ }
40
+ return ok({ kind: 'worktree' });
41
+ }
42
+ if (token === '--base' || token.startsWith('--base=')) {
43
+ if (base !== null)
44
+ return usageError('--base may only be given once.');
45
+ const value = token.startsWith('--base=') ? token.slice('--base='.length) : tokens[index + 1];
46
+ if (!value)
47
+ return usageError('--base requires a ref argument.');
48
+ if (token === '--base')
49
+ index += 1;
50
+ base = value;
51
+ continue;
52
+ }
53
+ if (token === '--path' || token.startsWith('--path=')) {
54
+ const value = token.startsWith('--path=') ? token.slice('--path='.length) : tokens[index + 1];
55
+ if (!value)
56
+ return usageError('--path requires a path argument.');
57
+ if (token === '--path')
58
+ index += 1;
59
+ if (!looksLikeRelativePath(value)) {
60
+ return usageError(`Invalid --path value: ${value}`);
61
+ }
62
+ paths.push(value);
63
+ continue;
64
+ }
65
+ if (token.startsWith('-')) {
66
+ return usageError(`Unknown option: ${token}`);
67
+ }
68
+ // Bare argument: back-compat single existing-path filter. Multiple bare
69
+ // arguments are ambiguous and rejected rather than guessed.
70
+ if (paths.length > 0 || base !== null) {
71
+ return usageError(`Unexpected argument: ${token}`);
72
+ }
73
+ if (!looksLikeRelativePath(token)) {
74
+ return usageError(`Invalid path argument: ${token}`);
75
+ }
76
+ paths.push(token);
77
+ }
78
+ if (base !== null) {
79
+ return ok(paths.length > 0 ? { kind: 'base', ref: base, paths } : { kind: 'base', ref: base });
80
+ }
81
+ return ok(paths.length > 0 ? { kind: 'worktree', paths } : { kind: 'worktree' });
82
+ }
83
+ //# sourceMappingURL=selector.js.map
@@ -0,0 +1,71 @@
1
+ import { type ReviewBudgetsV1, type ReviewSelectorV1 } from './contract';
2
+ export interface ReviewSnapshotFileSideV1 {
3
+ readonly source: 'staged' | 'unstaged' | 'base' | 'head';
4
+ readonly oid: string | null;
5
+ readonly contentDigest: string;
6
+ readonly byteSize: number;
7
+ readonly truncated: boolean;
8
+ }
9
+ export interface ReviewSnapshotHunkV1 {
10
+ readonly oldStart: number;
11
+ readonly oldLines: number;
12
+ readonly newStart: number;
13
+ readonly newLines: number;
14
+ readonly header: string;
15
+ }
16
+ export interface ReviewSnapshotFileV1 {
17
+ readonly fileKey: string;
18
+ readonly path: string;
19
+ readonly oldPath: string | null;
20
+ readonly sensitive: boolean;
21
+ readonly sides: readonly ReviewSnapshotFileSideV1[];
22
+ readonly hunks: readonly ReviewSnapshotHunkV1[];
23
+ }
24
+ export interface ReviewSnapshotSkipV1 {
25
+ readonly path: string;
26
+ readonly reason: 'over_budget_files' | 'over_budget_bytes' | 'binary' | 'sensitive' | 'unreadable' | 'changed_during_review' | 'other';
27
+ readonly detail?: string;
28
+ }
29
+ export interface CodeReviewSnapshotV1 {
30
+ readonly schemaVersion: 1;
31
+ readonly snapshotId: string;
32
+ readonly kind: 'worktree' | 'base';
33
+ readonly baseOid: string | null;
34
+ readonly headOid: string | null;
35
+ readonly requestedPaths: readonly string[];
36
+ readonly files: readonly ReviewSnapshotFileV1[];
37
+ readonly inputSnapshotDigest: string;
38
+ readonly skipped: readonly ReviewSnapshotSkipV1[];
39
+ }
40
+ export declare class ReviewSnapshotError extends Error {
41
+ readonly code: 'not_a_repository' | 'ref_unresolved' | 'no_merge_base' | 'path_invalid' | 'worktree_changed';
42
+ constructor(message: string, code: 'not_a_repository' | 'ref_unresolved' | 'no_merge_base' | 'path_invalid' | 'worktree_changed');
43
+ }
44
+ /** Runs a read-only git command with argument arrays. The caller injects this,
45
+ * so tests drive real temporary repositories without spawning beyond git. */
46
+ export type ReviewGitRunner = (args: readonly string[]) => Promise<string>;
47
+ export interface ReviewSnapshotOptionsV1 {
48
+ readonly root: string;
49
+ readonly selector: ReviewSelectorV1;
50
+ readonly budgets?: Partial<ReviewBudgetsV1>;
51
+ readonly runGit: ReviewGitRunner;
52
+ /** Reads one working-tree file relative to the root, or null when missing. */
53
+ readonly readWorktreeFile?: (relativePath: string) => Promise<string | null>;
54
+ readonly snapshotIdPrefix?: string;
55
+ }
56
+ /** The read-only git arguments for the scope's unified diff. Exported so the
57
+ * v0.3.28 runner captures the exact same diff the snapshot froze instead of
58
+ * re-deriving OIDs after admission. */
59
+ export declare function reviewUnifiedDiffArgs(selector: ReviewSelectorV1, oids: {
60
+ readonly baseOid: string | null;
61
+ readonly headOid: string | null;
62
+ }): string[];
63
+ /** Resolves the selector to a frozen snapshot. Every OID and digest in the
64
+ * returned object was taken at admission time; callers persist the returned
65
+ * digest as the review's input identity. */
66
+ export declare function captureReviewSnapshot(options: ReviewSnapshotOptionsV1): Promise<CodeReviewSnapshotV1>;
67
+ /** Reads one working-tree file for review purposes, refusing to leave the
68
+ * repository root (same containment discipline as the v0.3.25 conflict
69
+ * reader). Returns null when the file is missing or unreadable. */
70
+ export declare function createContainedWorktreeReader(rootInput: string): (relativePath: string) => Promise<string | null>;
71
+ //# sourceMappingURL=snapshot.d.ts.map