@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.
- package/CHANGELOG.md +109 -8
- package/README.md +11 -8
- package/README.zh-CN.md +8 -6
- package/dist/commands/workflow-command-handlers.d.ts +9 -1
- package/dist/commands/workflow-command-handlers.js +66 -0
- package/dist/commands/workflow-commands.js +5 -5
- package/dist/services/code-review/constraints.d.ts +25 -0
- package/dist/services/code-review/constraints.js +79 -0
- package/dist/services/code-review/contract.d.ts +172 -0
- package/dist/services/code-review/contract.js +184 -0
- package/dist/services/code-review/format.d.ts +12 -0
- package/dist/services/code-review/format.js +79 -0
- package/dist/services/code-review/runner.d.ts +71 -0
- package/dist/services/code-review/runner.js +354 -0
- package/dist/services/code-review/selector.d.ts +24 -0
- package/dist/services/code-review/selector.js +83 -0
- package/dist/services/code-review/snapshot.d.ts +71 -0
- package/dist/services/code-review/snapshot.js +330 -0
- package/npm-shrinkwrap.json +12 -12
- package/package.json +1 -1
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FINDING_TEXT_LIMITS = exports.DEFAULT_REVIEW_BUDGETS = void 0;
|
|
4
|
+
exports.validateReviewFinding = validateReviewFinding;
|
|
5
|
+
exports.resolveReviewConclusion = resolveReviewConclusion;
|
|
6
|
+
exports.resolveReviewBudgets = resolveReviewBudgets;
|
|
7
|
+
exports.reviewApplicability = reviewApplicability;
|
|
8
|
+
exports.computeReceiptDigest = computeReceiptDigest;
|
|
9
|
+
exports.verifyReceiptDigest = verifyReceiptDigest;
|
|
10
|
+
/**
|
|
11
|
+
* v0.3.27 PR0 — the shared code-review contract, frozen before any runtime
|
|
12
|
+
* wiring. This module is the single source of truth for review selectors,
|
|
13
|
+
* states, conclusions, findings, budgets and receipt digests; CLI and Web both
|
|
14
|
+
* consume it and neither may redefine the semantics locally (goal G3: one
|
|
15
|
+
* engine, one contract).
|
|
16
|
+
*
|
|
17
|
+
* Design sources: docs/plan/v0.3.27-plan.md §4 (identity, state, conclusions),
|
|
18
|
+
* §4.3 (bounded resources), §5 (read-only execution). The conclusion mapping
|
|
19
|
+
* encodes the plan's rule that tool success is not review success: a partial,
|
|
20
|
+
* failed, cancelled or untrustworthy output can never surface as a green
|
|
21
|
+
* "no findings".
|
|
22
|
+
*/
|
|
23
|
+
const canonical_1 = require("../../runtime/protocol/canonical");
|
|
24
|
+
exports.DEFAULT_REVIEW_BUDGETS = Object.freeze({
|
|
25
|
+
maxFiles: 50,
|
|
26
|
+
maxBodyBytes: 2 * 1024 * 1024,
|
|
27
|
+
maxDiffBytes: 1024 * 1024,
|
|
28
|
+
maxReadToolCalls: 50,
|
|
29
|
+
maxDurationMs: 5 * 60 * 1000,
|
|
30
|
+
maxFindings: 100,
|
|
31
|
+
maxResultBytes: 256 * 1024,
|
|
32
|
+
});
|
|
33
|
+
/** Text size caps for finding fields, in characters. Chosen so 100 maximal
|
|
34
|
+
* findings stay well inside the 256 KiB result bound. */
|
|
35
|
+
exports.FINDING_TEXT_LIMITS = Object.freeze({
|
|
36
|
+
title: 200,
|
|
37
|
+
trigger: 2000,
|
|
38
|
+
impact: 2000,
|
|
39
|
+
evidence: 4000,
|
|
40
|
+
fileKey: 1024,
|
|
41
|
+
});
|
|
42
|
+
const FINDING_ID_PATTERN = /^[0-9a-f]{8,64}$/iu;
|
|
43
|
+
const DIGEST_PATTERN = /^[0-9a-f]{64}$/;
|
|
44
|
+
/** Validates one model-produced finding against the frozen shape. Returns the
|
|
45
|
+
* finding unchanged when valid; a typed reason when not. Membership against
|
|
46
|
+
* the snapshot manifest is a separate Host check (PR1+). */
|
|
47
|
+
function validateReviewFinding(value) {
|
|
48
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
49
|
+
return { valid: false, reason: 'not_an_object' };
|
|
50
|
+
}
|
|
51
|
+
const candidate = value;
|
|
52
|
+
const text = (field) => typeof candidate[field] === 'string' ? candidate[field] : '';
|
|
53
|
+
if (typeof candidate.findingId !== 'string' || !FINDING_ID_PATTERN.test(candidate.findingId)) {
|
|
54
|
+
return { valid: false, reason: 'finding_id_invalid' };
|
|
55
|
+
}
|
|
56
|
+
if (candidate.priority !== 'P0' &&
|
|
57
|
+
candidate.priority !== 'P1' &&
|
|
58
|
+
candidate.priority !== 'P2' &&
|
|
59
|
+
candidate.priority !== 'P3') {
|
|
60
|
+
return { valid: false, reason: 'priority_invalid' };
|
|
61
|
+
}
|
|
62
|
+
const title = text('title').trim();
|
|
63
|
+
if (!title || title.length > exports.FINDING_TEXT_LIMITS.title) {
|
|
64
|
+
return { valid: false, reason: 'title_invalid' };
|
|
65
|
+
}
|
|
66
|
+
const trigger = text('trigger').trim();
|
|
67
|
+
if (!trigger || trigger.length > exports.FINDING_TEXT_LIMITS.trigger) {
|
|
68
|
+
return { valid: false, reason: 'trigger_invalid' };
|
|
69
|
+
}
|
|
70
|
+
const impact = text('impact').trim();
|
|
71
|
+
if (!impact || impact.length > exports.FINDING_TEXT_LIMITS.impact) {
|
|
72
|
+
return { valid: false, reason: 'impact_invalid' };
|
|
73
|
+
}
|
|
74
|
+
const fileKey = text('fileKey');
|
|
75
|
+
if (!fileKey ||
|
|
76
|
+
fileKey.length > exports.FINDING_TEXT_LIMITS.fileKey ||
|
|
77
|
+
fileKey.startsWith('/') ||
|
|
78
|
+
fileKey.includes('\0') ||
|
|
79
|
+
fileKey.split('/').includes('..')) {
|
|
80
|
+
return { valid: false, reason: 'file_key_invalid' };
|
|
81
|
+
}
|
|
82
|
+
if (candidate.side !== 'old' && candidate.side !== 'new') {
|
|
83
|
+
return { valid: false, reason: 'side_invalid' };
|
|
84
|
+
}
|
|
85
|
+
const startLine = candidate.startLine;
|
|
86
|
+
const endLine = candidate.endLine;
|
|
87
|
+
if (!Number.isSafeInteger(startLine) ||
|
|
88
|
+
startLine < 1 ||
|
|
89
|
+
!Number.isSafeInteger(endLine) ||
|
|
90
|
+
endLine < startLine) {
|
|
91
|
+
return { valid: false, reason: 'line_range_invalid' };
|
|
92
|
+
}
|
|
93
|
+
const evidence = text('evidence').trim();
|
|
94
|
+
if (!evidence || evidence.length > exports.FINDING_TEXT_LIMITS.evidence) {
|
|
95
|
+
return { valid: false, reason: 'evidence_invalid' };
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
valid: true,
|
|
99
|
+
finding: Object.freeze({
|
|
100
|
+
findingId: candidate.findingId,
|
|
101
|
+
priority: candidate.priority,
|
|
102
|
+
title,
|
|
103
|
+
trigger,
|
|
104
|
+
impact,
|
|
105
|
+
fileKey,
|
|
106
|
+
side: candidate.side,
|
|
107
|
+
startLine: startLine,
|
|
108
|
+
endLine: endLine,
|
|
109
|
+
evidence,
|
|
110
|
+
}),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* v0.3.27 §4.1 — the conclusion mapping, frozen as code so CLI and Web cannot
|
|
115
|
+
* diverge. The invariants:
|
|
116
|
+
* - `queued`/`running` results carry no conclusion (callers must not call this).
|
|
117
|
+
* - A state that is not `completed` can never conclude `no_findings`.
|
|
118
|
+
* - Invalid findings make the output untrustworthy: zero valid findings with
|
|
119
|
+
* invalid ones present is `inconclusive`, never a green pass.
|
|
120
|
+
* - Zero findings without complete coverage is `inconclusive`, never a green
|
|
121
|
+
* pass ("不静默截断后宣布全部完成").
|
|
122
|
+
*/
|
|
123
|
+
function resolveReviewConclusion(input) {
|
|
124
|
+
if (input.state === 'queued' || input.state === 'running') {
|
|
125
|
+
throw new Error('A queued or running review cannot have a terminal conclusion.');
|
|
126
|
+
}
|
|
127
|
+
if (input.state === 'failed' || input.state === 'cancelled') {
|
|
128
|
+
return 'inconclusive';
|
|
129
|
+
}
|
|
130
|
+
if (input.validFindingCount > 0) {
|
|
131
|
+
return 'findings';
|
|
132
|
+
}
|
|
133
|
+
if (input.state === 'partial') {
|
|
134
|
+
return 'inconclusive';
|
|
135
|
+
}
|
|
136
|
+
// state === 'completed'
|
|
137
|
+
if (input.invalidFindingCount > 0) {
|
|
138
|
+
return 'inconclusive';
|
|
139
|
+
}
|
|
140
|
+
return input.coverageComplete ? 'no_findings' : 'inconclusive';
|
|
141
|
+
}
|
|
142
|
+
/** Applies the budgets with the tighten-only rule: every override must be a
|
|
143
|
+
* safe integer at or below the frozen default, otherwise the run is refused
|
|
144
|
+
* rather than silently widened. */
|
|
145
|
+
function resolveReviewBudgets(overrides = {}) {
|
|
146
|
+
const resolved = {};
|
|
147
|
+
for (const [field, ceiling] of Object.entries(exports.DEFAULT_REVIEW_BUDGETS)) {
|
|
148
|
+
const requested = overrides[field];
|
|
149
|
+
if (requested === undefined) {
|
|
150
|
+
resolved[field] = ceiling;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (!Number.isSafeInteger(requested) || requested < 1 || requested > ceiling) {
|
|
154
|
+
return { ok: false, field };
|
|
155
|
+
}
|
|
156
|
+
resolved[field] = requested;
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
ok: true,
|
|
160
|
+
budgets: Object.freeze(resolved),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/** Marks a stored result's applicability against the current input snapshot and
|
|
164
|
+
* rules digests. The receipt itself is never rewritten. */
|
|
165
|
+
function reviewApplicability(result, current) {
|
|
166
|
+
return result.inputSnapshotDigest === current.inputSnapshotDigest &&
|
|
167
|
+
result.rulesDigest === current.rulesDigest
|
|
168
|
+
? 'current'
|
|
169
|
+
: 'stale';
|
|
170
|
+
}
|
|
171
|
+
/** The receipt digest covers the canonical form of the result with the digest
|
|
172
|
+
* field itself excluded, so verification is a recomputation, not a lookup. */
|
|
173
|
+
function computeReceiptDigest(result) {
|
|
174
|
+
const content = { ...result };
|
|
175
|
+
delete content.receiptDigest;
|
|
176
|
+
const digest = (0, canonical_1.digestRuntimeValue)(content);
|
|
177
|
+
return DIGEST_PATTERN.test(digest) ? digest : digest.toLowerCase();
|
|
178
|
+
}
|
|
179
|
+
/** Verifies a stored receipt by recomputing its digest. */
|
|
180
|
+
function verifyReceiptDigest(result) {
|
|
181
|
+
const { receiptDigest, ...content } = result;
|
|
182
|
+
return computeReceiptDigest(content) === receiptDigest;
|
|
183
|
+
}
|
|
184
|
+
//# sourceMappingURL=contract.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v0.3.28 — TUI rendering of a code-review receipt (docs/plan/v0.3.28-plan.md
|
|
3
|
+
* S3). The formatter is deliberately honest about state: a partial, failed,
|
|
4
|
+
* cancelled or inconclusive run never renders as a green "no findings" pass,
|
|
5
|
+
* and an empty scope is reported as "no changes under review", distinct from a
|
|
6
|
+
* completed review that found nothing (v0.3.27 plan §4.1).
|
|
7
|
+
*/
|
|
8
|
+
import type { CodeReviewResultV1 } from './contract';
|
|
9
|
+
/** Renders the full report shown after `/review` finishes. `fileLabels` maps
|
|
10
|
+
* snapshot fileKeys to human paths when the caller still holds the manifest. */
|
|
11
|
+
export declare function formatReviewReport(result: CodeReviewResultV1, fileLabels?: ReadonlyMap<string, string>): string;
|
|
12
|
+
//# sourceMappingURL=format.d.ts.map
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatReviewReport = formatReviewReport;
|
|
4
|
+
const PRIORITY_ORDER = ['P0', 'P1', 'P2', 'P3'];
|
|
5
|
+
const STATE_BANNERS = {
|
|
6
|
+
queued: 'Review queued.',
|
|
7
|
+
running: 'Review running.',
|
|
8
|
+
completed: 'Review completed.',
|
|
9
|
+
partial: 'Review PARTIAL — coverage was incomplete; findings below are valid but not exhaustive.',
|
|
10
|
+
failed: 'Review FAILED — no conclusions can be drawn from this run.',
|
|
11
|
+
cancelled: 'Review CANCELLED — no conclusions can be drawn from this run.',
|
|
12
|
+
};
|
|
13
|
+
function formatScope(result) {
|
|
14
|
+
const selector = result.requestedSelector;
|
|
15
|
+
if (selector.kind === 'base') {
|
|
16
|
+
const paths = selector.paths?.length ? ` paths: ${selector.paths.join(', ')}` : '';
|
|
17
|
+
return `base ${selector.ref} (${result.resolvedTarget.baseOid?.slice(0, 12) ?? '?'}..${result.resolvedTarget.headOid?.slice(0, 12) ?? '?'})${paths}`;
|
|
18
|
+
}
|
|
19
|
+
const paths = selector.paths?.length ? ` paths: ${selector.paths.join(', ')}` : '';
|
|
20
|
+
return `worktree (snapshot ${result.resolvedTarget.snapshotId})${paths}`;
|
|
21
|
+
}
|
|
22
|
+
/** Renders the full report shown after `/review` finishes. `fileLabels` maps
|
|
23
|
+
* snapshot fileKeys to human paths when the caller still holds the manifest. */
|
|
24
|
+
function formatReviewReport(result, fileLabels) {
|
|
25
|
+
const lines = [];
|
|
26
|
+
lines.push(`Code review ${result.reviewId} — ${formatScope(result)}`);
|
|
27
|
+
lines.push(`Model: ${result.model.provider}/${result.model.model} · input snapshot ${result.inputSnapshotDigest.slice(0, 12)}`);
|
|
28
|
+
const emptyScope = result.limits.includes('empty_scope_no_changes');
|
|
29
|
+
if (emptyScope) {
|
|
30
|
+
lines.push('');
|
|
31
|
+
lines.push('No changes under the selected scope — nothing to review.');
|
|
32
|
+
return lines.join('\n');
|
|
33
|
+
}
|
|
34
|
+
lines.push('');
|
|
35
|
+
lines.push(STATE_BANNERS[result.state]);
|
|
36
|
+
if (result.findings.length > 0) {
|
|
37
|
+
lines.push('');
|
|
38
|
+
lines.push(`Findings (${result.findings.length}):`);
|
|
39
|
+
for (const priority of PRIORITY_ORDER) {
|
|
40
|
+
const group = result.findings.filter(finding => finding.priority === priority);
|
|
41
|
+
if (group.length === 0)
|
|
42
|
+
continue;
|
|
43
|
+
for (const finding of group) {
|
|
44
|
+
lines.push('');
|
|
45
|
+
lines.push(`[${priority}] ${finding.title}`);
|
|
46
|
+
lines.push(` Location: ${fileLabels?.get(finding.fileKey) ?? finding.fileKey.slice(0, 12)} ${finding.side}:${finding.startLine}-${finding.endLine}`);
|
|
47
|
+
lines.push(` Trigger: ${finding.trigger}`);
|
|
48
|
+
lines.push(` Impact: ${finding.impact}`);
|
|
49
|
+
lines.push(` Evidence: ${finding.evidence}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
else if (result.state === 'completed' && result.conclusion === 'no_findings') {
|
|
54
|
+
lines.push('No findings within the reviewed scope.');
|
|
55
|
+
}
|
|
56
|
+
if (result.invalidFindingCount > 0) {
|
|
57
|
+
lines.push('');
|
|
58
|
+
lines.push(`${result.invalidFindingCount} finding(s) from the model were invalid or outside the frozen snapshot and are not shown.`);
|
|
59
|
+
}
|
|
60
|
+
lines.push('');
|
|
61
|
+
const coverage = result.coverage;
|
|
62
|
+
lines.push(`Coverage: ${coverage.inspectedFiles.length} file(s) inspected, ${coverage.skipped.length} skipped${coverage.truncated ? ', output truncated' : ''}`);
|
|
63
|
+
for (const skip of coverage.skipped) {
|
|
64
|
+
lines.push(` skipped: ${skip.fileKey} (${skip.reason})`);
|
|
65
|
+
}
|
|
66
|
+
if (coverage.budgetExhausted.length > 0) {
|
|
67
|
+
lines.push(`Budget exhausted: ${coverage.budgetExhausted.join(', ')}`);
|
|
68
|
+
}
|
|
69
|
+
for (const limit of result.limits) {
|
|
70
|
+
if (limit !== 'empty_scope_no_changes')
|
|
71
|
+
lines.push(`Limit: ${limit}`);
|
|
72
|
+
}
|
|
73
|
+
if (!result.coverageAssertion.complete && result.state === 'completed') {
|
|
74
|
+
lines.push(`Coverage incomplete: ${result.coverageAssertion.reason ?? 'not all changed files were reviewed'}`);
|
|
75
|
+
}
|
|
76
|
+
lines.push(`Receipt digest: ${result.receiptDigest.slice(0, 16)}`);
|
|
77
|
+
return lines.join('\n');
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=format.js.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { DEFAULT_REVIEW_BUDGETS, type CodeReviewResultV1, type ReviewBudgetsV1, type ReviewSelectorV1 } from './contract';
|
|
2
|
+
import { type ReviewGitRunner } from './snapshot';
|
|
3
|
+
/** One bounded model completion. CLI wiring adapts `LLMService.chat`. */
|
|
4
|
+
export interface ReviewModelAdapterV1 {
|
|
5
|
+
complete(messages: readonly {
|
|
6
|
+
readonly role: 'system' | 'user';
|
|
7
|
+
readonly content: string;
|
|
8
|
+
}[]): Promise<{
|
|
9
|
+
readonly content: string;
|
|
10
|
+
readonly model: string;
|
|
11
|
+
readonly usage?: {
|
|
12
|
+
readonly inputTokens: number;
|
|
13
|
+
readonly outputTokens: number;
|
|
14
|
+
};
|
|
15
|
+
}>;
|
|
16
|
+
}
|
|
17
|
+
export interface ReviewRunIdentityV1 {
|
|
18
|
+
readonly workspaceIdentity: string;
|
|
19
|
+
readonly sessionId: string;
|
|
20
|
+
readonly threadId: string;
|
|
21
|
+
readonly turnId: string;
|
|
22
|
+
}
|
|
23
|
+
export interface ReviewRunOptionsV1 {
|
|
24
|
+
readonly cwd: string;
|
|
25
|
+
readonly selector: ReviewSelectorV1;
|
|
26
|
+
readonly adapter: ReviewModelAdapterV1;
|
|
27
|
+
readonly identity: ReviewRunIdentityV1;
|
|
28
|
+
readonly budgets?: Partial<ReviewBudgetsV1>;
|
|
29
|
+
readonly abortSignal?: AbortSignal;
|
|
30
|
+
readonly runGit?: ReviewGitRunner;
|
|
31
|
+
readonly now?: () => number;
|
|
32
|
+
}
|
|
33
|
+
export type ReviewRunFailureV1 = {
|
|
34
|
+
readonly kind: 'budget';
|
|
35
|
+
readonly field: string;
|
|
36
|
+
} | {
|
|
37
|
+
readonly kind: 'snapshot';
|
|
38
|
+
readonly code: string;
|
|
39
|
+
readonly message: string;
|
|
40
|
+
} | {
|
|
41
|
+
readonly kind: 'cancelled';
|
|
42
|
+
} | {
|
|
43
|
+
readonly kind: 'model_error';
|
|
44
|
+
readonly message: string;
|
|
45
|
+
} | {
|
|
46
|
+
readonly kind: 'unparsable_output';
|
|
47
|
+
readonly message: string;
|
|
48
|
+
};
|
|
49
|
+
export type ReviewRunOutcomeV1 = {
|
|
50
|
+
readonly ok: true;
|
|
51
|
+
readonly result: CodeReviewResultV1;
|
|
52
|
+
} | {
|
|
53
|
+
readonly ok: false;
|
|
54
|
+
readonly failure: ReviewRunFailureV1;
|
|
55
|
+
};
|
|
56
|
+
/** Production git runner: argument arrays, no shell, read-only profile is the
|
|
57
|
+
* caller's responsibility (the runner only issues read commands). */
|
|
58
|
+
export declare function createProcessGitRunner(cwd: string): ReviewGitRunner;
|
|
59
|
+
/** Rules provenance: AGENTS.md contents from cwd up to the filesystem root
|
|
60
|
+
* (bounded to 16 levels), combined with the frozen read-only constraints
|
|
61
|
+
* digest. Changing the rules or the policy invalidates applicability. */
|
|
62
|
+
export declare function computeReviewRulesDigest(cwd: string): Promise<string>;
|
|
63
|
+
/** The frozen review instruction set assembled for the model. */
|
|
64
|
+
export declare function buildReviewSystemPrompt(rulesDigest: string): string;
|
|
65
|
+
/**
|
|
66
|
+
* Runs one bounded review. Never throws for expected review failures — the
|
|
67
|
+
* outcome is typed; unexpected internal errors surface as `model_error`.
|
|
68
|
+
*/
|
|
69
|
+
export declare function runCodeReview(options: ReviewRunOptionsV1): Promise<ReviewRunOutcomeV1>;
|
|
70
|
+
export { DEFAULT_REVIEW_BUDGETS };
|
|
71
|
+
//# sourceMappingURL=runner.d.ts.map
|