@bookedsolid/rea 0.10.1 → 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.
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Git-subprocess wrappers the review gate needs.
3
+ *
4
+ * ## Why a dedicated module
5
+ *
6
+ * The bash core spawns git via inline `$(cd "$REA_ROOT" && git ... 2>/dev/null)`
7
+ * subshells. Each invocation carries a handful of concerns the TS port
8
+ * separates cleanly:
9
+ *
10
+ * - Args passed as an ARRAY so the shell never interprets them
11
+ * (push-review-ts-port design §9 security posture: no argument-injection
12
+ * CVEs around refspec names like `main;rm -rf /`).
13
+ * - `cwd` always set to the resolved repo root — the caller never has to
14
+ * remember to `cd` first.
15
+ * - stdout/stderr captured separately. Git's error text goes to stderr in
16
+ * normal modes, and callers often want stderr for diagnostics while
17
+ * still deciding based on exit code.
18
+ * - A single shared timeout (10s) catches a hung `git` process. The bash
19
+ * core had no timeout — an upstream `git` stuck on NFS could wedge the
20
+ * whole push indefinitely.
21
+ *
22
+ * ## Mockability
23
+ *
24
+ * Every exported function takes a `GitRunner` as its first positional so
25
+ * tests can stub the subprocess layer. The default runner spawns `git`;
26
+ * tests supply a recording runner and assert over the command history. This
27
+ * is how `base-resolve.test.ts` and `diff.test.ts` avoid needing a real
28
+ * git repo.
29
+ *
30
+ * ## Defect carry-forwards
31
+ *
32
+ * - Two-dot `A..B` for diff inputs, NEVER three-dot `A...B`. The bash
33
+ * core's comment on push-review-core.sh §1053-1060 covers this: three-dot
34
+ * computes an implicit merge-base which FAILS when A is the empty-tree
35
+ * SHA (a valid bootstrap anchor in `base-resolve.ts`). Two-dot accepts
36
+ * any revision on the left.
37
+ * - `git diff --name-status` output is tab-separated, one line per change;
38
+ * `protected-paths.ts` owns the parse.
39
+ * - `git cat-file -e <sha>^{commit}` is the "object is locally resolvable"
40
+ * probe. Bash used a bare exit-code check; we preserve that.
41
+ */
42
+ /**
43
+ * Result of a git invocation. `status` is the git exit code (0 on success).
44
+ * Both `stdout` and `stderr` are trimmed of trailing newlines — the bash
45
+ * core's callers all applied `$(...)` which already collapses trailing
46
+ * whitespace, so TS callers expect the same contract.
47
+ */
48
+ export interface GitRunResult {
49
+ status: number;
50
+ stdout: string;
51
+ stderr: string;
52
+ }
53
+ /**
54
+ * Signature the git-subprocess layer must implement. Tests supply a stub
55
+ * that records calls and returns canned results; production uses
56
+ * {@link spawnGit}.
57
+ */
58
+ export type GitRunner = (args: readonly string[], cwd: string) => GitRunResult;
59
+ /**
60
+ * Default production git runner. Spawns `git` with the supplied args, cwd,
61
+ * and a fixed timeout. `encoding: 'utf8'` so the returned strings are
62
+ * decoded, matching the bash `$()` shape.
63
+ *
64
+ * Security: args is always an array; no shell string ever participates in
65
+ * the invocation. Refspec names that happen to contain shell metacharacters
66
+ * are inert.
67
+ */
68
+ export declare function spawnGit(args: readonly string[], cwd: string): GitRunResult;
69
+ /**
70
+ * Return the current branch name (empty string when detached or on failure).
71
+ * Bash-core parity (push-review-core.sh §687): `git branch --show-current`.
72
+ */
73
+ export declare function currentBranch(runner: GitRunner, cwd: string): string;
74
+ /**
75
+ * Resolve `HEAD` to a commit SHA, or return the empty string when the repo
76
+ * has no commits / the rev-parse fails. Bash-core parity
77
+ * (push-review-core.sh §134 and §412): `git rev-parse HEAD`.
78
+ */
79
+ export declare function resolveHead(runner: GitRunner, cwd: string): string;
80
+ /**
81
+ * Resolve a ref (e.g. `feature/foo`) to a commit SHA via
82
+ * `git rev-parse --verify <ref>^{commit}`, or return null on failure.
83
+ * Bash-core parity (push-review-core.sh §187): the `^{commit}` suffix
84
+ * forces resolution to the commit even for annotated-tag refs.
85
+ */
86
+ export declare function resolveRefToSha(runner: GitRunner, cwd: string, ref: string): string | null;
87
+ /**
88
+ * Return the short-name upstream for the current branch (e.g. `origin/main`)
89
+ * or null if no upstream is set. Bash-core parity (push-review-core.sh §129
90
+ * and §601): `git rev-parse --abbrev-ref --symbolic-full-name '@{upstream}'`.
91
+ */
92
+ export declare function resolveUpstream(runner: GitRunner, cwd: string): string | null;
93
+ /**
94
+ * Check whether a commit object is locally resolvable. Bash-core parity
95
+ * (push-review-core.sh §743): `git cat-file -e <sha>^{commit}`. Returns
96
+ * true on exit 0, false otherwise. Used before merge-base computation on
97
+ * the non-new-branch path — if the remote ref isn't fetched, `git merge-
98
+ * base` would silently return an unrelated base and the gate would diff
99
+ * against the wrong anchor.
100
+ */
101
+ export declare function hasCommitLocally(runner: GitRunner, cwd: string, sha: string): boolean;
102
+ /**
103
+ * Compute the merge-base between two refs. Returns the SHA on success,
104
+ * null on failure or empty output. Bash-core parity
105
+ * (push-review-core.sh §756 and §860): `git merge-base <a> <b>`.
106
+ */
107
+ export declare function mergeBase(runner: GitRunner, cwd: string, a: string, b: string): string | null;
108
+ /**
109
+ * True iff `ref` is a resolvable rev (commit, tag, or branch). Bash-core
110
+ * parity (push-review-core.sh §815 and §835): `git rev-parse --verify
111
+ * --quiet <ref>` with stdout suppressed and stderr ignored.
112
+ */
113
+ export declare function refExists(runner: GitRunner, cwd: string, ref: string): boolean;
114
+ /**
115
+ * Read a git config value (e.g. `branch.<name>.base`). Returns the value
116
+ * or the empty string when the config entry is absent or `git config` fails.
117
+ * Bash-core parity (push-review-core.sh §808): `git config --get <key>`.
118
+ */
119
+ export declare function readGitConfig(runner: GitRunner, cwd: string, key: string): string;
120
+ /**
121
+ * Resolve `refs/remotes/<remote>/HEAD` to its symbolic target (e.g.
122
+ * `refs/remotes/origin/main`). Returns null on failure. Bash-core parity
123
+ * (push-review-core.sh §826): `git symbolic-ref refs/remotes/<remote>/HEAD`.
124
+ */
125
+ export declare function resolveRemoteDefaultRef(runner: GitRunner, cwd: string, remote: string): string | null;
126
+ /**
127
+ * Full `git diff <a>..<b>` output (two-dot per the §1053-1060 rationale).
128
+ * Returns the diff body on success; throws via the typed error if git
129
+ * exits non-zero so the caller can translate to the banner + exit 2.
130
+ *
131
+ * Empty diff → empty string, status 0 is a legitimate no-op push and the
132
+ * caller routes to its "no reviewable diff" branch.
133
+ */
134
+ export interface DiffResult {
135
+ /** The full `git diff` output (may be empty). */
136
+ diff: string;
137
+ /** git's exit code. */
138
+ status: number;
139
+ /** git's stderr for error-path diagnostics. */
140
+ stderr: string;
141
+ }
142
+ export declare function fullDiff(runner: GitRunner, cwd: string, a: string, b: string): DiffResult;
143
+ /**
144
+ * `git diff --name-status <a>..<b>` output. One line per change, tab-
145
+ * separated `<STATUS>\t<path1>[\t<path2>]`. Consumed by
146
+ * `protected-paths.ts`. Returns stdout on success or null on error (the
147
+ * caller emits a banner + exit 2; see bash core §904-914).
148
+ */
149
+ export interface NameStatusResult {
150
+ /** Raw name-status output (may be empty on a zero-change diff). */
151
+ output: string;
152
+ /** git's exit code. */
153
+ status: number;
154
+ /** git's stderr for error-path diagnostics. */
155
+ stderr: string;
156
+ }
157
+ export declare function diffNameStatus(runner: GitRunner, cwd: string, a: string, b: string): NameStatusResult;
158
+ /**
159
+ * Commit count between two refs. Bash-core parity
160
+ * (push-review-core.sh §996): `git rev-list --count <a>..<b>`. Returns -1
161
+ * on error so callers can distinguish "git failed" (exit 2) from "zero
162
+ * commits" (legitimate, usually a same-ref push).
163
+ */
164
+ export declare function revListCount(runner: GitRunner, cwd: string, a: string, b: string): number;
165
+ /**
166
+ * Resolve the repository's common-dir (the path to `.git` or to a
167
+ * shared worktree parent). Returns the absolute path or null when not in
168
+ * a git repo. Bash-core parity (push-review-core.sh §272-273):
169
+ * `git rev-parse --path-format=absolute --git-common-dir`.
170
+ *
171
+ * Used by the cross-repo guard in §1a to distinguish two checkouts of the
172
+ * same repo (linked worktrees share a common-dir) from two unrelated
173
+ * repos. Phase 2a ships the primitive; composition happens in Phase 2b.
174
+ */
175
+ export declare function gitCommonDir(runner: GitRunner, cwd: string): string | null;
176
+ /**
177
+ * Read git's user email + name fallback for skip-audit actor attribution.
178
+ * Bash-core parity (push-review-core.sh §393-396 and §563-566): prefer
179
+ * email; fall back to name if email is empty; empty string if both missing.
180
+ */
181
+ export declare function readGitActor(runner: GitRunner, cwd: string): string;
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Git-subprocess wrappers the review gate needs.
3
+ *
4
+ * ## Why a dedicated module
5
+ *
6
+ * The bash core spawns git via inline `$(cd "$REA_ROOT" && git ... 2>/dev/null)`
7
+ * subshells. Each invocation carries a handful of concerns the TS port
8
+ * separates cleanly:
9
+ *
10
+ * - Args passed as an ARRAY so the shell never interprets them
11
+ * (push-review-ts-port design §9 security posture: no argument-injection
12
+ * CVEs around refspec names like `main;rm -rf /`).
13
+ * - `cwd` always set to the resolved repo root — the caller never has to
14
+ * remember to `cd` first.
15
+ * - stdout/stderr captured separately. Git's error text goes to stderr in
16
+ * normal modes, and callers often want stderr for diagnostics while
17
+ * still deciding based on exit code.
18
+ * - A single shared timeout (10s) catches a hung `git` process. The bash
19
+ * core had no timeout — an upstream `git` stuck on NFS could wedge the
20
+ * whole push indefinitely.
21
+ *
22
+ * ## Mockability
23
+ *
24
+ * Every exported function takes a `GitRunner` as its first positional so
25
+ * tests can stub the subprocess layer. The default runner spawns `git`;
26
+ * tests supply a recording runner and assert over the command history. This
27
+ * is how `base-resolve.test.ts` and `diff.test.ts` avoid needing a real
28
+ * git repo.
29
+ *
30
+ * ## Defect carry-forwards
31
+ *
32
+ * - Two-dot `A..B` for diff inputs, NEVER three-dot `A...B`. The bash
33
+ * core's comment on push-review-core.sh §1053-1060 covers this: three-dot
34
+ * computes an implicit merge-base which FAILS when A is the empty-tree
35
+ * SHA (a valid bootstrap anchor in `base-resolve.ts`). Two-dot accepts
36
+ * any revision on the left.
37
+ * - `git diff --name-status` output is tab-separated, one line per change;
38
+ * `protected-paths.ts` owns the parse.
39
+ * - `git cat-file -e <sha>^{commit}` is the "object is locally resolvable"
40
+ * probe. Bash used a bare exit-code check; we preserve that.
41
+ */
42
+ import { spawnSync } from 'node:child_process';
43
+ /** Hard cap on git invocation runtime. Bash core had no cap; 10s is generous for a hot-cache repo. */
44
+ const GIT_TIMEOUT_MS = 10_000;
45
+ /**
46
+ * Default production git runner. Spawns `git` with the supplied args, cwd,
47
+ * and a fixed timeout. `encoding: 'utf8'` so the returned strings are
48
+ * decoded, matching the bash `$()` shape.
49
+ *
50
+ * Security: args is always an array; no shell string ever participates in
51
+ * the invocation. Refspec names that happen to contain shell metacharacters
52
+ * are inert.
53
+ */
54
+ export function spawnGit(args, cwd) {
55
+ const opts = {
56
+ cwd,
57
+ encoding: 'utf8',
58
+ timeout: GIT_TIMEOUT_MS,
59
+ // Explicitly drop stdin — some git subcommands try to read (e.g. `git
60
+ // commit` prompting for a message); we never want that.
61
+ stdio: ['ignore', 'pipe', 'pipe'],
62
+ // Never use a shell. Args are an array; spawn does argv[0] execve
63
+ // directly, so `git` is looked up on PATH with no shell parsing.
64
+ shell: false,
65
+ };
66
+ const result = spawnSync('git', args, opts);
67
+ const stdout = typeof result.stdout === 'string' ? result.stdout.replace(/\n+$/, '') : '';
68
+ const stderr = typeof result.stderr === 'string' ? result.stderr.replace(/\n+$/, '') : '';
69
+ // On timeout/signal kill, spawnSync returns status=null and populates
70
+ // `signal`. Treat as a non-zero exit so callers fall through the normal
71
+ // error paths.
72
+ const status = typeof result.status === 'number' ? result.status : 1;
73
+ return { status, stdout, stderr };
74
+ }
75
+ /**
76
+ * SHA validator. Bash uses `=~ ^[0-9a-f]{40}$`; we match exactly.
77
+ */
78
+ const SHA_HEX_40 = /^[0-9a-f]{40}$/;
79
+ /**
80
+ * Return the current branch name (empty string when detached or on failure).
81
+ * Bash-core parity (push-review-core.sh §687): `git branch --show-current`.
82
+ */
83
+ export function currentBranch(runner, cwd) {
84
+ const r = runner(['branch', '--show-current'], cwd);
85
+ if (r.status !== 0)
86
+ return '';
87
+ return r.stdout;
88
+ }
89
+ /**
90
+ * Resolve `HEAD` to a commit SHA, or return the empty string when the repo
91
+ * has no commits / the rev-parse fails. Bash-core parity
92
+ * (push-review-core.sh §134 and §412): `git rev-parse HEAD`.
93
+ */
94
+ export function resolveHead(runner, cwd) {
95
+ const r = runner(['rev-parse', 'HEAD'], cwd);
96
+ if (r.status !== 0)
97
+ return '';
98
+ const sha = r.stdout;
99
+ return SHA_HEX_40.test(sha) ? sha : '';
100
+ }
101
+ /**
102
+ * Resolve a ref (e.g. `feature/foo`) to a commit SHA via
103
+ * `git rev-parse --verify <ref>^{commit}`, or return null on failure.
104
+ * Bash-core parity (push-review-core.sh §187): the `^{commit}` suffix
105
+ * forces resolution to the commit even for annotated-tag refs.
106
+ */
107
+ export function resolveRefToSha(runner, cwd, ref) {
108
+ const r = runner(['rev-parse', '--verify', `${ref}^{commit}`], cwd);
109
+ if (r.status !== 0)
110
+ return null;
111
+ const sha = r.stdout;
112
+ return SHA_HEX_40.test(sha) ? sha : null;
113
+ }
114
+ /**
115
+ * Return the short-name upstream for the current branch (e.g. `origin/main`)
116
+ * or null if no upstream is set. Bash-core parity (push-review-core.sh §129
117
+ * and §601): `git rev-parse --abbrev-ref --symbolic-full-name '@{upstream}'`.
118
+ */
119
+ export function resolveUpstream(runner, cwd) {
120
+ const r = runner(['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}'], cwd);
121
+ if (r.status !== 0)
122
+ return null;
123
+ const out = r.stdout;
124
+ return out.length > 0 ? out : null;
125
+ }
126
+ /**
127
+ * Check whether a commit object is locally resolvable. Bash-core parity
128
+ * (push-review-core.sh §743): `git cat-file -e <sha>^{commit}`. Returns
129
+ * true on exit 0, false otherwise. Used before merge-base computation on
130
+ * the non-new-branch path — if the remote ref isn't fetched, `git merge-
131
+ * base` would silently return an unrelated base and the gate would diff
132
+ * against the wrong anchor.
133
+ */
134
+ export function hasCommitLocally(runner, cwd, sha) {
135
+ if (!SHA_HEX_40.test(sha))
136
+ return false;
137
+ const r = runner(['cat-file', '-e', `${sha}^{commit}`], cwd);
138
+ return r.status === 0;
139
+ }
140
+ /**
141
+ * Compute the merge-base between two refs. Returns the SHA on success,
142
+ * null on failure or empty output. Bash-core parity
143
+ * (push-review-core.sh §756 and §860): `git merge-base <a> <b>`.
144
+ */
145
+ export function mergeBase(runner, cwd, a, b) {
146
+ const r = runner(['merge-base', a, b], cwd);
147
+ if (r.status !== 0)
148
+ return null;
149
+ const sha = r.stdout;
150
+ return SHA_HEX_40.test(sha) ? sha : null;
151
+ }
152
+ /**
153
+ * True iff `ref` is a resolvable rev (commit, tag, or branch). Bash-core
154
+ * parity (push-review-core.sh §815 and §835): `git rev-parse --verify
155
+ * --quiet <ref>` with stdout suppressed and stderr ignored.
156
+ */
157
+ export function refExists(runner, cwd, ref) {
158
+ const r = runner(['rev-parse', '--verify', '--quiet', ref], cwd);
159
+ return r.status === 0;
160
+ }
161
+ /**
162
+ * Read a git config value (e.g. `branch.<name>.base`). Returns the value
163
+ * or the empty string when the config entry is absent or `git config` fails.
164
+ * Bash-core parity (push-review-core.sh §808): `git config --get <key>`.
165
+ */
166
+ export function readGitConfig(runner, cwd, key) {
167
+ const r = runner(['config', '--get', key], cwd);
168
+ if (r.status !== 0)
169
+ return '';
170
+ return r.stdout;
171
+ }
172
+ /**
173
+ * Resolve `refs/remotes/<remote>/HEAD` to its symbolic target (e.g.
174
+ * `refs/remotes/origin/main`). Returns null on failure. Bash-core parity
175
+ * (push-review-core.sh §826): `git symbolic-ref refs/remotes/<remote>/HEAD`.
176
+ */
177
+ export function resolveRemoteDefaultRef(runner, cwd, remote) {
178
+ const r = runner(['symbolic-ref', `refs/remotes/${remote}/HEAD`], cwd);
179
+ if (r.status !== 0)
180
+ return null;
181
+ const out = r.stdout;
182
+ return out.length > 0 ? out : null;
183
+ }
184
+ export function fullDiff(runner, cwd, a, b) {
185
+ const r = runner(['diff', `${a}..${b}`], cwd);
186
+ return { diff: r.stdout, status: r.status, stderr: r.stderr };
187
+ }
188
+ export function diffNameStatus(runner, cwd, a, b) {
189
+ const r = runner(['diff', '--name-status', `${a}..${b}`], cwd);
190
+ return { output: r.stdout, status: r.status, stderr: r.stderr };
191
+ }
192
+ /**
193
+ * Commit count between two refs. Bash-core parity
194
+ * (push-review-core.sh §996): `git rev-list --count <a>..<b>`. Returns -1
195
+ * on error so callers can distinguish "git failed" (exit 2) from "zero
196
+ * commits" (legitimate, usually a same-ref push).
197
+ */
198
+ export function revListCount(runner, cwd, a, b) {
199
+ const r = runner(['rev-list', '--count', `${a}..${b}`], cwd);
200
+ if (r.status !== 0)
201
+ return -1;
202
+ const n = Number.parseInt(r.stdout, 10);
203
+ return Number.isFinite(n) && n >= 0 ? n : 0;
204
+ }
205
+ /**
206
+ * Resolve the repository's common-dir (the path to `.git` or to a
207
+ * shared worktree parent). Returns the absolute path or null when not in
208
+ * a git repo. Bash-core parity (push-review-core.sh §272-273):
209
+ * `git rev-parse --path-format=absolute --git-common-dir`.
210
+ *
211
+ * Used by the cross-repo guard in §1a to distinguish two checkouts of the
212
+ * same repo (linked worktrees share a common-dir) from two unrelated
213
+ * repos. Phase 2a ships the primitive; composition happens in Phase 2b.
214
+ */
215
+ export function gitCommonDir(runner, cwd) {
216
+ const r = runner(['rev-parse', '--path-format=absolute', '--git-common-dir'], cwd);
217
+ if (r.status !== 0)
218
+ return null;
219
+ const out = r.stdout;
220
+ return out.length > 0 ? out : null;
221
+ }
222
+ /**
223
+ * Read git's user email + name fallback for skip-audit actor attribution.
224
+ * Bash-core parity (push-review-core.sh §393-396 and §563-566): prefer
225
+ * email; fall back to name if email is empty; empty string if both missing.
226
+ */
227
+ export function readGitActor(runner, cwd) {
228
+ const email = readGitConfig(runner, cwd, 'user.email');
229
+ if (email.length > 0)
230
+ return email;
231
+ return readGitConfig(runner, cwd, 'user.name');
232
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Typed error set for the review-gate modules (G — push-review/commit-review
3
+ * TypeScript port).
4
+ *
5
+ * The bash core signals outcomes via exit codes + stderr banners. The TS port
6
+ * expresses each outcome as a typed error so callers can branch on class
7
+ * instead of parsing banner text, and so the eventual CLI entry point can
8
+ * translate a thrown error into the same exit code + banner the bash core
9
+ * emitted (preserving external contract per design §2, non-goals).
10
+ *
11
+ * Every error subclass carries:
12
+ * - a stable `code` (for programmatic dispatch in tests + the CLI shim)
13
+ * - an `exitCode` (matches the bash core's `exit N` semantics)
14
+ * - the operator-facing `message` composed by `banner.ts`
15
+ *
16
+ * This module is intentionally dependency-free so unit tests can import it
17
+ * without dragging in fs/child_process. The CLI entry point is the single
18
+ * place that translates these to `process.exit(N)`.
19
+ */
20
+ /**
21
+ * Stable discriminator used by tests and the CLI dispatch layer. String literals
22
+ * (not enum) so they survive JSON serialization in audit metadata.
23
+ */
24
+ export type ReviewGateErrorCode = 'PUSH_BLOCKED_DELETE' | 'PUSH_BLOCKED_HEAD_REFSPEC' | 'PUSH_BLOCKED_SOURCE_UNRESOLVABLE' | 'PUSH_BLOCKED_NO_REFSPECS' | 'PUSH_BLOCKED_REMOTE_OBJECT_MISSING' | 'PUSH_BLOCKED_NO_MERGE_BASE' | 'PUSH_BLOCKED_NO_BASE_RESOLVABLE' | 'PUSH_BLOCKED_DIFF_FAILED' | 'PUSH_BLOCKED_REV_LIST_FAILED' | 'PUSH_BLOCKED_PROTECTED_PATHS' | 'PUSH_BLOCKED_SKIP_REFUSED_IN_CI' | 'PUSH_BLOCKED_SKIP_NO_ACTOR' | 'PUSH_BLOCKED_SKIP_AUDIT_FAILED' | 'PUSH_BLOCKED_SKIP_NOT_BUILT' | 'PUSH_BLOCKED_SKIP_METADATA_FAILED' | 'PUSH_BLOCKED_CACHE_MKTEMP_UNAVAILABLE' | 'PUSH_BLOCKED_NOT_IN_REPO' | 'PUSH_BLOCKED_DEPENDENCY_MISSING' | 'PUSH_REVIEW_REQUIRED';
25
+ /**
26
+ * Base class. All review-gate errors derive from this so a CLI dispatch layer
27
+ * can `catch (e) { if (e instanceof ReviewGateError) ... }`.
28
+ */
29
+ export declare class ReviewGateError extends Error {
30
+ readonly code: ReviewGateErrorCode;
31
+ readonly exitCode: number;
32
+ readonly details: Record<string, unknown>;
33
+ constructor(code: ReviewGateErrorCode, message: string, exitCode: number, details?: Record<string, unknown>);
34
+ }
35
+ /**
36
+ * Blocked-push errors (exit 2). The bash core uses exit 2 for every blocked
37
+ * condition; we preserve that invariant so the shim's exit code is unchanged.
38
+ */
39
+ export declare class BlockedError extends ReviewGateError {
40
+ constructor(code: ReviewGateErrorCode, message: string, details?: Record<string, unknown>);
41
+ }
42
+ /**
43
+ * Deletion detected anywhere in the push (defect J). Must fail-closed even
44
+ * when a sibling refspec would have been reviewable.
45
+ */
46
+ export declare class DeletionBlockedError extends BlockedError {
47
+ constructor();
48
+ }
49
+ /**
50
+ * A refspec with `HEAD` as destination is operator error (design §3.1,
51
+ * `pr_resolve_argv_refspecs`). Carry the refspec in details for banner render.
52
+ */
53
+ export declare class HeadRefspecBlockedError extends BlockedError {
54
+ constructor(spec: string);
55
+ }
56
+ /**
57
+ * Invalid `--delete` refspec (empty destination or HEAD destination).
58
+ * Distinct from the general HEAD-refspec error because bash emits a
59
+ * different operator banner for the delete-mode case — the remediation
60
+ * text is "name the branch you meant to delete" rather than the HEAD
61
+ * destination error. See push-review-core.sh §161-168.
62
+ */
63
+ export declare class InvalidDeleteRefspecError extends BlockedError {
64
+ constructor(spec: string);
65
+ }
66
+ /**
67
+ * Defect N completion (landed in phase 4, type reserved in phase 1 so
68
+ * `base-resolve.ts` can throw it later without schema churn).
69
+ */
70
+ export declare class NoBaseResolvableError extends BlockedError {
71
+ constructor(source: string);
72
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Typed error set for the review-gate modules (G — push-review/commit-review
3
+ * TypeScript port).
4
+ *
5
+ * The bash core signals outcomes via exit codes + stderr banners. The TS port
6
+ * expresses each outcome as a typed error so callers can branch on class
7
+ * instead of parsing banner text, and so the eventual CLI entry point can
8
+ * translate a thrown error into the same exit code + banner the bash core
9
+ * emitted (preserving external contract per design §2, non-goals).
10
+ *
11
+ * Every error subclass carries:
12
+ * - a stable `code` (for programmatic dispatch in tests + the CLI shim)
13
+ * - an `exitCode` (matches the bash core's `exit N` semantics)
14
+ * - the operator-facing `message` composed by `banner.ts`
15
+ *
16
+ * This module is intentionally dependency-free so unit tests can import it
17
+ * without dragging in fs/child_process. The CLI entry point is the single
18
+ * place that translates these to `process.exit(N)`.
19
+ */
20
+ /**
21
+ * Base class. All review-gate errors derive from this so a CLI dispatch layer
22
+ * can `catch (e) { if (e instanceof ReviewGateError) ... }`.
23
+ */
24
+ export class ReviewGateError extends Error {
25
+ code;
26
+ exitCode;
27
+ details;
28
+ constructor(code, message, exitCode, details = {}) {
29
+ super(message);
30
+ this.name = 'ReviewGateError';
31
+ this.code = code;
32
+ this.exitCode = exitCode;
33
+ this.details = details;
34
+ // Node gives us a stable prototype chain via Error; no need to hack it.
35
+ }
36
+ }
37
+ /**
38
+ * Blocked-push errors (exit 2). The bash core uses exit 2 for every blocked
39
+ * condition; we preserve that invariant so the shim's exit code is unchanged.
40
+ */
41
+ export class BlockedError extends ReviewGateError {
42
+ constructor(code, message, details = {}) {
43
+ super(code, message, 2, details);
44
+ this.name = 'BlockedError';
45
+ }
46
+ }
47
+ /**
48
+ * Deletion detected anywhere in the push (defect J). Must fail-closed even
49
+ * when a sibling refspec would have been reviewable.
50
+ */
51
+ export class DeletionBlockedError extends BlockedError {
52
+ constructor() {
53
+ super('PUSH_BLOCKED_DELETE', 'refspec is a branch deletion.\n' +
54
+ '\n' +
55
+ ' Branch deletions are sensitive operations and require explicit\n' +
56
+ ' human action outside the agent. Perform the deletion manually.\n');
57
+ this.name = 'DeletionBlockedError';
58
+ }
59
+ }
60
+ /**
61
+ * A refspec with `HEAD` as destination is operator error (design §3.1,
62
+ * `pr_resolve_argv_refspecs`). Carry the refspec in details for banner render.
63
+ */
64
+ export class HeadRefspecBlockedError extends BlockedError {
65
+ constructor(spec) {
66
+ super('PUSH_BLOCKED_HEAD_REFSPEC', `refspec resolves to HEAD (from ${JSON.stringify(spec)})\n` +
67
+ '\n' +
68
+ ' `git push <remote> HEAD:<branch>` or similar is almost always\n' +
69
+ ' operator error in this context. Name the destination branch\n' +
70
+ ' explicitly so the review gate can diff against it.\n', { spec });
71
+ this.name = 'HeadRefspecBlockedError';
72
+ }
73
+ }
74
+ /**
75
+ * Invalid `--delete` refspec (empty destination or HEAD destination).
76
+ * Distinct from the general HEAD-refspec error because bash emits a
77
+ * different operator banner for the delete-mode case — the remediation
78
+ * text is "name the branch you meant to delete" rather than the HEAD
79
+ * destination error. See push-review-core.sh §161-168.
80
+ */
81
+ export class InvalidDeleteRefspecError extends BlockedError {
82
+ constructor(spec) {
83
+ super('PUSH_BLOCKED_HEAD_REFSPEC', `--delete refspec resolves to HEAD or empty (from ${JSON.stringify(spec)})\n`, { spec, mode: 'delete' });
84
+ this.name = 'InvalidDeleteRefspecError';
85
+ }
86
+ }
87
+ /**
88
+ * Defect N completion (landed in phase 4, type reserved in phase 1 so
89
+ * `base-resolve.ts` can throw it later without schema churn).
90
+ */
91
+ export class NoBaseResolvableError extends BlockedError {
92
+ constructor(source) {
93
+ super('PUSH_BLOCKED_NO_BASE_RESOLVABLE', `cannot resolve base branch for ${source}; run ` +
94
+ '`git branch --set-upstream-to=origin/<target>` or ' +
95
+ '`git config branch.' +
96
+ source +
97
+ '.base <ref>`', { source });
98
+ this.name = 'NoBaseResolvableError';
99
+ }
100
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Portable SHA-256 over arbitrary strings. Replaces the bash core's
3
+ * sha256sum → shasum → openssl fallback chain (defect L) with a single
4
+ * Node-stdlib call, removing the Alpine/distroless "no hasher on PATH"
5
+ * regression class entirely.
6
+ *
7
+ * ## Why a dedicated module
8
+ *
9
+ * The cache-key contract (design §8) requires byte-exact parity with the
10
+ * 0.10.1 bash implementation. The bash implementation computes
11
+ * `sha256sum( <full git diff> )` and uses the hex digest as the cache key.
12
+ * `crypto.createHash('sha256').update(s).digest('hex')` is bit-identical to
13
+ * GNU `sha256sum < <(printf '%s' s)` output (neither includes the
14
+ * filename-suffix padding). Regression-tested against the fixture in
15
+ * `__fixtures__/cache-keys.json`.
16
+ *
17
+ * ## Hex-64 validation
18
+ *
19
+ * The bash core validates the hasher output is `^[0-9a-f]{64}$` before using
20
+ * it as a cache key; a partial read or broken pipe would otherwise cache
21
+ * garbage. Node's `createHash` is synchronous and crypto-backed, so the
22
+ * digest is always a valid hex-64. We preserve the validation helper for
23
+ * any future path where user-supplied strings might be treated as SHAs (the
24
+ * bash core does this for push_sha env-pass, which the TS port rejects at
25
+ * the type level instead).
26
+ */
27
+ /** A hex-lowercased SHA-256 digest (64 chars). */
28
+ export type HexSha256 = string;
29
+ /**
30
+ * Compute a SHA-256 over the UTF-8 bytes of `input`. Returns the lowercase
31
+ * hex digest.
32
+ *
33
+ * This is the one function the cache-key contract depends on — never change
34
+ * the encoding or the digest format without bumping the cache-key version
35
+ * (which none of phases 1–4 are permitted to do, per design §8).
36
+ */
37
+ export declare function sha256Hex(input: string): HexSha256;
38
+ /**
39
+ * True iff the input looks like a canonical lowercase SHA-256 hex digest.
40
+ * Used by tests and by defensive callers that accept a string and want to
41
+ * reject malformed input before writing it into the cache.
42
+ */
43
+ export declare function isValidSha256Hex(value: string): value is HexSha256;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Portable SHA-256 over arbitrary strings. Replaces the bash core's
3
+ * sha256sum → shasum → openssl fallback chain (defect L) with a single
4
+ * Node-stdlib call, removing the Alpine/distroless "no hasher on PATH"
5
+ * regression class entirely.
6
+ *
7
+ * ## Why a dedicated module
8
+ *
9
+ * The cache-key contract (design §8) requires byte-exact parity with the
10
+ * 0.10.1 bash implementation. The bash implementation computes
11
+ * `sha256sum( <full git diff> )` and uses the hex digest as the cache key.
12
+ * `crypto.createHash('sha256').update(s).digest('hex')` is bit-identical to
13
+ * GNU `sha256sum < <(printf '%s' s)` output (neither includes the
14
+ * filename-suffix padding). Regression-tested against the fixture in
15
+ * `__fixtures__/cache-keys.json`.
16
+ *
17
+ * ## Hex-64 validation
18
+ *
19
+ * The bash core validates the hasher output is `^[0-9a-f]{64}$` before using
20
+ * it as a cache key; a partial read or broken pipe would otherwise cache
21
+ * garbage. Node's `createHash` is synchronous and crypto-backed, so the
22
+ * digest is always a valid hex-64. We preserve the validation helper for
23
+ * any future path where user-supplied strings might be treated as SHAs (the
24
+ * bash core does this for push_sha env-pass, which the TS port rejects at
25
+ * the type level instead).
26
+ */
27
+ import { createHash } from 'node:crypto';
28
+ /**
29
+ * Compute a SHA-256 over the UTF-8 bytes of `input`. Returns the lowercase
30
+ * hex digest.
31
+ *
32
+ * This is the one function the cache-key contract depends on — never change
33
+ * the encoding or the digest format without bumping the cache-key version
34
+ * (which none of phases 1–4 are permitted to do, per design §8).
35
+ */
36
+ export function sha256Hex(input) {
37
+ return createHash('sha256').update(input, 'utf8').digest('hex');
38
+ }
39
+ /**
40
+ * True iff the input looks like a canonical lowercase SHA-256 hex digest.
41
+ * Used by tests and by defensive callers that accept a string and want to
42
+ * reject malformed input before writing it into the cache.
43
+ */
44
+ export function isValidSha256Hex(value) {
45
+ return /^[0-9a-f]{64}$/.test(value);
46
+ }