@bookedsolid/rea 0.10.2 → 0.10.3
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/dist/hooks/review-gate/audit.d.ts +131 -0
- package/dist/hooks/review-gate/audit.js +181 -0
- package/dist/hooks/review-gate/base-resolve.d.ts +155 -0
- package/dist/hooks/review-gate/base-resolve.js +247 -0
- package/dist/hooks/review-gate/cache.d.ts +108 -0
- package/dist/hooks/review-gate/cache.js +120 -0
- package/dist/hooks/review-gate/diff.d.ts +181 -0
- package/dist/hooks/review-gate/diff.js +232 -0
- package/dist/hooks/review-gate/index.d.ts +16 -6
- package/dist/hooks/review-gate/index.js +20 -6
- package/package.json +1 -1
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit-record emission + consumption for the review gate.
|
|
3
|
+
*
|
|
4
|
+
* ## Responsibilities
|
|
5
|
+
*
|
|
6
|
+
* 1. Emit `push.review.skipped` and `codex.review.skipped` records via the
|
|
7
|
+
* existing `appendAuditRecord()` helper. These are NEVER forgeable-
|
|
8
|
+
* verdict records (the push-review gate never consults them as Codex
|
|
9
|
+
* receipts), so they intentionally go through the `"other"`-stamped
|
|
10
|
+
* public path rather than the `"rea-cli"` dedicated writer.
|
|
11
|
+
*
|
|
12
|
+
* 2. Scan `.rea/audit.jsonl` for a qualifying `codex.review` receipt
|
|
13
|
+
* certifying a given `head_sha`. This is the TS equivalent of the
|
|
14
|
+
* bash core's `jq -R 'fromjson? | select(...)'` predicate
|
|
15
|
+
* (push-review-core.sh §959-966).
|
|
16
|
+
*
|
|
17
|
+
* ## Defect carry-forwards
|
|
18
|
+
*
|
|
19
|
+
* - **Defect P** (forgery rejection). The scan filter requires
|
|
20
|
+
* `emission_source ∈ {"rea-cli", "codex-cli"}`. The public
|
|
21
|
+
* `appendAuditRecord()` helper stamps `"other"`; only the dedicated
|
|
22
|
+
* `appendCodexReviewAuditRecord()` helper and the Codex CLI write
|
|
23
|
+
* `"rea-cli"` / `"codex-cli"`. Records with `emission_source: "other"`
|
|
24
|
+
* or missing the field entirely are rejected here.
|
|
25
|
+
*
|
|
26
|
+
* - **Defect U** (streaming-parse tolerance). Every line in `.rea/
|
|
27
|
+
* audit.jsonl` is parsed independently in a try/catch. A single
|
|
28
|
+
* corrupt line mid-file does NOT abort the scan — later lines still
|
|
29
|
+
* get a chance. Before 0.10.2 the bash `jq -e` scan would bail on the
|
|
30
|
+
* first unparseable line and miss every subsequent legitimate record.
|
|
31
|
+
*
|
|
32
|
+
* - **Verdict whitelist**. Only `verdict ∈ {"pass", "concerns"}` records
|
|
33
|
+
* satisfy the protected-path gate. `blocking` and `error` verdicts are
|
|
34
|
+
* receipts that a review HAPPENED but with a negative outcome, which
|
|
35
|
+
* does NOT unblock the push. Mirrors push-review-core.sh §964.
|
|
36
|
+
*/
|
|
37
|
+
import { type AuditRecord } from '../../audit/append.js';
|
|
38
|
+
import { type OsIdentity } from './metadata.js';
|
|
39
|
+
/** Tool-names the gate emits. Kept as constants so string-literal drift is caught at compile time. */
|
|
40
|
+
export declare const PUSH_REVIEW_SKIPPED_TOOL = "push.review.skipped";
|
|
41
|
+
export declare const CODEX_REVIEW_SKIPPED_TOOL = "codex.review.skipped";
|
|
42
|
+
export declare const PUSH_REVIEW_CACHE_HIT_TOOL = "push.review.cache.hit";
|
|
43
|
+
export declare const PUSH_REVIEW_CACHE_ERROR_TOOL = "push.review.cache.error";
|
|
44
|
+
/** Server-names for the emit paths — carry forward from bash §473/§639. */
|
|
45
|
+
export declare const ESCAPE_HATCH_SERVER = "rea.escape_hatch";
|
|
46
|
+
export declare const PUSH_REVIEW_SERVER = "rea.push_review";
|
|
47
|
+
/**
|
|
48
|
+
* Input shape for the `REA_SKIP_PUSH_REVIEW` escape hatch's audit record.
|
|
49
|
+
*
|
|
50
|
+
* The `os_identity` field is captured inside this module (not by the
|
|
51
|
+
* caller) so every emitter gets the same shape and failing fields degrade
|
|
52
|
+
* to empty strings uniformly. The pid/ppid numeric-not-string invariant
|
|
53
|
+
* (defect M) is enforced by `metadata.ts`.
|
|
54
|
+
*/
|
|
55
|
+
export interface SkipPushReviewAuditInput {
|
|
56
|
+
/** Repo root (the dir containing `.rea/`). */
|
|
57
|
+
baseDir: string;
|
|
58
|
+
/** `HEAD` SHA at the time of the skip. */
|
|
59
|
+
head_sha: string;
|
|
60
|
+
/** Current branch or empty string. */
|
|
61
|
+
branch: string;
|
|
62
|
+
/** The non-empty value of `REA_SKIP_PUSH_REVIEW` (the reason). */
|
|
63
|
+
reason: string;
|
|
64
|
+
/** The resolved git actor (email, then name, else empty). */
|
|
65
|
+
actor: string;
|
|
66
|
+
/**
|
|
67
|
+
* OS-identity fields. Optional — when absent, `collectOsIdentity()` runs
|
|
68
|
+
* and fills them. Tests inject a deterministic stub for snapshot stability.
|
|
69
|
+
*/
|
|
70
|
+
os_identity?: OsIdentity;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Emit the `push.review.skipped` audit record. Wraps the public
|
|
74
|
+
* `appendAuditRecord()` helper — emission_source lands as `"other"`.
|
|
75
|
+
*
|
|
76
|
+
* The skipped record is intentionally NOT a `codex.review` receipt: the
|
|
77
|
+
* push-review cache-gate scan rejects any record whose `tool_name` is not
|
|
78
|
+
* `codex.review` AND any record whose `emission_source` is not
|
|
79
|
+
* `rea-cli` / `codex-cli`. So this record is on the hash chain as
|
|
80
|
+
* forensic evidence but cannot be confused with a real Codex review.
|
|
81
|
+
*/
|
|
82
|
+
export declare function emitPushReviewSkipped(input: SkipPushReviewAuditInput): Promise<AuditRecord>;
|
|
83
|
+
/**
|
|
84
|
+
* Input shape for the `REA_SKIP_CODEX_REVIEW` (Codex-only) waiver.
|
|
85
|
+
*
|
|
86
|
+
* `metadata_source` records whether the skip metadata came from the
|
|
87
|
+
* pre-push stdin (`"prepush-stdin"`) or from a local HEAD fallback
|
|
88
|
+
* (`"local-fallback"`). Bash-core §594+§606.
|
|
89
|
+
*/
|
|
90
|
+
export interface SkipCodexReviewAuditInput {
|
|
91
|
+
baseDir: string;
|
|
92
|
+
head_sha: string;
|
|
93
|
+
target: string;
|
|
94
|
+
reason: string;
|
|
95
|
+
actor: string;
|
|
96
|
+
metadata_source: 'prepush-stdin' | 'local-fallback';
|
|
97
|
+
}
|
|
98
|
+
export declare function emitCodexReviewSkipped(input: SkipCodexReviewAuditInput): Promise<AuditRecord>;
|
|
99
|
+
/**
|
|
100
|
+
* Predicate: does this parsed JSON object qualify as a valid
|
|
101
|
+
* `codex.review` receipt for the given `head_sha`?
|
|
102
|
+
*
|
|
103
|
+
* Exported for unit tests; callers should usually use
|
|
104
|
+
* `hasValidCodexReview()` below.
|
|
105
|
+
*/
|
|
106
|
+
export declare function isQualifyingCodexReview(record: unknown, head_sha: string): boolean;
|
|
107
|
+
/**
|
|
108
|
+
* Scan `.rea/audit.jsonl` for a qualifying `codex.review` record matching
|
|
109
|
+
* the given `head_sha`. Returns true as soon as one is found.
|
|
110
|
+
*
|
|
111
|
+
* ## Defect U tolerance
|
|
112
|
+
*
|
|
113
|
+
* Each line is parsed independently via `JSON.parse` inside try/catch. A
|
|
114
|
+
* malformed line logs nothing and the scan continues. The bash fix in
|
|
115
|
+
* 0.10.2 was `jq -R 'fromjson?'`; we mirror the per-line behavior in
|
|
116
|
+
* native JS.
|
|
117
|
+
*
|
|
118
|
+
* ## Path safety
|
|
119
|
+
*
|
|
120
|
+
* The audit file is always `<baseDir>/.rea/audit.jsonl` — baseDir flows
|
|
121
|
+
* in from the caller and is the same resolved path used everywhere else.
|
|
122
|
+
* No caller-supplied path segments.
|
|
123
|
+
*
|
|
124
|
+
* ## Missing file
|
|
125
|
+
*
|
|
126
|
+
* ENOENT resolves to `false` (no receipt exists yet). Any other error
|
|
127
|
+
* propagates — the caller's policy is to fail-closed, and a permission
|
|
128
|
+
* error on the audit file is a distinct operational concern the caller
|
|
129
|
+
* should surface rather than silently mask as "no receipt".
|
|
130
|
+
*/
|
|
131
|
+
export declare function hasValidCodexReview(baseDir: string, head_sha: string): Promise<boolean>;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit-record emission + consumption for the review gate.
|
|
3
|
+
*
|
|
4
|
+
* ## Responsibilities
|
|
5
|
+
*
|
|
6
|
+
* 1. Emit `push.review.skipped` and `codex.review.skipped` records via the
|
|
7
|
+
* existing `appendAuditRecord()` helper. These are NEVER forgeable-
|
|
8
|
+
* verdict records (the push-review gate never consults them as Codex
|
|
9
|
+
* receipts), so they intentionally go through the `"other"`-stamped
|
|
10
|
+
* public path rather than the `"rea-cli"` dedicated writer.
|
|
11
|
+
*
|
|
12
|
+
* 2. Scan `.rea/audit.jsonl` for a qualifying `codex.review` receipt
|
|
13
|
+
* certifying a given `head_sha`. This is the TS equivalent of the
|
|
14
|
+
* bash core's `jq -R 'fromjson? | select(...)'` predicate
|
|
15
|
+
* (push-review-core.sh §959-966).
|
|
16
|
+
*
|
|
17
|
+
* ## Defect carry-forwards
|
|
18
|
+
*
|
|
19
|
+
* - **Defect P** (forgery rejection). The scan filter requires
|
|
20
|
+
* `emission_source ∈ {"rea-cli", "codex-cli"}`. The public
|
|
21
|
+
* `appendAuditRecord()` helper stamps `"other"`; only the dedicated
|
|
22
|
+
* `appendCodexReviewAuditRecord()` helper and the Codex CLI write
|
|
23
|
+
* `"rea-cli"` / `"codex-cli"`. Records with `emission_source: "other"`
|
|
24
|
+
* or missing the field entirely are rejected here.
|
|
25
|
+
*
|
|
26
|
+
* - **Defect U** (streaming-parse tolerance). Every line in `.rea/
|
|
27
|
+
* audit.jsonl` is parsed independently in a try/catch. A single
|
|
28
|
+
* corrupt line mid-file does NOT abort the scan — later lines still
|
|
29
|
+
* get a chance. Before 0.10.2 the bash `jq -e` scan would bail on the
|
|
30
|
+
* first unparseable line and miss every subsequent legitimate record.
|
|
31
|
+
*
|
|
32
|
+
* - **Verdict whitelist**. Only `verdict ∈ {"pass", "concerns"}` records
|
|
33
|
+
* satisfy the protected-path gate. `blocking` and `error` verdicts are
|
|
34
|
+
* receipts that a review HAPPENED but with a negative outcome, which
|
|
35
|
+
* does NOT unblock the push. Mirrors push-review-core.sh §964.
|
|
36
|
+
*/
|
|
37
|
+
import fs from 'node:fs/promises';
|
|
38
|
+
import path from 'node:path';
|
|
39
|
+
import { appendAuditRecord, InvocationStatus, Tier, } from '../../audit/append.js';
|
|
40
|
+
import { collectOsIdentity } from './metadata.js';
|
|
41
|
+
/** Tool-names the gate emits. Kept as constants so string-literal drift is caught at compile time. */
|
|
42
|
+
export const PUSH_REVIEW_SKIPPED_TOOL = 'push.review.skipped';
|
|
43
|
+
export const CODEX_REVIEW_SKIPPED_TOOL = 'codex.review.skipped';
|
|
44
|
+
export const PUSH_REVIEW_CACHE_HIT_TOOL = 'push.review.cache.hit';
|
|
45
|
+
export const PUSH_REVIEW_CACHE_ERROR_TOOL = 'push.review.cache.error';
|
|
46
|
+
/** Server-names for the emit paths — carry forward from bash §473/§639. */
|
|
47
|
+
export const ESCAPE_HATCH_SERVER = 'rea.escape_hatch';
|
|
48
|
+
export const PUSH_REVIEW_SERVER = 'rea.push_review';
|
|
49
|
+
/**
|
|
50
|
+
* Emit the `push.review.skipped` audit record. Wraps the public
|
|
51
|
+
* `appendAuditRecord()` helper — emission_source lands as `"other"`.
|
|
52
|
+
*
|
|
53
|
+
* The skipped record is intentionally NOT a `codex.review` receipt: the
|
|
54
|
+
* push-review cache-gate scan rejects any record whose `tool_name` is not
|
|
55
|
+
* `codex.review` AND any record whose `emission_source` is not
|
|
56
|
+
* `rea-cli` / `codex-cli`. So this record is on the hash chain as
|
|
57
|
+
* forensic evidence but cannot be confused with a real Codex review.
|
|
58
|
+
*/
|
|
59
|
+
export async function emitPushReviewSkipped(input) {
|
|
60
|
+
const osIdentity = input.os_identity ?? collectOsIdentity();
|
|
61
|
+
const metadata = {
|
|
62
|
+
head_sha: input.head_sha,
|
|
63
|
+
branch: input.branch,
|
|
64
|
+
reason: input.reason,
|
|
65
|
+
actor: input.actor,
|
|
66
|
+
verdict: 'skipped',
|
|
67
|
+
os_identity: osIdentity,
|
|
68
|
+
};
|
|
69
|
+
const record = {
|
|
70
|
+
tool_name: PUSH_REVIEW_SKIPPED_TOOL,
|
|
71
|
+
server_name: ESCAPE_HATCH_SERVER,
|
|
72
|
+
status: InvocationStatus.Allowed,
|
|
73
|
+
tier: Tier.Read,
|
|
74
|
+
metadata,
|
|
75
|
+
};
|
|
76
|
+
return appendAuditRecord(input.baseDir, record);
|
|
77
|
+
}
|
|
78
|
+
export async function emitCodexReviewSkipped(input) {
|
|
79
|
+
const metadata = {
|
|
80
|
+
head_sha: input.head_sha,
|
|
81
|
+
target: input.target,
|
|
82
|
+
reason: input.reason,
|
|
83
|
+
actor: input.actor,
|
|
84
|
+
verdict: 'skipped',
|
|
85
|
+
files_changed: null,
|
|
86
|
+
metadata_source: input.metadata_source,
|
|
87
|
+
};
|
|
88
|
+
const record = {
|
|
89
|
+
tool_name: CODEX_REVIEW_SKIPPED_TOOL,
|
|
90
|
+
server_name: ESCAPE_HATCH_SERVER,
|
|
91
|
+
status: InvocationStatus.Allowed,
|
|
92
|
+
tier: Tier.Read,
|
|
93
|
+
metadata,
|
|
94
|
+
};
|
|
95
|
+
return appendAuditRecord(input.baseDir, record);
|
|
96
|
+
}
|
|
97
|
+
/** Verdicts that satisfy the protected-path Codex-receipt gate. */
|
|
98
|
+
const ACCEPTABLE_VERDICTS = new Set(['pass', 'concerns']);
|
|
99
|
+
/** Emission sources that satisfy the protected-path Codex-receipt gate. */
|
|
100
|
+
const ACCEPTABLE_SOURCES = new Set(['rea-cli', 'codex-cli']);
|
|
101
|
+
/**
|
|
102
|
+
* Predicate: does this parsed JSON object qualify as a valid
|
|
103
|
+
* `codex.review` receipt for the given `head_sha`?
|
|
104
|
+
*
|
|
105
|
+
* Exported for unit tests; callers should usually use
|
|
106
|
+
* `hasValidCodexReview()` below.
|
|
107
|
+
*/
|
|
108
|
+
export function isQualifyingCodexReview(record, head_sha) {
|
|
109
|
+
if (record === null || typeof record !== 'object')
|
|
110
|
+
return false;
|
|
111
|
+
const r = record;
|
|
112
|
+
if (r.tool_name !== 'codex.review')
|
|
113
|
+
return false;
|
|
114
|
+
if (typeof r.emission_source !== 'string' || !ACCEPTABLE_SOURCES.has(r.emission_source)) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
const md = r.metadata;
|
|
118
|
+
if (md === null || md === undefined || typeof md !== 'object')
|
|
119
|
+
return false;
|
|
120
|
+
if (md.head_sha !== head_sha)
|
|
121
|
+
return false;
|
|
122
|
+
if (typeof md.verdict !== 'string' || !ACCEPTABLE_VERDICTS.has(md.verdict)) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Scan `.rea/audit.jsonl` for a qualifying `codex.review` record matching
|
|
129
|
+
* the given `head_sha`. Returns true as soon as one is found.
|
|
130
|
+
*
|
|
131
|
+
* ## Defect U tolerance
|
|
132
|
+
*
|
|
133
|
+
* Each line is parsed independently via `JSON.parse` inside try/catch. A
|
|
134
|
+
* malformed line logs nothing and the scan continues. The bash fix in
|
|
135
|
+
* 0.10.2 was `jq -R 'fromjson?'`; we mirror the per-line behavior in
|
|
136
|
+
* native JS.
|
|
137
|
+
*
|
|
138
|
+
* ## Path safety
|
|
139
|
+
*
|
|
140
|
+
* The audit file is always `<baseDir>/.rea/audit.jsonl` — baseDir flows
|
|
141
|
+
* in from the caller and is the same resolved path used everywhere else.
|
|
142
|
+
* No caller-supplied path segments.
|
|
143
|
+
*
|
|
144
|
+
* ## Missing file
|
|
145
|
+
*
|
|
146
|
+
* ENOENT resolves to `false` (no receipt exists yet). Any other error
|
|
147
|
+
* propagates — the caller's policy is to fail-closed, and a permission
|
|
148
|
+
* error on the audit file is a distinct operational concern the caller
|
|
149
|
+
* should surface rather than silently mask as "no receipt".
|
|
150
|
+
*/
|
|
151
|
+
export async function hasValidCodexReview(baseDir, head_sha) {
|
|
152
|
+
const auditFile = path.join(baseDir, '.rea', 'audit.jsonl');
|
|
153
|
+
let raw;
|
|
154
|
+
try {
|
|
155
|
+
raw = await fs.readFile(auditFile, 'utf8');
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
if (err.code === 'ENOENT')
|
|
159
|
+
return false;
|
|
160
|
+
throw err;
|
|
161
|
+
}
|
|
162
|
+
if (raw.length === 0)
|
|
163
|
+
return false;
|
|
164
|
+
// Walk lines. Each line is independently parsed; a corrupt line is
|
|
165
|
+
// silently skipped. A matching record short-circuits the scan.
|
|
166
|
+
for (const line of raw.split('\n')) {
|
|
167
|
+
if (line.length === 0)
|
|
168
|
+
continue;
|
|
169
|
+
let parsed;
|
|
170
|
+
try {
|
|
171
|
+
parsed = JSON.parse(line);
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
// Defect U tolerance — move on.
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (isQualifyingCodexReview(parsed, head_sha))
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base-ref resolution for the push-review gate.
|
|
3
|
+
*
|
|
4
|
+
* ## What "base resolution" means
|
|
5
|
+
*
|
|
6
|
+
* Given a pushed refspec (a `local_sha` + `remote_ref` pair, plus the
|
|
7
|
+
* remote name), determine:
|
|
8
|
+
*
|
|
9
|
+
* 1. the commit SHA the local changes should be diffed against
|
|
10
|
+
* (the "merge base"), and
|
|
11
|
+
* 2. the human-facing label for the `Target:` banner line
|
|
12
|
+
* (defect N semantic: the SEMANTIC base, not the refspec destination).
|
|
13
|
+
*
|
|
14
|
+
* The four code paths the bash core walked (push-review-core.sh §720-889):
|
|
15
|
+
*
|
|
16
|
+
* A. Tracked-branch push (`remote_sha != ZERO`). Use
|
|
17
|
+
* `git merge-base <remote_sha> <local_sha>`. Label = refspec target.
|
|
18
|
+
* B. New-branch push with `branch.<source>.base` config (defect N). The
|
|
19
|
+
* operator opted into a specific base. Prefer `refs/remotes/<remote>/
|
|
20
|
+
* <configured>` if it exists, else fall back to `refs/heads/<configured>`
|
|
21
|
+
* with a WARN on stderr. Label = configured base name.
|
|
22
|
+
* C. New-branch push without config, with `refs/remotes/<remote>/HEAD`
|
|
23
|
+
* resolvable. Use that symbolic-ref as the anchor. Label = refspec
|
|
24
|
+
* target (preserves the cache-key contract for bare pushes).
|
|
25
|
+
* D. Bootstrap: no config, no symbolic-ref, probe `main` then `master`.
|
|
26
|
+
* If both fail, anchor on the empty-tree SHA so the full push content
|
|
27
|
+
* is reviewable. Label = refspec target.
|
|
28
|
+
*
|
|
29
|
+
* ## Phase 2a scope (this file)
|
|
30
|
+
*
|
|
31
|
+
* `resolveBaseForRefspec()` composes the four paths via the `GitRunner`
|
|
32
|
+
* port from `diff.ts`. This module is pure in the same sense `diff.ts`
|
|
33
|
+
* is — every git hit goes through the injected runner, so unit tests
|
|
34
|
+
* enumerate the four paths without touching a real repo.
|
|
35
|
+
*
|
|
36
|
+
* Defect-N fail-loud (design §7) is Phase 4's final cutover and is NOT
|
|
37
|
+
* turned on here. `NoBaseResolvableError` is reserved in `errors.ts` but
|
|
38
|
+
* the empty-tree bootstrap remains the current production fallback.
|
|
39
|
+
* Phase 2b composes the final policy into `runPushReviewGate()`.
|
|
40
|
+
*/
|
|
41
|
+
import { type GitRunner } from './diff.js';
|
|
42
|
+
import type { RefspecRecord } from './args.js';
|
|
43
|
+
/**
|
|
44
|
+
* Resolved base outcome for a single refspec. Never thrown — callers
|
|
45
|
+
* translate blocked conditions (remote object missing, no merge-base)
|
|
46
|
+
* into `BlockedError` subclasses up the stack.
|
|
47
|
+
*/
|
|
48
|
+
export interface ResolvedBase {
|
|
49
|
+
/**
|
|
50
|
+
* The commit / tree SHA to diff against. Always set when
|
|
51
|
+
* `status === 'ok'`; otherwise null.
|
|
52
|
+
*/
|
|
53
|
+
merge_base: string | null;
|
|
54
|
+
/**
|
|
55
|
+
* The human-facing `Target:` label (defect N). The bash core defaults
|
|
56
|
+
* this to the refspec target and promotes it to the configured base's
|
|
57
|
+
* short name only when `branch.<source>.base` resolved. We mirror that.
|
|
58
|
+
*/
|
|
59
|
+
target_label: string;
|
|
60
|
+
/** Discriminator for the caller. */
|
|
61
|
+
status: 'ok' | 'remote_object_missing' | 'no_merge_base' | 'no_base_resolvable';
|
|
62
|
+
/**
|
|
63
|
+
* For the "tracked branch but the remote commit isn't locally present"
|
|
64
|
+
* path, return the remote SHA so the caller's banner can echo it. Empty
|
|
65
|
+
* otherwise.
|
|
66
|
+
*/
|
|
67
|
+
remote_sha?: string;
|
|
68
|
+
/**
|
|
69
|
+
* True when the configured-base branch was resolved via the LOCAL ref
|
|
70
|
+
* (`refs/heads/<configured>`) instead of the remote-tracking ref. The
|
|
71
|
+
* bash core prints a WARN in this case (push-review-core.sh §819-820).
|
|
72
|
+
* Phase 2a carries the signal; Phase 2b's composition emits the banner.
|
|
73
|
+
*/
|
|
74
|
+
local_ref_fallback_warning?: string;
|
|
75
|
+
/**
|
|
76
|
+
* The resolution path taken. Audit + debugging aid; never part of the
|
|
77
|
+
* cache key. Phase 2b's audit records include this for forensic trace.
|
|
78
|
+
*/
|
|
79
|
+
path: 'tracked' | 'new_branch_config' | 'new_branch_origin_head' | 'bootstrap_empty_tree';
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Deps for base resolution. Same `GitRunner` port `diff.ts` uses; plus the
|
|
83
|
+
* remote name (from the adapter's argv, defaults to `origin`). `cwd` is
|
|
84
|
+
* the resolved repo root.
|
|
85
|
+
*/
|
|
86
|
+
export interface ResolveBaseDeps {
|
|
87
|
+
runner: GitRunner;
|
|
88
|
+
cwd: string;
|
|
89
|
+
/** Remote name (`origin` by convention, but respect what git passed to the hook). */
|
|
90
|
+
remote: string;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Strip `refs/heads/` / `refs/for/` prefixes from a ref and return the
|
|
94
|
+
* trailing branch name. Used for the target-label normalization path
|
|
95
|
+
* where both ref families should collapse to a bare branch name for
|
|
96
|
+
* display. Exported for tests.
|
|
97
|
+
*/
|
|
98
|
+
export declare function stripRefsPrefix(ref: string): string;
|
|
99
|
+
/**
|
|
100
|
+
* Strip ONLY the `refs/heads/` prefix — leaves `refs/for/`, `refs/tags/`,
|
|
101
|
+
* and every other ref-namespace untouched. Mirrors the bash core's
|
|
102
|
+
* `${local_ref#refs/heads/}` on the source-branch lookup path (push-review-
|
|
103
|
+
* core.sh §797), so Gerrit-style pushes (`refs/for/main`) keep their
|
|
104
|
+
* namespace and do NOT accidentally match a `branch.main.base` config
|
|
105
|
+
* entry intended for a regular branch push.
|
|
106
|
+
*
|
|
107
|
+
* Codex pass-1 on Phase 2a flagged the earlier implementation that used
|
|
108
|
+
* `stripRefsPrefix` here — it would have promoted the Target: label for
|
|
109
|
+
* a Gerrit push against the reviewer's intent. Exported for tests.
|
|
110
|
+
*/
|
|
111
|
+
export declare function stripRefsHeadsOnly(ref: string): string;
|
|
112
|
+
/**
|
|
113
|
+
* Resolve the base anchor for a single push refspec. See the file-top
|
|
114
|
+
* docstring for the four code paths.
|
|
115
|
+
*
|
|
116
|
+
* Deletion refspecs (local_sha === ZERO_SHA) return `{merge_base: null,
|
|
117
|
+
* status: 'ok'}` with `path: 'tracked'` — the caller is expected to have
|
|
118
|
+
* already trapped deletions via `hasDeletion()` before calling here. We
|
|
119
|
+
* don't throw in that case because the caller owns the deletion policy,
|
|
120
|
+
* not this resolver.
|
|
121
|
+
*/
|
|
122
|
+
export declare function resolveBaseForRefspec(record: RefspecRecord, deps: ResolveBaseDeps): ResolvedBase;
|
|
123
|
+
/**
|
|
124
|
+
* Compute the initial `Target:` label for a refspec: the short name of
|
|
125
|
+
* the remote ref, falling back to `main` when it's empty (defensive;
|
|
126
|
+
* `args.ts` should never emit an empty remote_ref for a non-deletion).
|
|
127
|
+
*
|
|
128
|
+
* Exported for unit tests. Mirrors push-review-core.sh §725-727.
|
|
129
|
+
*/
|
|
130
|
+
export declare function computeInitialTargetLabel(record: RefspecRecord): string;
|
|
131
|
+
/**
|
|
132
|
+
* Inner helper: resolve the new-branch anchor via the B→C→D walk. Stays
|
|
133
|
+
* module-private so callers only see `resolveBaseForRefspec` as the
|
|
134
|
+
* public surface. Returns a discriminated union so the caller can branch
|
|
135
|
+
* on path + extract the warning / label cleanly.
|
|
136
|
+
*/
|
|
137
|
+
type NewBranchOutcome = {
|
|
138
|
+
kind: 'config_hit';
|
|
139
|
+
ref: string;
|
|
140
|
+
label: string;
|
|
141
|
+
/** Non-null when we fell back to `refs/heads/<base>` (§819-820 WARN). */
|
|
142
|
+
warning: string | null;
|
|
143
|
+
} | {
|
|
144
|
+
kind: 'origin_head';
|
|
145
|
+
ref: string;
|
|
146
|
+
} | {
|
|
147
|
+
kind: 'bootstrap';
|
|
148
|
+
};
|
|
149
|
+
/**
|
|
150
|
+
* Path B: consult `branch.<source>.base`. Returns `config_hit` iff a base
|
|
151
|
+
* was configured AND resolvable to a ref that exists. Falls through
|
|
152
|
+
* otherwise. Exported so tests can exercise the config-path independently.
|
|
153
|
+
*/
|
|
154
|
+
export declare function resolveNewBranchBase(sourceBranch: string, deps: ResolveBaseDeps): NewBranchOutcome;
|
|
155
|
+
export {};
|