@orion-agents/orion-code 0.3.26 → 0.3.27
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 +48 -8
- package/README.md +10 -8
- package/README.zh-CN.md +7 -6
- 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/snapshot.d.ts +64 -0
- package/dist/services/code-review/snapshot.js +321 -0
- package/npm-shrinkwrap.json +12 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -22,14 +22,54 @@ which is **not** a pass.
|
|
|
22
22
|
|
|
23
23
|
## [Unreleased]
|
|
24
24
|
|
|
25
|
-
## [0.3.
|
|
26
|
-
|
|
27
|
-
> **Status: candidate.** Open.
|
|
28
|
-
>
|
|
29
|
-
>
|
|
30
|
-
>
|
|
31
|
-
>
|
|
32
|
-
>
|
|
25
|
+
## [0.3.27] — UNRELEASED
|
|
26
|
+
|
|
27
|
+
> **Status: candidate.** Open. First slice of the professional code review loop
|
|
28
|
+
> (plan: `docs/plan/v0.3.27-plan.md`): the shared review contract (selectors,
|
|
29
|
+
> states, conclusions, findings, tighten-only budgets, receipt digests,
|
|
30
|
+
> stale-applicability) is frozen in `src/services/code-review` and bound by
|
|
31
|
+
> contract tests; the shared Git snapshot reader captures worktree (staged /
|
|
32
|
+
> unstaged / untracked, separate sources) and base (frozen merge-base..HEAD
|
|
33
|
+
> OIDs, rename tracking) scopes with typed scope errors; and the read-only
|
|
34
|
+
> workflow constraints deny every non-read tool by default with a dispatch-level
|
|
35
|
+
> guard whose digest binds receipts. Runtime / CLI wiring (PR2), the Web loop
|
|
36
|
+
> (PR3) and the qualification benchmark (PR4) are subsequent slices of the same
|
|
37
|
+
> plan and are not part of this release. Not merged, tagged or published.
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
|
|
41
|
+
- #277-plan PR0: `src/services/code-review/contract.ts` — the shared review
|
|
42
|
+
contract with the plan §4.1 conclusion mapping (a partial, failed, cancelled
|
|
43
|
+
or untrustworthy review can never surface as a green "no findings"),
|
|
44
|
+
tighten-only budgets from plan §4.3, finding validation, receipt digests
|
|
45
|
+
verified by recomputation, and stale-applicability annotation that never
|
|
46
|
+
rewrites history. Twelve contract tests pin the semantics.
|
|
47
|
+
- PR0: the human-annotation review-quality benchmark set (12 defect + 8 clean
|
|
48
|
+
samples across TypeScript/JavaScript/Python with content digests, expected
|
|
49
|
+
triggers, allowed locations and frozen scoring thresholds), awaiting
|
|
50
|
+
per-sample human sign-off before any paid model run (plan §9.3).
|
|
51
|
+
- PR1: `src/services/code-review/snapshot.ts` — the shared Git snapshot reader:
|
|
52
|
+
worktree scope records staged (index OIDs), unstaged (working content with
|
|
53
|
+
before/after digest re-checks) and untracked changes as separate sources;
|
|
54
|
+
base scope freezes ref/HEAD/merge-base into immutable OIDs with rename
|
|
55
|
+
tracking and typed `ref_unresolved` / `no_merge_base` errors; sensitive and
|
|
56
|
+
binary entries are skipped without being read; tightened budgets skip the
|
|
57
|
+
excess with typed reasons; the contained worktree reader refuses to leave the
|
|
58
|
+
repository root.
|
|
59
|
+
- PR1: `src/services/code-review/constraints.ts` — deny-by-default read-only
|
|
60
|
+
workflow constraints (only `read_file`, `list_files`, `glob`, `grep` pass the
|
|
61
|
+
dispatch-level guard) with a frozen digest that receipts bind.
|
|
62
|
+
|
|
63
|
+
## [0.3.26] — 2026-10-17
|
|
64
|
+
|
|
65
|
+
> **Status: published.** Released as `@orion-agents/orion-code@0.3.26` from
|
|
66
|
+
> `refs/tags/v0.3.26`, and merged to `main` as `b810996` (#278). Structured-diff
|
|
67
|
+
> integrity and Session lifecycle: header-lookalike body rows stay body rows
|
|
68
|
+
> (#274), hunks split across pagination keep their rows, line numbers and
|
|
69
|
+
> identity (#275), permanent Session deletion removes the v2 Thread records
|
|
70
|
+
> (#276), and exiting a cutover Session no longer rewrites its summary from the
|
|
71
|
+
> frozen legacy JSONL (#277). Plan: `docs/plan/v0.3.26-plan.md`. GitHub Release:
|
|
72
|
+
> https://github.com/orion-agents/orion-code/releases/tag/v0.3.26
|
|
33
73
|
|
|
34
74
|
### Fixed
|
|
35
75
|
|
package/README.md
CHANGED
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
Local-first, goal-driven coding agent for the terminal and browser.
|
|
4
4
|
|
|
5
|
-
> v0.3.
|
|
6
|
-
>
|
|
7
|
-
>
|
|
8
|
-
>
|
|
9
|
-
>
|
|
10
|
-
>
|
|
5
|
+
> v0.3.27 candidate — open. First slice of the professional code review loop: the
|
|
6
|
+
> shared review contract (selectors, states, conclusions, findings, tighten-only
|
|
7
|
+
> budgets, receipt digests, stale-applicability) is frozen and contract-tested,
|
|
8
|
+
> the shared Git snapshot reader captures worktree and frozen-base scopes with
|
|
9
|
+
> typed errors, and the read-only workflow constraints deny every non-read tool
|
|
10
|
+
> by default. Runtime/CLI wiring, the Web loop and the qualification benchmark
|
|
11
|
+
> are subsequent slices of the same plan. The previous release 0.3.26 is on npm
|
|
12
|
+
> (registry `latest`), tagged `v0.3.26` (`b810996`, #278) and released on GitHub.
|
|
11
13
|
|
|
12
14
|
[中文说明](README.zh-CN.md) ·
|
|
13
15
|
[v0.3.24 plan](docs/plan/v0.3.24-plan.md) ·
|
|
@@ -170,10 +172,10 @@ user extension boundaries.
|
|
|
170
172
|
Node.js 22.12+, 24, and 26 are supported. Use Node 24 LTS for production or Node 26 Current for
|
|
171
173
|
current development environments. Node 20 is upstream EOL and is no longer a v0.3 runtime.
|
|
172
174
|
|
|
173
|
-
To install the pinned v0.3.
|
|
175
|
+
To install the pinned v0.3.27 package when it is available on npm:
|
|
174
176
|
|
|
175
177
|
```bash
|
|
176
|
-
npm install -g @orion-agents/orion-code@0.3.
|
|
178
|
+
npm install -g @orion-agents/orion-code@0.3.27
|
|
177
179
|
orion --version
|
|
178
180
|
orion doctor
|
|
179
181
|
```
|
package/README.zh-CN.md
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
面向终端与浏览器、本地优先的目标驱动 Coding Agent。
|
|
4
4
|
|
|
5
|
-
> v0.3.
|
|
6
|
-
>
|
|
7
|
-
>
|
|
8
|
-
>
|
|
5
|
+
> v0.3.27 候选版本:进行中。专业代码审查闭环首切片:共享审查契约(selector、状态、结论、
|
|
6
|
+
> finding、只收紧预算、receipt digest、stale 标注)已冻结并通过契约测试;共享 Git snapshot
|
|
7
|
+
> reader 捕获 worktree 与冻结 base 两种范围并给出类型化错误;只读 workflow 约束默认拒绝一切
|
|
8
|
+
> 非读取工具。Runtime/CLI 接线、Web 闭环与资格验证为同一计划的后续切片。上一版 0.3.26 已发布
|
|
9
|
+
> npm(registry `latest`)、打 tag `v0.3.26`(`b810996`,#278)并发布 GitHub Release。
|
|
9
10
|
|
|
10
11
|
[English](README.md) ·
|
|
11
12
|
[v0.3.24 方案](docs/plan/v0.3.24-plan.md) ·
|
|
@@ -100,10 +101,10 @@ MCP 仍是用户可见的扩展边界。
|
|
|
100
101
|
支持 Node.js 22.12+、24 和 26。生产环境建议使用 Node 24 LTS,当前开发环境也支持 Node 26
|
|
101
102
|
Current。Node 20 已结束上游维护,不再属于 v0.3 Runtime 合同。
|
|
102
103
|
|
|
103
|
-
v0.3.
|
|
104
|
+
v0.3.27 在 npm 可用后,可安装固定版本:
|
|
104
105
|
|
|
105
106
|
```bash
|
|
106
|
-
npm install -g @orion-agents/orion-code@0.3.
|
|
107
|
+
npm install -g @orion-agents/orion-code@0.3.27
|
|
107
108
|
orion --version
|
|
108
109
|
orion doctor
|
|
109
110
|
```
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Read-only tools the review turn may use. Only first-party core tools with
|
|
2
|
+
* `readOnly: true` in their descriptors qualify (plan §5.1); `edit_file`,
|
|
3
|
+
* `exec_command` and `write_file` are structurally excluded. */
|
|
4
|
+
export declare const REVIEW_ALLOWED_TOOLS: ReadonlySet<string>;
|
|
5
|
+
export interface ReviewWorkflowConstraintsV1 {
|
|
6
|
+
readonly schemaVersion: 1;
|
|
7
|
+
readonly workflow: 'code-review';
|
|
8
|
+
readonly allowedTools: readonly string[];
|
|
9
|
+
readonly constraintsDigest: string;
|
|
10
|
+
readonly policy: string;
|
|
11
|
+
}
|
|
12
|
+
/** The frozen constraint set with its digest. Receipts and capability
|
|
13
|
+
* compilations bind this digest so a policy change invalidates old reviews'
|
|
14
|
+
* rules provenance alongside the rules digest. */
|
|
15
|
+
export declare const REVIEW_WORKFLOW_CONSTRAINTS: ReviewWorkflowConstraintsV1;
|
|
16
|
+
export declare class ReviewToolPolicyError extends Error {
|
|
17
|
+
readonly code: 'review_tool_denied';
|
|
18
|
+
readonly toolName: string;
|
|
19
|
+
constructor(message: string, code: 'review_tool_denied', toolName: string);
|
|
20
|
+
}
|
|
21
|
+
/** Dispatch-level re-validation. Called on every tool invocation of a review
|
|
22
|
+
* turn — direct calls, batch calls, aliases and steering all funnel through
|
|
23
|
+
* here before the gateway executes anything. */
|
|
24
|
+
export declare function assertReviewToolAllowed(toolName: string): void;
|
|
25
|
+
//# sourceMappingURL=constraints.d.ts.map
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ReviewToolPolicyError = exports.REVIEW_WORKFLOW_CONSTRAINTS = exports.REVIEW_ALLOWED_TOOLS = void 0;
|
|
4
|
+
exports.assertReviewToolAllowed = assertReviewToolAllowed;
|
|
5
|
+
/**
|
|
6
|
+
* v0.3.27 PR1 — read-only workflow constraints for code review (plan §5.1).
|
|
7
|
+
*
|
|
8
|
+
* The review turn runs through the normal AgentLoop / ToolGateway, but under
|
|
9
|
+
* immutable workflow constraints: the model only sees bounded read/list/glob/
|
|
10
|
+
* grep capabilities, and every dispatch re-validates against this module so
|
|
11
|
+
* aliases, batch calls, steering or permission escalation (AUTO / ALLOW) can
|
|
12
|
+
* never widen the turn. The constraint set is deny-by-default: a tool that is
|
|
13
|
+
* not explicitly allowlisted is rejected, which covers long-tail tools, MCP,
|
|
14
|
+
* network, task derivation and future additions without maintaining a denylist.
|
|
15
|
+
*
|
|
16
|
+
* PR2 wires this guard into the review workflow's dispatch; PR1 ships the
|
|
17
|
+
* frozen model, its digest, and the negative tests.
|
|
18
|
+
*/
|
|
19
|
+
const canonical_1 = require("../../runtime/protocol/canonical");
|
|
20
|
+
/** Read-only tools the review turn may use. Only first-party core tools with
|
|
21
|
+
* `readOnly: true` in their descriptors qualify (plan §5.1); `edit_file`,
|
|
22
|
+
* `exec_command` and `write_file` are structurally excluded. */
|
|
23
|
+
exports.REVIEW_ALLOWED_TOOLS = new Set([
|
|
24
|
+
'read_file',
|
|
25
|
+
'list_files',
|
|
26
|
+
'glob',
|
|
27
|
+
'grep',
|
|
28
|
+
]);
|
|
29
|
+
const REVIEW_CONSTRAINT_CONTENT = Object.freeze({
|
|
30
|
+
schemaVersion: 1,
|
|
31
|
+
workflow: 'code-review',
|
|
32
|
+
mode: 'read-only',
|
|
33
|
+
allowedTools: [...exports.REVIEW_ALLOWED_TOOLS].sort(),
|
|
34
|
+
deniedByDefault: Object.freeze([
|
|
35
|
+
'exec_command',
|
|
36
|
+
'write_file',
|
|
37
|
+
'edit_file',
|
|
38
|
+
'git mutations',
|
|
39
|
+
'mcp',
|
|
40
|
+
'network tools',
|
|
41
|
+
'dynamic skills',
|
|
42
|
+
'task derivation',
|
|
43
|
+
'settings mutation',
|
|
44
|
+
]),
|
|
45
|
+
/** Rationale text is part of the digest: changing the policy changes every
|
|
46
|
+
* receipt that binds it. */
|
|
47
|
+
policy: 'The review turn may only inspect the frozen snapshot through the ' +
|
|
48
|
+
'allowlisted read tools. Permission escalation (AUTO / ALLOW) may tighten ' +
|
|
49
|
+
'or maintain but never widen this set. Test commands are not run: they ' +
|
|
50
|
+
'can write caches and mutate the workspace under review.',
|
|
51
|
+
});
|
|
52
|
+
/** The frozen constraint set with its digest. Receipts and capability
|
|
53
|
+
* compilations bind this digest so a policy change invalidates old reviews'
|
|
54
|
+
* rules provenance alongside the rules digest. */
|
|
55
|
+
exports.REVIEW_WORKFLOW_CONSTRAINTS = Object.freeze({
|
|
56
|
+
schemaVersion: 1,
|
|
57
|
+
workflow: 'code-review',
|
|
58
|
+
allowedTools: REVIEW_CONSTRAINT_CONTENT.allowedTools,
|
|
59
|
+
constraintsDigest: (0, canonical_1.digestRuntimeValue)(REVIEW_CONSTRAINT_CONTENT),
|
|
60
|
+
policy: REVIEW_CONSTRAINT_CONTENT.policy,
|
|
61
|
+
});
|
|
62
|
+
class ReviewToolPolicyError extends Error {
|
|
63
|
+
constructor(message, code, toolName) {
|
|
64
|
+
super(message);
|
|
65
|
+
this.code = code;
|
|
66
|
+
this.toolName = toolName;
|
|
67
|
+
this.name = 'ReviewToolPolicyError';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
exports.ReviewToolPolicyError = ReviewToolPolicyError;
|
|
71
|
+
/** Dispatch-level re-validation. Called on every tool invocation of a review
|
|
72
|
+
* turn — direct calls, batch calls, aliases and steering all funnel through
|
|
73
|
+
* here before the gateway executes anything. */
|
|
74
|
+
function assertReviewToolAllowed(toolName) {
|
|
75
|
+
if (!exports.REVIEW_ALLOWED_TOOLS.has(toolName)) {
|
|
76
|
+
throw new ReviewToolPolicyError(`Tool "${toolName}" is not available in a read-only code review turn.`, 'review_tool_denied', toolName);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=constraints.js.map
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/** v0.3.27 §3.1 — what a review examines. Parsed once, frozen at admission. */
|
|
2
|
+
export type ReviewSelectorV1 = {
|
|
3
|
+
readonly kind: 'worktree';
|
|
4
|
+
readonly paths?: readonly string[];
|
|
5
|
+
} | {
|
|
6
|
+
readonly kind: 'base';
|
|
7
|
+
readonly ref: string;
|
|
8
|
+
readonly paths?: readonly string[];
|
|
9
|
+
};
|
|
10
|
+
/** v0.3.27 §4.1 — run states. `queued`/`running` never carry a conclusion. */
|
|
11
|
+
export type ReviewRunStateV1 = 'queued' | 'running' | 'completed' | 'partial' | 'failed' | 'cancelled';
|
|
12
|
+
/** v0.3.27 §4.1 — terminal conclusions. Absence can only be asserted when the
|
|
13
|
+
* state is `completed`, coverage is complete and the output contained no
|
|
14
|
+
* invalid findings; everything else maps to findings or inconclusive. */
|
|
15
|
+
export type ReviewConclusionV1 = 'findings' | 'no_findings' | 'inconclusive';
|
|
16
|
+
/** v0.3.27 §6.1 — a stored result's applicability after code or rules change.
|
|
17
|
+
* History is never rewritten; staleness is an annotation, not an edit. */
|
|
18
|
+
export type ReviewApplicabilityV1 = 'current' | 'stale';
|
|
19
|
+
/** Priority levels. P0 requires evidence that the defect blocks usage without
|
|
20
|
+
* special preconditions; the definition is frozen here and in PR0 tests. */
|
|
21
|
+
export type ReviewFindingPriorityV1 = 'P0' | 'P1' | 'P2' | 'P3';
|
|
22
|
+
/** v0.3.27 §4.1 — one review finding. `fileKey` is validated for shape here;
|
|
23
|
+
* membership against the frozen snapshot manifest happens at Host validation
|
|
24
|
+
* time (PR1+), never in the model's favor. */
|
|
25
|
+
export interface ReviewFindingV1 {
|
|
26
|
+
readonly findingId: string;
|
|
27
|
+
readonly priority: ReviewFindingPriorityV1;
|
|
28
|
+
readonly title: string;
|
|
29
|
+
/** The condition under which the defect manifests — not a style opinion. */
|
|
30
|
+
readonly trigger: string;
|
|
31
|
+
readonly impact: string;
|
|
32
|
+
readonly fileKey: string;
|
|
33
|
+
readonly side: 'old' | 'new';
|
|
34
|
+
readonly startLine: number;
|
|
35
|
+
readonly endLine: number;
|
|
36
|
+
readonly evidence: string;
|
|
37
|
+
}
|
|
38
|
+
/** Coverage is evidence, not a count: what was actually inspected, what was
|
|
39
|
+
* skipped and why, and where truncation or budget exhaustion occurred. */
|
|
40
|
+
export interface ReviewCoverageV1 {
|
|
41
|
+
readonly inspectedFiles: readonly string[];
|
|
42
|
+
readonly inspectedRanges: readonly {
|
|
43
|
+
readonly fileKey: string;
|
|
44
|
+
readonly startLine: number;
|
|
45
|
+
readonly endLine: number;
|
|
46
|
+
}[];
|
|
47
|
+
readonly skipped: readonly {
|
|
48
|
+
readonly fileKey: string;
|
|
49
|
+
readonly reason: 'over_budget_files' | 'over_budget_bytes' | 'binary' | 'sensitive' | 'unreadable' | 'changed_during_review' | 'other';
|
|
50
|
+
readonly detail?: string;
|
|
51
|
+
}[];
|
|
52
|
+
readonly truncated: boolean;
|
|
53
|
+
readonly budgetExhausted: readonly string[];
|
|
54
|
+
}
|
|
55
|
+
/** Whether the declared coverage is internally consistent and complete for the
|
|
56
|
+
* resolved selector — computed by the Host from the frozen manifest, never
|
|
57
|
+
* taken from the model's self-report (§4.1). */
|
|
58
|
+
export interface ReviewCoverageAssertionV1 {
|
|
59
|
+
readonly complete: boolean;
|
|
60
|
+
readonly reason?: string;
|
|
61
|
+
}
|
|
62
|
+
/** v0.3.27 §4.1 — the persisted receipt. `receiptDigest` covers the canonical
|
|
63
|
+
* form of everything above it and is recomputed on read. */
|
|
64
|
+
export interface CodeReviewResultV1 {
|
|
65
|
+
readonly schemaVersion: 1;
|
|
66
|
+
readonly reviewId: string;
|
|
67
|
+
readonly workspaceIdentity: string;
|
|
68
|
+
readonly sessionId: string;
|
|
69
|
+
readonly threadId: string;
|
|
70
|
+
readonly turnId: string;
|
|
71
|
+
readonly requestedSelector: ReviewSelectorV1;
|
|
72
|
+
/** The actually resolved target (commit OIDs, snapshot ids), not the request. */
|
|
73
|
+
readonly resolvedTarget: {
|
|
74
|
+
readonly kind: 'worktree' | 'base';
|
|
75
|
+
readonly baseOid?: string;
|
|
76
|
+
readonly headOid?: string;
|
|
77
|
+
readonly snapshotId: string;
|
|
78
|
+
};
|
|
79
|
+
/** Digest of the frozen input snapshot; a stored result is stale when this
|
|
80
|
+
* no longer matches the current snapshot digest for the same selector. */
|
|
81
|
+
readonly inputSnapshotDigest: string;
|
|
82
|
+
readonly rulesDigest: string;
|
|
83
|
+
readonly state: ReviewRunStateV1;
|
|
84
|
+
readonly conclusion: ReviewConclusionV1;
|
|
85
|
+
readonly findings: readonly ReviewFindingV1[];
|
|
86
|
+
/** Findings that failed validation — counted and surfaced, never silently
|
|
87
|
+
* dropped (dropping them must not turn the run into no_findings). */
|
|
88
|
+
readonly invalidFindingCount: number;
|
|
89
|
+
readonly coverage: ReviewCoverageV1;
|
|
90
|
+
readonly coverageAssertion: ReviewCoverageAssertionV1;
|
|
91
|
+
readonly limits: readonly string[];
|
|
92
|
+
readonly model: {
|
|
93
|
+
readonly provider: string;
|
|
94
|
+
readonly model: string;
|
|
95
|
+
};
|
|
96
|
+
readonly usage: {
|
|
97
|
+
readonly inputTokens: number;
|
|
98
|
+
readonly outputTokens: number;
|
|
99
|
+
};
|
|
100
|
+
readonly startedAt: number;
|
|
101
|
+
readonly endedAt: number;
|
|
102
|
+
readonly receiptDigest: string;
|
|
103
|
+
}
|
|
104
|
+
/** Budgets are ceilings that may only be tightened (§4.3): a provider with a
|
|
105
|
+
* smaller window takes the stricter value; nothing may widen them at runtime. */
|
|
106
|
+
export interface ReviewBudgetsV1 {
|
|
107
|
+
readonly maxFiles: number;
|
|
108
|
+
readonly maxBodyBytes: number;
|
|
109
|
+
readonly maxDiffBytes: number;
|
|
110
|
+
readonly maxReadToolCalls: number;
|
|
111
|
+
readonly maxDurationMs: number;
|
|
112
|
+
readonly maxFindings: number;
|
|
113
|
+
readonly maxResultBytes: number;
|
|
114
|
+
}
|
|
115
|
+
export declare const DEFAULT_REVIEW_BUDGETS: ReviewBudgetsV1;
|
|
116
|
+
/** Text size caps for finding fields, in characters. Chosen so 100 maximal
|
|
117
|
+
* findings stay well inside the 256 KiB result bound. */
|
|
118
|
+
export declare const FINDING_TEXT_LIMITS: Readonly<{
|
|
119
|
+
title: 200;
|
|
120
|
+
trigger: 2000;
|
|
121
|
+
impact: 2000;
|
|
122
|
+
evidence: 4000;
|
|
123
|
+
fileKey: 1024;
|
|
124
|
+
}>;
|
|
125
|
+
/** Validates one model-produced finding against the frozen shape. Returns the
|
|
126
|
+
* finding unchanged when valid; a typed reason when not. Membership against
|
|
127
|
+
* the snapshot manifest is a separate Host check (PR1+). */
|
|
128
|
+
export declare function validateReviewFinding(value: unknown): {
|
|
129
|
+
readonly valid: true;
|
|
130
|
+
readonly finding: ReviewFindingV1;
|
|
131
|
+
} | {
|
|
132
|
+
readonly valid: false;
|
|
133
|
+
readonly reason: 'not_an_object' | 'finding_id_invalid' | 'priority_invalid' | 'title_invalid' | 'trigger_invalid' | 'impact_invalid' | 'file_key_invalid' | 'side_invalid' | 'line_range_invalid' | 'evidence_invalid';
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* v0.3.27 §4.1 — the conclusion mapping, frozen as code so CLI and Web cannot
|
|
137
|
+
* diverge. The invariants:
|
|
138
|
+
* - `queued`/`running` results carry no conclusion (callers must not call this).
|
|
139
|
+
* - A state that is not `completed` can never conclude `no_findings`.
|
|
140
|
+
* - Invalid findings make the output untrustworthy: zero valid findings with
|
|
141
|
+
* invalid ones present is `inconclusive`, never a green pass.
|
|
142
|
+
* - Zero findings without complete coverage is `inconclusive`, never a green
|
|
143
|
+
* pass ("不静默截断后宣布全部完成").
|
|
144
|
+
*/
|
|
145
|
+
export declare function resolveReviewConclusion(input: {
|
|
146
|
+
readonly state: ReviewRunStateV1;
|
|
147
|
+
readonly validFindingCount: number;
|
|
148
|
+
readonly invalidFindingCount: number;
|
|
149
|
+
readonly coverageComplete: boolean;
|
|
150
|
+
}): ReviewConclusionV1;
|
|
151
|
+
/** Applies the budgets with the tighten-only rule: every override must be a
|
|
152
|
+
* safe integer at or below the frozen default, otherwise the run is refused
|
|
153
|
+
* rather than silently widened. */
|
|
154
|
+
export declare function resolveReviewBudgets(overrides?: Partial<ReviewBudgetsV1>): {
|
|
155
|
+
readonly ok: true;
|
|
156
|
+
readonly budgets: ReviewBudgetsV1;
|
|
157
|
+
} | {
|
|
158
|
+
readonly ok: false;
|
|
159
|
+
readonly field: string;
|
|
160
|
+
};
|
|
161
|
+
/** Marks a stored result's applicability against the current input snapshot and
|
|
162
|
+
* rules digests. The receipt itself is never rewritten. */
|
|
163
|
+
export declare function reviewApplicability(result: Pick<CodeReviewResultV1, 'inputSnapshotDigest' | 'rulesDigest'>, current: {
|
|
164
|
+
readonly inputSnapshotDigest: string;
|
|
165
|
+
readonly rulesDigest: string;
|
|
166
|
+
}): ReviewApplicabilityV1;
|
|
167
|
+
/** The receipt digest covers the canonical form of the result with the digest
|
|
168
|
+
* field itself excluded, so verification is a recomputation, not a lookup. */
|
|
169
|
+
export declare function computeReceiptDigest(result: Omit<CodeReviewResultV1, 'receiptDigest'>): string;
|
|
170
|
+
/** Verifies a stored receipt by recomputing its digest. */
|
|
171
|
+
export declare function verifyReceiptDigest(result: CodeReviewResultV1): boolean;
|
|
172
|
+
//# sourceMappingURL=contract.d.ts.map
|
|
@@ -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,64 @@
|
|
|
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
|
+
/** Resolves the selector to a frozen snapshot. Every OID and digest in the
|
|
57
|
+
* returned object was taken at admission time; callers persist the returned
|
|
58
|
+
* digest as the review's input identity. */
|
|
59
|
+
export declare function captureReviewSnapshot(options: ReviewSnapshotOptionsV1): Promise<CodeReviewSnapshotV1>;
|
|
60
|
+
/** Reads one working-tree file for review purposes, refusing to leave the
|
|
61
|
+
* repository root (same containment discipline as the v0.3.25 conflict
|
|
62
|
+
* reader). Returns null when the file is missing or unreadable. */
|
|
63
|
+
export declare function createContainedWorktreeReader(rootInput: string): (relativePath: string) => Promise<string | null>;
|
|
64
|
+
//# sourceMappingURL=snapshot.d.ts.map
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ReviewSnapshotError = void 0;
|
|
4
|
+
exports.captureReviewSnapshot = captureReviewSnapshot;
|
|
5
|
+
exports.createContainedWorktreeReader = createContainedWorktreeReader;
|
|
6
|
+
/**
|
|
7
|
+
* v0.3.27 PR1 — the minimal shared Git snapshot reader for code review
|
|
8
|
+
* (plan §4.2). One reader, two scopes:
|
|
9
|
+
*
|
|
10
|
+
* - `worktree`: staged, unstaged and untracked changes are recorded as
|
|
11
|
+
* separate sources for the same path and are never merged into one
|
|
12
|
+
* composite diff.
|
|
13
|
+
* - `base`: the ref, HEAD and merge-base are resolved to immutable OIDs at
|
|
14
|
+
* admission; later ref movement cannot change the frozen input.
|
|
15
|
+
*
|
|
16
|
+
* Security posture (§4.2.7): git runs with argument arrays through the read
|
|
17
|
+
* profile, never a shell; external diff drivers and textconv are disabled;
|
|
18
|
+
* blobs are read by OID; untracked working files are read directly under the
|
|
19
|
+
* same sensitive/binary/boundary policies as everything else. No optional
|
|
20
|
+
* locks, no arbitrary subcommands exposed to the model.
|
|
21
|
+
*/
|
|
22
|
+
const crypto_1 = require("crypto");
|
|
23
|
+
const fs_1 = require("fs");
|
|
24
|
+
const promises_1 = require("fs/promises");
|
|
25
|
+
const path_1 = require("path");
|
|
26
|
+
const redaction_1 = require("../redaction");
|
|
27
|
+
const contract_1 = require("./contract");
|
|
28
|
+
class ReviewSnapshotError extends Error {
|
|
29
|
+
constructor(message, code) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.code = code;
|
|
32
|
+
this.name = 'ReviewSnapshotError';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
exports.ReviewSnapshotError = ReviewSnapshotError;
|
|
36
|
+
function sha256(value) {
|
|
37
|
+
return (0, crypto_1.createHash)('sha256').update(value, 'utf8').digest('hex');
|
|
38
|
+
}
|
|
39
|
+
function assertSafePath(value) {
|
|
40
|
+
if (!value ||
|
|
41
|
+
value.length > 1024 ||
|
|
42
|
+
value.startsWith('/') ||
|
|
43
|
+
value.includes('\0') ||
|
|
44
|
+
value.split('/').includes('..')) {
|
|
45
|
+
throw new ReviewSnapshotError(`Unsafe review path: ${value}`, 'path_invalid');
|
|
46
|
+
}
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
/** Parses `git diff --name-status -z` records into status/path groups. */
|
|
50
|
+
function parseNameStatusZ(records) {
|
|
51
|
+
const parsed = [];
|
|
52
|
+
let index = 0;
|
|
53
|
+
while (index < records.length) {
|
|
54
|
+
const record = records[index];
|
|
55
|
+
if (!record) {
|
|
56
|
+
index += 1;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const status = record.slice(0, 1);
|
|
60
|
+
const paths = [];
|
|
61
|
+
if (status === 'R' || status === 'C') {
|
|
62
|
+
paths.push(records[index + 1] ?? '', records[index + 2] ?? '');
|
|
63
|
+
index += 3;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
paths.push(records[index + 1] ?? '');
|
|
67
|
+
index += 2;
|
|
68
|
+
}
|
|
69
|
+
parsed.push({ status, paths });
|
|
70
|
+
}
|
|
71
|
+
return parsed;
|
|
72
|
+
}
|
|
73
|
+
function parseDiffHunks(diffText) {
|
|
74
|
+
const files = [];
|
|
75
|
+
let current;
|
|
76
|
+
const HUNK = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)$/;
|
|
77
|
+
for (const line of diffText.split('\n')) {
|
|
78
|
+
if (line.startsWith('diff --git ')) {
|
|
79
|
+
const rest = line.slice('diff --git '.length);
|
|
80
|
+
const split = rest.indexOf(' b/');
|
|
81
|
+
current = {
|
|
82
|
+
path: split >= 0 ? rest.slice(split + 3) : rest,
|
|
83
|
+
hunks: [],
|
|
84
|
+
diffBytes: 0,
|
|
85
|
+
};
|
|
86
|
+
files.push(current);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (!current)
|
|
90
|
+
continue;
|
|
91
|
+
current.diffBytes += line.length + 1;
|
|
92
|
+
const header = HUNK.exec(line);
|
|
93
|
+
if (header) {
|
|
94
|
+
current.hunks.push({
|
|
95
|
+
oldStart: Number(header[1]),
|
|
96
|
+
oldLines: header[2] === undefined ? 1 : Number(header[2]),
|
|
97
|
+
newStart: Number(header[3]),
|
|
98
|
+
newLines: header[4] === undefined ? 1 : Number(header[4]),
|
|
99
|
+
header: line,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return files;
|
|
104
|
+
}
|
|
105
|
+
function pathMatches(path, requestedPaths) {
|
|
106
|
+
if (requestedPaths.length === 0)
|
|
107
|
+
return true;
|
|
108
|
+
return requestedPaths.some(filter => path === filter || path.startsWith(`${filter}/`));
|
|
109
|
+
}
|
|
110
|
+
/** Resolves the selector to a frozen snapshot. Every OID and digest in the
|
|
111
|
+
* returned object was taken at admission time; callers persist the returned
|
|
112
|
+
* digest as the review's input identity. */
|
|
113
|
+
async function captureReviewSnapshot(options) {
|
|
114
|
+
const budgets = { ...contract_1.DEFAULT_REVIEW_BUDGETS, ...options.budgets };
|
|
115
|
+
const { runGit } = options;
|
|
116
|
+
const requestedPaths = (options.selector.paths ?? []).map(assertSafePath);
|
|
117
|
+
let baseOid = null;
|
|
118
|
+
let headOid = null;
|
|
119
|
+
let nameStatusArgs;
|
|
120
|
+
let unifiedArgs;
|
|
121
|
+
if (options.selector.kind === 'base') {
|
|
122
|
+
headOid = (await runGit(['rev-parse', '--verify', 'HEAD'])).trim();
|
|
123
|
+
const refOid = (await runGit(['rev-parse', '--verify', `${options.selector.ref}^{commit}`]).catch(() => '')).trim();
|
|
124
|
+
if (!refOid) {
|
|
125
|
+
throw new ReviewSnapshotError(`Review base ref could not be resolved: ${options.selector.ref}`, 'ref_unresolved');
|
|
126
|
+
}
|
|
127
|
+
baseOid = (await runGit(['merge-base', refOid, headOid]).catch(() => '')).trim();
|
|
128
|
+
if (!baseOid) {
|
|
129
|
+
throw new ReviewSnapshotError(`No merge base between ${options.selector.ref} and HEAD`, 'no_merge_base');
|
|
130
|
+
}
|
|
131
|
+
nameStatusArgs = [
|
|
132
|
+
'diff',
|
|
133
|
+
'--no-ext-diff',
|
|
134
|
+
'--no-textconv',
|
|
135
|
+
'--find-renames',
|
|
136
|
+
'--name-status',
|
|
137
|
+
'-z',
|
|
138
|
+
baseOid,
|
|
139
|
+
headOid,
|
|
140
|
+
];
|
|
141
|
+
unifiedArgs = ['diff', '--no-ext-diff', '--no-textconv', baseOid, headOid];
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
nameStatusArgs = [
|
|
145
|
+
'diff',
|
|
146
|
+
'--no-ext-diff',
|
|
147
|
+
'--no-textconv',
|
|
148
|
+
'--find-renames',
|
|
149
|
+
'--name-status',
|
|
150
|
+
'-z',
|
|
151
|
+
'HEAD',
|
|
152
|
+
];
|
|
153
|
+
unifiedArgs = ['diff', '--no-ext-diff', '--no-textconv', 'HEAD'];
|
|
154
|
+
}
|
|
155
|
+
const nameStatusRaw = await runGit(nameStatusArgs);
|
|
156
|
+
const changed = parseNameStatusZ(nameStatusRaw.split('\0')).filter(entry => {
|
|
157
|
+
const path = entry.paths.at(-1);
|
|
158
|
+
return Boolean(path) && pathMatches(path, requestedPaths);
|
|
159
|
+
});
|
|
160
|
+
const files = [];
|
|
161
|
+
const skipped = [];
|
|
162
|
+
const seenPaths = new Set();
|
|
163
|
+
for (const entry of changed) {
|
|
164
|
+
const path = assertSafePath(entry.paths.at(-1));
|
|
165
|
+
if (seenPaths.has(path))
|
|
166
|
+
continue;
|
|
167
|
+
if (files.length >= budgets.maxFiles) {
|
|
168
|
+
skipped.push({ path, reason: 'over_budget_files' });
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
seenPaths.add(path);
|
|
172
|
+
const oldPath = entry.status === 'R' || entry.status === 'C' ? (entry.paths[0] ?? null) : null;
|
|
173
|
+
const sensitive = (0, redaction_1.isSensitiveFilePath)(path) || (oldPath ? (0, redaction_1.isSensitiveFilePath)(oldPath) : false);
|
|
174
|
+
if (sensitive) {
|
|
175
|
+
skipped.push({ path, reason: 'sensitive' });
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (entry.status === 'B') {
|
|
179
|
+
skipped.push({ path, reason: 'binary' });
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
files.push({
|
|
183
|
+
fileKey: sha256(`${entry.status}:${oldPath ?? ''}:${path}`).slice(0, 16),
|
|
184
|
+
path,
|
|
185
|
+
oldPath,
|
|
186
|
+
sensitive: false,
|
|
187
|
+
sides: [],
|
|
188
|
+
hunks: [],
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
const parsedFiles = parseDiffHunks(await runGit(unifiedArgs));
|
|
192
|
+
for (const file of files) {
|
|
193
|
+
const match = parsedFiles.find(candidate => candidate.path === file.path);
|
|
194
|
+
if (match)
|
|
195
|
+
file.hunks.push(...match.hunks);
|
|
196
|
+
}
|
|
197
|
+
for (const file of files) {
|
|
198
|
+
const totalDiff = file.hunks.reduce((sum, hunk) => sum + hunk.oldLines + hunk.newLines, 0);
|
|
199
|
+
if (totalDiff * 128 > budgets.maxDiffBytes) {
|
|
200
|
+
skipped.push({ path: file.path, reason: 'over_budget_bytes' });
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (options.selector.kind === 'base') {
|
|
204
|
+
const headOidForPath = (await runGit(['rev-parse', '--verify', `${headOid}:${file.path}`]).catch(() => '')).trim();
|
|
205
|
+
const baseOidForPath = (await runGit([
|
|
206
|
+
'rev-parse',
|
|
207
|
+
'--verify',
|
|
208
|
+
`${baseOid}:${file.oldPath ?? file.path}`,
|
|
209
|
+
]).catch(() => '')).trim();
|
|
210
|
+
if (headOidForPath) {
|
|
211
|
+
file.sides.push({
|
|
212
|
+
source: 'head',
|
|
213
|
+
oid: headOidForPath,
|
|
214
|
+
contentDigest: headOidForPath,
|
|
215
|
+
byteSize: Number((await runGit(['cat-file', '-s', headOidForPath])).trim()) || 0,
|
|
216
|
+
truncated: false,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
if (baseOidForPath) {
|
|
220
|
+
file.sides.push({
|
|
221
|
+
source: 'base',
|
|
222
|
+
oid: baseOidForPath,
|
|
223
|
+
contentDigest: baseOidForPath,
|
|
224
|
+
byteSize: Number((await runGit(['cat-file', '-s', baseOidForPath])).trim()) || 0,
|
|
225
|
+
truncated: false,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
const indexOid = (await runGit(['ls-files', '-s', '-z', '--', file.path]).then(raw => {
|
|
231
|
+
const record = raw.split('\0').find(item => item.length > 0);
|
|
232
|
+
return record ? (record.split(' ')[1] ?? '') : '';
|
|
233
|
+
})).trim();
|
|
234
|
+
if (indexOid) {
|
|
235
|
+
file.sides.push({
|
|
236
|
+
source: 'staged',
|
|
237
|
+
oid: indexOid,
|
|
238
|
+
contentDigest: indexOid,
|
|
239
|
+
byteSize: Number((await runGit(['cat-file', '-s', indexOid])).trim()) || 0,
|
|
240
|
+
truncated: false,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
if (options.readWorktreeFile) {
|
|
244
|
+
const before = await options.readWorktreeFile(file.path);
|
|
245
|
+
if (before === null) {
|
|
246
|
+
file.sides.push({
|
|
247
|
+
source: 'unstaged',
|
|
248
|
+
oid: null,
|
|
249
|
+
contentDigest: sha256(''),
|
|
250
|
+
byteSize: 0,
|
|
251
|
+
truncated: false,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
const beforeDigest = sha256(before);
|
|
256
|
+
const after = await options.readWorktreeFile(file.path);
|
|
257
|
+
if (after === null || sha256(after) !== beforeDigest) {
|
|
258
|
+
throw new ReviewSnapshotError(`Working file changed while snapshotting: ${file.path}`, 'worktree_changed');
|
|
259
|
+
}
|
|
260
|
+
file.sides.push({
|
|
261
|
+
source: 'unstaged',
|
|
262
|
+
oid: null,
|
|
263
|
+
contentDigest: beforeDigest,
|
|
264
|
+
byteSize: Buffer.byteLength(before, 'utf8'),
|
|
265
|
+
truncated: before.length > budgets.maxBodyBytes,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const manifest = {
|
|
272
|
+
kind: options.selector.kind,
|
|
273
|
+
baseOid,
|
|
274
|
+
headOid,
|
|
275
|
+
requestedPaths,
|
|
276
|
+
files: files.map(file => ({
|
|
277
|
+
fileKey: file.fileKey,
|
|
278
|
+
path: file.path,
|
|
279
|
+
oldPath: file.oldPath,
|
|
280
|
+
sides: file.sides.map(side => ({ source: side.source, contentDigest: side.contentDigest })),
|
|
281
|
+
})),
|
|
282
|
+
};
|
|
283
|
+
const inputSnapshotDigest = sha256(JSON.stringify(manifest));
|
|
284
|
+
const snapshotId = `${options.snapshotIdPrefix ?? 'rev'}-${inputSnapshotDigest.slice(0, 16)}`;
|
|
285
|
+
return Object.freeze({
|
|
286
|
+
schemaVersion: 1,
|
|
287
|
+
snapshotId,
|
|
288
|
+
kind: options.selector.kind,
|
|
289
|
+
baseOid,
|
|
290
|
+
headOid,
|
|
291
|
+
requestedPaths,
|
|
292
|
+
files: Object.freeze(files.map(file => Object.freeze({
|
|
293
|
+
...file,
|
|
294
|
+
sides: Object.freeze(file.sides),
|
|
295
|
+
hunks: Object.freeze(file.hunks),
|
|
296
|
+
}))),
|
|
297
|
+
inputSnapshotDigest,
|
|
298
|
+
skipped: Object.freeze(skipped),
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
/** Reads one working-tree file for review purposes, refusing to leave the
|
|
302
|
+
* repository root (same containment discipline as the v0.3.25 conflict
|
|
303
|
+
* reader). Returns null when the file is missing or unreadable. */
|
|
304
|
+
function createContainedWorktreeReader(rootInput) {
|
|
305
|
+
const root = (0, fs_1.realpathSync)(rootInput);
|
|
306
|
+
return async (relativePath) => {
|
|
307
|
+
try {
|
|
308
|
+
const absolute = (0, path_1.resolve)(root, relativePath);
|
|
309
|
+
const fileReal = (0, fs_1.realpathSync)(absolute);
|
|
310
|
+
if (fileReal !== root && !fileReal.startsWith(root + path_1.sep))
|
|
311
|
+
return null;
|
|
312
|
+
if (!(0, fs_1.statSync)(fileReal).isFile())
|
|
313
|
+
return null;
|
|
314
|
+
return await (0, promises_1.readFile)(fileReal, 'utf8');
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
//# sourceMappingURL=snapshot.js.map
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orion-agents/orion-code",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.27",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@orion-agents/orion-code",
|
|
9
|
-
"version": "0.3.
|
|
9
|
+
"version": "0.3.27",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@clack/prompts": "^0.7.0",
|
|
@@ -4293,9 +4293,9 @@
|
|
|
4293
4293
|
"license": "ISC"
|
|
4294
4294
|
},
|
|
4295
4295
|
"node_modules/follow-redirects": {
|
|
4296
|
-
"version": "1.16.
|
|
4297
|
-
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.
|
|
4298
|
-
"integrity": "sha512-
|
|
4296
|
+
"version": "1.16.1",
|
|
4297
|
+
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.1.tgz",
|
|
4298
|
+
"integrity": "sha512-FNvFGzoMLWmE6Yj9spb/zjd7yiNCHiAW9/Tg9CXrQ8wuu32HtlJOwWO11OJafl5FfY3DxTdQ0vj42zU1kvv5jg==",
|
|
4299
4299
|
"funding": [
|
|
4300
4300
|
{
|
|
4301
4301
|
"type": "individual",
|
|
@@ -4563,13 +4563,13 @@
|
|
|
4563
4563
|
"license": "MIT"
|
|
4564
4564
|
},
|
|
4565
4565
|
"node_modules/handlebars": {
|
|
4566
|
-
"version": "4.7.
|
|
4567
|
-
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.
|
|
4568
|
-
"integrity": "sha512-
|
|
4566
|
+
"version": "4.7.10",
|
|
4567
|
+
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.10.tgz",
|
|
4568
|
+
"integrity": "sha512-P5VJMVM7qgBn6vjXMw8WG9uVI+ncf2pi72j4de4yz5ZULLj2RGqLYaKOYGsgyrViQ0tePOVlN1tDCCXXtFqXKg==",
|
|
4569
4569
|
"dev": true,
|
|
4570
4570
|
"license": "MIT",
|
|
4571
4571
|
"dependencies": {
|
|
4572
|
-
"minimist": "^1.2.
|
|
4572
|
+
"minimist": "^1.2.8",
|
|
4573
4573
|
"neo-async": "^2.6.2",
|
|
4574
4574
|
"source-map": "^0.6.1",
|
|
4575
4575
|
"wordwrap": "^1.0.0"
|
|
@@ -7147,9 +7147,9 @@
|
|
|
7147
7147
|
}
|
|
7148
7148
|
},
|
|
7149
7149
|
"node_modules/source-map-js": {
|
|
7150
|
-
"version": "1.2.
|
|
7151
|
-
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.
|
|
7152
|
-
"integrity": "sha512-
|
|
7150
|
+
"version": "1.2.2",
|
|
7151
|
+
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz",
|
|
7152
|
+
"integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==",
|
|
7153
7153
|
"dev": true,
|
|
7154
7154
|
"license": "BSD-3-Clause",
|
|
7155
7155
|
"engines": {
|