@bookedsolid/rea 0.10.3 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.husky/pre-push +48 -162
- package/README.md +834 -552
- package/agents/codex-adversarial.md +5 -3
- package/commands/codex-review.md +3 -5
- package/dist/audit/append.d.ts +7 -32
- package/dist/audit/append.js +7 -35
- package/dist/cli/audit.d.ts +0 -31
- package/dist/cli/audit.js +5 -74
- package/dist/cli/doctor.d.ts +12 -0
- package/dist/cli/doctor.js +96 -17
- package/dist/cli/hook.d.ts +55 -0
- package/dist/cli/hook.js +138 -0
- package/dist/cli/index.js +5 -80
- package/dist/cli/init.js +1 -1
- package/dist/cli/install/gitignore.d.ts +2 -2
- package/dist/cli/install/gitignore.js +3 -3
- package/dist/cli/install/pre-push.d.ts +158 -272
- package/dist/cli/install/pre-push.js +491 -2633
- package/dist/cli/install/settings-merge.d.ts +17 -0
- package/dist/cli/install/settings-merge.js +48 -1
- package/dist/cli/upgrade.js +131 -3
- package/dist/config/tier-map.js +18 -25
- package/dist/hooks/push-gate/base.d.ts +104 -0
- package/dist/hooks/push-gate/base.js +198 -0
- package/dist/hooks/push-gate/codex-runner.d.ts +126 -0
- package/dist/hooks/push-gate/codex-runner.js +223 -0
- package/dist/hooks/push-gate/findings.d.ts +68 -0
- package/dist/hooks/push-gate/findings.js +142 -0
- package/dist/hooks/push-gate/halt.d.ts +28 -0
- package/dist/hooks/push-gate/halt.js +49 -0
- package/dist/hooks/push-gate/index.d.ts +98 -0
- package/dist/hooks/push-gate/index.js +416 -0
- package/dist/hooks/push-gate/policy.d.ts +55 -0
- package/dist/hooks/push-gate/policy.js +64 -0
- package/dist/hooks/push-gate/report.d.ts +89 -0
- package/dist/hooks/push-gate/report.js +140 -0
- package/dist/policy/loader.d.ts +15 -10
- package/dist/policy/loader.js +8 -6
- package/dist/policy/types.d.ts +73 -22
- package/package.json +1 -1
- package/scripts/tarball-smoke.sh +7 -2
- package/dist/cache/review-cache.d.ts +0 -115
- package/dist/cache/review-cache.js +0 -200
- package/dist/cli/cache.d.ts +0 -84
- package/dist/cli/cache.js +0 -150
- package/dist/hooks/review-gate/args.d.ts +0 -126
- package/dist/hooks/review-gate/args.js +0 -315
- package/dist/hooks/review-gate/audit.d.ts +0 -131
- package/dist/hooks/review-gate/audit.js +0 -181
- package/dist/hooks/review-gate/banner.d.ts +0 -97
- package/dist/hooks/review-gate/banner.js +0 -172
- package/dist/hooks/review-gate/base-resolve.d.ts +0 -155
- package/dist/hooks/review-gate/base-resolve.js +0 -247
- package/dist/hooks/review-gate/cache-key.d.ts +0 -55
- package/dist/hooks/review-gate/cache-key.js +0 -41
- package/dist/hooks/review-gate/cache.d.ts +0 -108
- package/dist/hooks/review-gate/cache.js +0 -120
- package/dist/hooks/review-gate/constants.d.ts +0 -26
- package/dist/hooks/review-gate/constants.js +0 -34
- package/dist/hooks/review-gate/diff.d.ts +0 -181
- package/dist/hooks/review-gate/diff.js +0 -232
- package/dist/hooks/review-gate/errors.d.ts +0 -72
- package/dist/hooks/review-gate/errors.js +0 -100
- package/dist/hooks/review-gate/hash.d.ts +0 -43
- package/dist/hooks/review-gate/hash.js +0 -46
- package/dist/hooks/review-gate/index.d.ts +0 -31
- package/dist/hooks/review-gate/index.js +0 -35
- package/dist/hooks/review-gate/metadata.d.ts +0 -98
- package/dist/hooks/review-gate/metadata.js +0 -158
- package/dist/hooks/review-gate/policy.d.ts +0 -55
- package/dist/hooks/review-gate/policy.js +0 -71
- package/dist/hooks/review-gate/protected-paths.d.ts +0 -46
- package/dist/hooks/review-gate/protected-paths.js +0 -76
- package/hooks/_lib/push-review-core.sh +0 -1250
- package/hooks/commit-review-gate.sh +0 -330
- package/hooks/push-review-gate-git.sh +0 -94
- package/hooks/push-review-gate.sh +0 -92
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Operator-facing banner composition.
|
|
3
|
-
*
|
|
4
|
-
* The bash core builds its banners via `printf` inside a `{ ... } >&2`
|
|
5
|
-
* block, counting diff lines with `grep -cE ...` and file changes with
|
|
6
|
-
* `grep -c '^+++ '`. Defect K (rea#62) surfaced because `grep -c`
|
|
7
|
-
* emits `0` to stdout AND exits non-zero on no-match, and the bash author
|
|
8
|
-
* wrote `$(grep -c ... || echo 0)` — which emitted `0\n0` when the pipe
|
|
9
|
-
* produced no matches. The fix in the bash core was `|| true` + default
|
|
10
|
-
* via `${LINE_COUNT:-0}`.
|
|
11
|
-
*
|
|
12
|
-
* The TS port closes this entire class of bug: counting happens over an
|
|
13
|
-
* actual string in Node, not via a pipe-on-a-side-effect. The only way
|
|
14
|
-
* LINE_COUNT / FILE_COUNT can ever be wrong now is a test-missed edge in
|
|
15
|
-
* `countChangedLines` or `countChangedFiles` — unit tests in `banner.test.ts`
|
|
16
|
-
* cover the zero case, the empty-diff case, the unicode-filename case, and
|
|
17
|
-
* the `+++ b/-file` edge explicitly.
|
|
18
|
-
*
|
|
19
|
-
* ## Format parity
|
|
20
|
-
*
|
|
21
|
-
* `renderPushReviewRequiredBanner` reproduces the byte-exact output of the
|
|
22
|
-
* bash core's "PUSH REVIEW GATE: Review required..." block, including the
|
|
23
|
-
* cache-disabled fallback branch. A fixture test in `banner.test.ts` asserts
|
|
24
|
-
* the output against a snapshot captured from the 0.10.1 bash core.
|
|
25
|
-
*/
|
|
26
|
-
export interface DiffStats {
|
|
27
|
-
/** Number of `^\+[^+]|^-[^-]` lines in the unified diff. */
|
|
28
|
-
line_count: number;
|
|
29
|
-
/** Number of `^\+\+\+ ` lines (one per changed file). */
|
|
30
|
-
file_count: number;
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Count lines that begin with `+` followed by a non-`+` character, OR `-`
|
|
34
|
-
* followed by a non-`-` character. Bash-core parity (push-review-core.sh
|
|
35
|
-
* §1082): `grep -cE '^\+[^+]|^-[^-]'`. This rejects every line whose
|
|
36
|
-
* SECOND character is the same as the first — not just `+++`/`---`
|
|
37
|
-
* headers, but also pathological `++foo` and `--bar` strings (which bash
|
|
38
|
-
* did not count). Codex pass-1 on phase 1 flagged the earlier too-lax
|
|
39
|
-
* char-1-only TS implementation that would have silently changed the
|
|
40
|
-
* Scope: banner line count vs. bash and broken phase-4 byte compatibility.
|
|
41
|
-
*
|
|
42
|
-
* Empty input → 0. Bare `+` or `-` (single char line) → 0, same as bash
|
|
43
|
-
* (the regex requires a second character).
|
|
44
|
-
*/
|
|
45
|
-
export declare function countChangedLines(diff: string): number;
|
|
46
|
-
/**
|
|
47
|
-
* Count `^\+\+\+ ` header lines (one per file in the diff). Parity with
|
|
48
|
-
* the bash core's `grep -c '^\+\+\+ '`.
|
|
49
|
-
*/
|
|
50
|
-
export declare function countChangedFiles(diff: string): number;
|
|
51
|
-
/**
|
|
52
|
-
* Compute `{line_count, file_count}` over a diff string. Exposed separately
|
|
53
|
-
* so callers can use just the stats without generating the full banner.
|
|
54
|
-
*/
|
|
55
|
-
export declare function computeDiffStats(diff: string): DiffStats;
|
|
56
|
-
export interface PushReviewRequiredBannerInput {
|
|
57
|
-
/** The ref being pushed (e.g. `refs/heads/feature/foo` or `HEAD`). */
|
|
58
|
-
source_ref: string;
|
|
59
|
-
/** The source commit SHA (12 chars + rest; full sha expected). */
|
|
60
|
-
source_sha: string;
|
|
61
|
-
/** Target branch / base label (defect N completion surfaces here). */
|
|
62
|
-
target_branch: string;
|
|
63
|
-
/** Resolved merge-base SHA. */
|
|
64
|
-
merge_base: string;
|
|
65
|
-
/** Diff stats — pre-computed by `computeDiffStats`. */
|
|
66
|
-
stats: DiffStats;
|
|
67
|
-
/**
|
|
68
|
-
* The sha256-of-diff cache key. When empty, the banner emits the
|
|
69
|
-
* cache-disabled fallback branch (`Cache is DISABLED on this host`).
|
|
70
|
-
*/
|
|
71
|
-
push_sha: string;
|
|
72
|
-
/** Source branch name for the `rea cache set` hint. */
|
|
73
|
-
source_branch: string;
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Compose the "PUSH REVIEW GATE: Review required before pushing" banner.
|
|
77
|
-
* Output goes to stderr via the caller; this function is pure. Returns the
|
|
78
|
-
* exact text the bash core would have printed (including trailing blank
|
|
79
|
-
* line and spacing), so the fixture snapshot can be compared byte-exactly.
|
|
80
|
-
*/
|
|
81
|
-
export declare function renderPushReviewRequiredBanner(input: PushReviewRequiredBannerInput): string;
|
|
82
|
-
export interface ProtectedPathsBlockedBannerInput {
|
|
83
|
-
source_ref: string;
|
|
84
|
-
source_sha: string;
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* Compose the "PUSH BLOCKED: protected paths changed — /codex-review
|
|
88
|
-
* required" banner. Pure; exit-2 translation happens in the CLI shim.
|
|
89
|
-
*/
|
|
90
|
-
export declare function renderProtectedPathsBlockedBanner(input: ProtectedPathsBlockedBannerInput): string;
|
|
91
|
-
/**
|
|
92
|
-
* Strip C0 control characters (0x00-0x1F, 0x7F) and C1 (0x80-0x9F) from a
|
|
93
|
-
* string. Used when a banner embeds text from a subprocess's stderr (e.g.
|
|
94
|
-
* the cache-check failure case). Mirrors the `LC_ALL=C tr -d` invocation
|
|
95
|
-
* in the bash core's cache-error path. Codex LOW 5 on the 0.9.4 pass.
|
|
96
|
-
*/
|
|
97
|
-
export declare function stripControlChars(input: string): string;
|
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Operator-facing banner composition.
|
|
3
|
-
*
|
|
4
|
-
* The bash core builds its banners via `printf` inside a `{ ... } >&2`
|
|
5
|
-
* block, counting diff lines with `grep -cE ...` and file changes with
|
|
6
|
-
* `grep -c '^+++ '`. Defect K (rea#62) surfaced because `grep -c`
|
|
7
|
-
* emits `0` to stdout AND exits non-zero on no-match, and the bash author
|
|
8
|
-
* wrote `$(grep -c ... || echo 0)` — which emitted `0\n0` when the pipe
|
|
9
|
-
* produced no matches. The fix in the bash core was `|| true` + default
|
|
10
|
-
* via `${LINE_COUNT:-0}`.
|
|
11
|
-
*
|
|
12
|
-
* The TS port closes this entire class of bug: counting happens over an
|
|
13
|
-
* actual string in Node, not via a pipe-on-a-side-effect. The only way
|
|
14
|
-
* LINE_COUNT / FILE_COUNT can ever be wrong now is a test-missed edge in
|
|
15
|
-
* `countChangedLines` or `countChangedFiles` — unit tests in `banner.test.ts`
|
|
16
|
-
* cover the zero case, the empty-diff case, the unicode-filename case, and
|
|
17
|
-
* the `+++ b/-file` edge explicitly.
|
|
18
|
-
*
|
|
19
|
-
* ## Format parity
|
|
20
|
-
*
|
|
21
|
-
* `renderPushReviewRequiredBanner` reproduces the byte-exact output of the
|
|
22
|
-
* bash core's "PUSH REVIEW GATE: Review required..." block, including the
|
|
23
|
-
* cache-disabled fallback branch. A fixture test in `banner.test.ts` asserts
|
|
24
|
-
* the output against a snapshot captured from the 0.10.1 bash core.
|
|
25
|
-
*/
|
|
26
|
-
/**
|
|
27
|
-
* Count lines that begin with `+` followed by a non-`+` character, OR `-`
|
|
28
|
-
* followed by a non-`-` character. Bash-core parity (push-review-core.sh
|
|
29
|
-
* §1082): `grep -cE '^\+[^+]|^-[^-]'`. This rejects every line whose
|
|
30
|
-
* SECOND character is the same as the first — not just `+++`/`---`
|
|
31
|
-
* headers, but also pathological `++foo` and `--bar` strings (which bash
|
|
32
|
-
* did not count). Codex pass-1 on phase 1 flagged the earlier too-lax
|
|
33
|
-
* char-1-only TS implementation that would have silently changed the
|
|
34
|
-
* Scope: banner line count vs. bash and broken phase-4 byte compatibility.
|
|
35
|
-
*
|
|
36
|
-
* Empty input → 0. Bare `+` or `-` (single char line) → 0, same as bash
|
|
37
|
-
* (the regex requires a second character).
|
|
38
|
-
*/
|
|
39
|
-
export function countChangedLines(diff) {
|
|
40
|
-
if (diff.length === 0)
|
|
41
|
-
return 0;
|
|
42
|
-
let n = 0;
|
|
43
|
-
const lines = diff.split('\n');
|
|
44
|
-
for (const line of lines) {
|
|
45
|
-
if (line.length < 2)
|
|
46
|
-
continue;
|
|
47
|
-
const c0 = line.charCodeAt(0);
|
|
48
|
-
const c1 = line.charCodeAt(1);
|
|
49
|
-
// `+` = 43: match only when char-2 is NOT `+`.
|
|
50
|
-
if (c0 === 43 && c1 !== 43) {
|
|
51
|
-
n++;
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
// `-` = 45: match only when char-2 is NOT `-`.
|
|
55
|
-
if (c0 === 45 && c1 !== 45) {
|
|
56
|
-
n++;
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
// Any other leading char, including `++...` and `--...`, is skipped.
|
|
60
|
-
}
|
|
61
|
-
return n;
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Count `^\+\+\+ ` header lines (one per file in the diff). Parity with
|
|
65
|
-
* the bash core's `grep -c '^\+\+\+ '`.
|
|
66
|
-
*/
|
|
67
|
-
export function countChangedFiles(diff) {
|
|
68
|
-
if (diff.length === 0)
|
|
69
|
-
return 0;
|
|
70
|
-
let n = 0;
|
|
71
|
-
const lines = diff.split('\n');
|
|
72
|
-
for (const line of lines) {
|
|
73
|
-
if (line.startsWith('+++ '))
|
|
74
|
-
n++;
|
|
75
|
-
}
|
|
76
|
-
return n;
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* Compute `{line_count, file_count}` over a diff string. Exposed separately
|
|
80
|
-
* so callers can use just the stats without generating the full banner.
|
|
81
|
-
*/
|
|
82
|
-
export function computeDiffStats(diff) {
|
|
83
|
-
return {
|
|
84
|
-
line_count: countChangedLines(diff),
|
|
85
|
-
file_count: countChangedFiles(diff),
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* Compose the "PUSH REVIEW GATE: Review required before pushing" banner.
|
|
90
|
-
* Output goes to stderr via the caller; this function is pure. Returns the
|
|
91
|
-
* exact text the bash core would have printed (including trailing blank
|
|
92
|
-
* line and spacing), so the fixture snapshot can be compared byte-exactly.
|
|
93
|
-
*/
|
|
94
|
-
export function renderPushReviewRequiredBanner(input) {
|
|
95
|
-
const lines = [];
|
|
96
|
-
lines.push('PUSH REVIEW GATE: Review required before pushing');
|
|
97
|
-
lines.push('');
|
|
98
|
-
lines.push(` Source ref: ${input.source_ref} (${input.source_sha.slice(0, 12)})`);
|
|
99
|
-
lines.push(` Target: ${input.target_branch}`);
|
|
100
|
-
lines.push(` Scope: ${input.stats.file_count} files changed, ${input.stats.line_count} lines`);
|
|
101
|
-
lines.push('');
|
|
102
|
-
lines.push(' Action required:');
|
|
103
|
-
lines.push(` 1. Spawn a code-reviewer agent to review: git diff ${input.merge_base}..${input.source_sha}`);
|
|
104
|
-
lines.push(' 2. Spawn a security-engineer agent for security review');
|
|
105
|
-
if (input.push_sha.length > 0) {
|
|
106
|
-
lines.push(' 3. After both pass, cache the result:');
|
|
107
|
-
lines.push(` rea cache set ${input.push_sha} pass --branch ${input.source_branch} --base ${input.target_branch}`);
|
|
108
|
-
}
|
|
109
|
-
else {
|
|
110
|
-
lines.push(' 3. Cache is DISABLED on this host (no sha256 hasher found).');
|
|
111
|
-
lines.push(' After both reviews pass, bypass the push-review gate with:');
|
|
112
|
-
lines.push(' REA_SKIP_PUSH_REVIEW="<reason>" git push ...');
|
|
113
|
-
lines.push(' The bypass is audited as push.review.skipped — this is the');
|
|
114
|
-
lines.push(' documented escape hatch when cache is unavailable.');
|
|
115
|
-
lines.push(' To restore the cache path, install one of: sha256sum,');
|
|
116
|
-
lines.push(' shasum (Perl Digest::SHA), or openssl.');
|
|
117
|
-
}
|
|
118
|
-
lines.push('');
|
|
119
|
-
// bash `printf '%s\n'` with no trailing args adds a final newline; the
|
|
120
|
-
// block renders terminal-ready. `lines.join('\n') + '\n'` reproduces
|
|
121
|
-
// exactly that shape.
|
|
122
|
-
return lines.join('\n') + '\n';
|
|
123
|
-
}
|
|
124
|
-
/**
|
|
125
|
-
* Compose the "PUSH BLOCKED: protected paths changed — /codex-review
|
|
126
|
-
* required" banner. Pure; exit-2 translation happens in the CLI shim.
|
|
127
|
-
*/
|
|
128
|
-
export function renderProtectedPathsBlockedBanner(input) {
|
|
129
|
-
const lines = [];
|
|
130
|
-
lines.push(`PUSH BLOCKED: protected paths changed — /codex-review required for ${input.source_sha}`);
|
|
131
|
-
lines.push('');
|
|
132
|
-
lines.push(` Source ref: ${input.source_ref}`);
|
|
133
|
-
lines.push(' Diff touches one of:');
|
|
134
|
-
lines.push(' - src/gateway/middleware/');
|
|
135
|
-
lines.push(' - hooks/');
|
|
136
|
-
lines.push(' - .claude/hooks/');
|
|
137
|
-
lines.push(' - src/policy/');
|
|
138
|
-
lines.push(' - .github/workflows/');
|
|
139
|
-
lines.push(' - .rea/');
|
|
140
|
-
lines.push(' - .husky/');
|
|
141
|
-
lines.push('');
|
|
142
|
-
lines.push(` Run /codex-review against ${input.source_sha}, then retry the push.`);
|
|
143
|
-
lines.push(' The codex-adversarial agent emits the required audit entry.');
|
|
144
|
-
lines.push(' Only `pass` or `concerns` verdicts satisfy this gate.');
|
|
145
|
-
lines.push('');
|
|
146
|
-
return lines.join('\n') + '\n';
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* Strip C0 control characters (0x00-0x1F, 0x7F) and C1 (0x80-0x9F) from a
|
|
150
|
-
* string. Used when a banner embeds text from a subprocess's stderr (e.g.
|
|
151
|
-
* the cache-check failure case). Mirrors the `LC_ALL=C tr -d` invocation
|
|
152
|
-
* in the bash core's cache-error path. Codex LOW 5 on the 0.9.4 pass.
|
|
153
|
-
*/
|
|
154
|
-
export function stripControlChars(input) {
|
|
155
|
-
let out = '';
|
|
156
|
-
for (let i = 0; i < input.length; i++) {
|
|
157
|
-
const c = input.charCodeAt(i);
|
|
158
|
-
// Allow tab (9), LF (10), CR (13) — but not any other C0/C1 byte.
|
|
159
|
-
if (c === 9 || c === 10 || c === 13) {
|
|
160
|
-
out += input[i];
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
|
-
if (c <= 0x1f)
|
|
164
|
-
continue;
|
|
165
|
-
if (c === 0x7f)
|
|
166
|
-
continue;
|
|
167
|
-
if (c >= 0x80 && c <= 0x9f)
|
|
168
|
-
continue;
|
|
169
|
-
out += input[i];
|
|
170
|
-
}
|
|
171
|
-
return out;
|
|
172
|
-
}
|
|
@@ -1,155 +0,0 @@
|
|
|
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 {};
|
|
@@ -1,247 +0,0 @@
|
|
|
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 { EMPTY_TREE_SHA, ZERO_SHA } from './constants.js';
|
|
42
|
-
import { hasCommitLocally, mergeBase, readGitConfig, refExists, resolveRemoteDefaultRef, } from './diff.js';
|
|
43
|
-
/**
|
|
44
|
-
* Strip `refs/heads/` / `refs/for/` prefixes from a ref and return the
|
|
45
|
-
* trailing branch name. Used for the target-label normalization path
|
|
46
|
-
* where both ref families should collapse to a bare branch name for
|
|
47
|
-
* display. Exported for tests.
|
|
48
|
-
*/
|
|
49
|
-
export function stripRefsPrefix(ref) {
|
|
50
|
-
if (ref.startsWith('refs/heads/'))
|
|
51
|
-
return ref.slice('refs/heads/'.length);
|
|
52
|
-
if (ref.startsWith('refs/for/'))
|
|
53
|
-
return ref.slice('refs/for/'.length);
|
|
54
|
-
return ref;
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Strip ONLY the `refs/heads/` prefix — leaves `refs/for/`, `refs/tags/`,
|
|
58
|
-
* and every other ref-namespace untouched. Mirrors the bash core's
|
|
59
|
-
* `${local_ref#refs/heads/}` on the source-branch lookup path (push-review-
|
|
60
|
-
* core.sh §797), so Gerrit-style pushes (`refs/for/main`) keep their
|
|
61
|
-
* namespace and do NOT accidentally match a `branch.main.base` config
|
|
62
|
-
* entry intended for a regular branch push.
|
|
63
|
-
*
|
|
64
|
-
* Codex pass-1 on Phase 2a flagged the earlier implementation that used
|
|
65
|
-
* `stripRefsPrefix` here — it would have promoted the Target: label for
|
|
66
|
-
* a Gerrit push against the reviewer's intent. Exported for tests.
|
|
67
|
-
*/
|
|
68
|
-
export function stripRefsHeadsOnly(ref) {
|
|
69
|
-
if (ref.startsWith('refs/heads/'))
|
|
70
|
-
return ref.slice('refs/heads/'.length);
|
|
71
|
-
return ref;
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* Resolve the base anchor for a single push refspec. See the file-top
|
|
75
|
-
* docstring for the four code paths.
|
|
76
|
-
*
|
|
77
|
-
* Deletion refspecs (local_sha === ZERO_SHA) return `{merge_base: null,
|
|
78
|
-
* status: 'ok'}` with `path: 'tracked'` — the caller is expected to have
|
|
79
|
-
* already trapped deletions via `hasDeletion()` before calling here. We
|
|
80
|
-
* don't throw in that case because the caller owns the deletion policy,
|
|
81
|
-
* not this resolver.
|
|
82
|
-
*/
|
|
83
|
-
export function resolveBaseForRefspec(record, deps) {
|
|
84
|
-
const { runner, cwd } = deps;
|
|
85
|
-
const targetLabel = computeInitialTargetLabel(record);
|
|
86
|
-
// Deletion — caller owns the policy.
|
|
87
|
-
if (record.is_deletion) {
|
|
88
|
-
return {
|
|
89
|
-
merge_base: null,
|
|
90
|
-
target_label: targetLabel,
|
|
91
|
-
status: 'ok',
|
|
92
|
-
path: 'tracked',
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
// Path A: tracked-branch push (remote_sha is not ZERO). Existing
|
|
96
|
-
// history is available; merge-base against the remote's tip.
|
|
97
|
-
if (record.remote_sha !== ZERO_SHA) {
|
|
98
|
-
if (!hasCommitLocally(runner, cwd, record.remote_sha)) {
|
|
99
|
-
return {
|
|
100
|
-
merge_base: null,
|
|
101
|
-
target_label: targetLabel,
|
|
102
|
-
status: 'remote_object_missing',
|
|
103
|
-
remote_sha: record.remote_sha,
|
|
104
|
-
path: 'tracked',
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
const mb = mergeBase(runner, cwd, record.remote_sha, record.local_sha);
|
|
108
|
-
if (mb === null) {
|
|
109
|
-
return {
|
|
110
|
-
merge_base: null,
|
|
111
|
-
target_label: targetLabel,
|
|
112
|
-
status: 'no_merge_base',
|
|
113
|
-
remote_sha: record.remote_sha,
|
|
114
|
-
path: 'tracked',
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
return {
|
|
118
|
-
merge_base: mb,
|
|
119
|
-
target_label: targetLabel,
|
|
120
|
-
status: 'ok',
|
|
121
|
-
path: 'tracked',
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
// Path B/C/D: new-branch push. Need a server-authoritative anchor.
|
|
125
|
-
//
|
|
126
|
-
// Bash-core parity (push-review-core.sh §797): the source-branch lookup
|
|
127
|
-
// uses `${local_ref#refs/heads/}` — strips ONLY the `refs/heads/`
|
|
128
|
-
// prefix. For a Gerrit-style `refs/for/main` push, bash leaves the ref
|
|
129
|
-
// as `refs/for/main`, the `branch.refs/for/main.base` config key never
|
|
130
|
-
// matches, and the new-branch walk proceeds to the origin/HEAD /
|
|
131
|
-
// bootstrap path. We mirror that exactly: use `stripRefsHeadsOnly`
|
|
132
|
-
// rather than the more aggressive `stripRefsPrefix` (which also strips
|
|
133
|
-
// `refs/for/`). The aggressive strip was a Phase 1 carry-over from
|
|
134
|
-
// `args.ts`'s destination-ref normalization; applied here it would
|
|
135
|
-
// cause a `refs/for/main` push to look up `branch.main.base` and
|
|
136
|
-
// potentially promote the Target: label against the reviewer's
|
|
137
|
-
// intent. Codex pass-1 on Phase 2a flagged this (P3) and we preserve
|
|
138
|
-
// byte-for-byte bash parity.
|
|
139
|
-
const sourceBranch = stripRefsHeadsOnly(record.local_ref);
|
|
140
|
-
const newBranchOutcome = resolveNewBranchBase(sourceBranch, deps);
|
|
141
|
-
if (newBranchOutcome.kind === 'config_hit') {
|
|
142
|
-
// Defect N: promote the target label to the configured base's short
|
|
143
|
-
// name. The bash core does this ONLY when the config hit fires — for
|
|
144
|
-
// all other new-branch paths the label stays as the refspec target
|
|
145
|
-
// (preserves cache-key / label continuity for pre-config consumers).
|
|
146
|
-
//
|
|
147
|
-
// `exactOptionalPropertyTypes: true` in tsconfig means we must NOT
|
|
148
|
-
// set `local_ref_fallback_warning: undefined` — we either include the
|
|
149
|
-
// key (as a string) or omit it entirely. Spread the conditional.
|
|
150
|
-
const result = {
|
|
151
|
-
merge_base: mergeBase(runner, cwd, newBranchOutcome.ref, record.local_sha) ?? EMPTY_TREE_SHA,
|
|
152
|
-
target_label: newBranchOutcome.label,
|
|
153
|
-
status: 'ok',
|
|
154
|
-
path: 'new_branch_config',
|
|
155
|
-
};
|
|
156
|
-
if (newBranchOutcome.warning !== null) {
|
|
157
|
-
result.local_ref_fallback_warning = newBranchOutcome.warning;
|
|
158
|
-
}
|
|
159
|
-
return result;
|
|
160
|
-
}
|
|
161
|
-
if (newBranchOutcome.kind === 'origin_head') {
|
|
162
|
-
return {
|
|
163
|
-
merge_base: mergeBase(runner, cwd, newBranchOutcome.ref, record.local_sha) ?? EMPTY_TREE_SHA,
|
|
164
|
-
target_label: targetLabel,
|
|
165
|
-
status: 'ok',
|
|
166
|
-
path: 'new_branch_origin_head',
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
// Bootstrap: no config, no remote HEAD — anchor on the empty-tree SHA
|
|
170
|
-
// so the full push content is reviewable. Matches bash §887.
|
|
171
|
-
return {
|
|
172
|
-
merge_base: EMPTY_TREE_SHA,
|
|
173
|
-
target_label: targetLabel,
|
|
174
|
-
status: 'ok',
|
|
175
|
-
path: 'bootstrap_empty_tree',
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
/**
|
|
179
|
-
* Compute the initial `Target:` label for a refspec: the short name of
|
|
180
|
-
* the remote ref, falling back to `main` when it's empty (defensive;
|
|
181
|
-
* `args.ts` should never emit an empty remote_ref for a non-deletion).
|
|
182
|
-
*
|
|
183
|
-
* Exported for unit tests. Mirrors push-review-core.sh §725-727.
|
|
184
|
-
*/
|
|
185
|
-
export function computeInitialTargetLabel(record) {
|
|
186
|
-
let target = stripRefsPrefix(record.remote_ref);
|
|
187
|
-
if (target.length === 0)
|
|
188
|
-
target = 'main';
|
|
189
|
-
return target;
|
|
190
|
-
}
|
|
191
|
-
/**
|
|
192
|
-
* Path B: consult `branch.<source>.base`. Returns `config_hit` iff a base
|
|
193
|
-
* was configured AND resolvable to a ref that exists. Falls through
|
|
194
|
-
* otherwise. Exported so tests can exercise the config-path independently.
|
|
195
|
-
*/
|
|
196
|
-
export function resolveNewBranchBase(sourceBranch, deps) {
|
|
197
|
-
const { runner, cwd, remote } = deps;
|
|
198
|
-
// B — branch.<source>.base config hit.
|
|
199
|
-
if (sourceBranch.length > 0 && sourceBranch !== 'HEAD') {
|
|
200
|
-
const configuredBase = readGitConfig(runner, cwd, `branch.${sourceBranch}.base`);
|
|
201
|
-
if (configuredBase.length > 0) {
|
|
202
|
-
const remoteRef = `refs/remotes/${remote}/${configuredBase}`;
|
|
203
|
-
const localRef = `refs/heads/${configuredBase}`;
|
|
204
|
-
if (refExists(runner, cwd, remoteRef)) {
|
|
205
|
-
return {
|
|
206
|
-
kind: 'config_hit',
|
|
207
|
-
ref: remoteRef,
|
|
208
|
-
label: configuredBase,
|
|
209
|
-
warning: null,
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
if (refExists(runner, cwd, localRef)) {
|
|
213
|
-
// Bash-core §819-820: local-ref fallback is less trustworthy; emit
|
|
214
|
-
// a WARN so the reviewer knows the anchor may be stale.
|
|
215
|
-
const warning = `WARN: branch.${sourceBranch}.base=${configuredBase} resolved to local ref; ` +
|
|
216
|
-
`remote counterpart ${remote}/${configuredBase} missing — reviewer-side diff may be stale`;
|
|
217
|
-
return {
|
|
218
|
-
kind: 'config_hit',
|
|
219
|
-
ref: localRef,
|
|
220
|
-
label: configuredBase,
|
|
221
|
-
warning,
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
// Config key set but neither ref exists: fall through to origin/HEAD
|
|
225
|
-
// (bash does the same — `configured_base` stays non-empty, but
|
|
226
|
-
// `default_ref` is still empty so the OR at §844 takes over).
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
// C — refs/remotes/<remote>/HEAD.
|
|
230
|
-
const symbolic = resolveRemoteDefaultRef(runner, cwd, remote);
|
|
231
|
-
if (symbolic !== null && symbolic.length > 0) {
|
|
232
|
-
return { kind: 'origin_head', ref: symbolic };
|
|
233
|
-
}
|
|
234
|
-
// C-bis — fall-back probes: `main`, then `master`. Bash §834-843 probes
|
|
235
|
-
// both because symbolic-ref fails on shallow or mirror clones where
|
|
236
|
-
// origin/HEAD was never set.
|
|
237
|
-
const mainRef = `refs/remotes/${remote}/main`;
|
|
238
|
-
if (refExists(runner, cwd, mainRef)) {
|
|
239
|
-
return { kind: 'origin_head', ref: mainRef };
|
|
240
|
-
}
|
|
241
|
-
const masterRef = `refs/remotes/${remote}/master`;
|
|
242
|
-
if (refExists(runner, cwd, masterRef)) {
|
|
243
|
-
return { kind: 'origin_head', ref: masterRef };
|
|
244
|
-
}
|
|
245
|
-
// D — bootstrap.
|
|
246
|
-
return { kind: 'bootstrap' };
|
|
247
|
-
}
|