@bookedsolid/rea 0.10.0 → 0.10.2
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/audit/append.d.ts +35 -1
- package/dist/audit/append.js +79 -11
- package/dist/cli/audit.js +130 -34
- package/dist/cli/doctor.js +1 -1
- package/dist/cli/index.js +18 -0
- package/dist/cli/tofu.d.ts +57 -0
- package/dist/cli/tofu.js +134 -0
- package/dist/gateway/audit/rotator.js +4 -0
- package/dist/gateway/middleware/audit-types.d.ts +35 -0
- package/dist/gateway/middleware/audit.js +6 -0
- package/dist/hooks/review-gate/args.d.ts +126 -0
- package/dist/hooks/review-gate/args.js +315 -0
- package/dist/hooks/review-gate/banner.d.ts +97 -0
- package/dist/hooks/review-gate/banner.js +172 -0
- package/dist/hooks/review-gate/cache-key.d.ts +55 -0
- package/dist/hooks/review-gate/cache-key.js +41 -0
- package/dist/hooks/review-gate/constants.d.ts +26 -0
- package/dist/hooks/review-gate/constants.js +34 -0
- package/dist/hooks/review-gate/errors.d.ts +72 -0
- package/dist/hooks/review-gate/errors.js +100 -0
- package/dist/hooks/review-gate/hash.d.ts +43 -0
- package/dist/hooks/review-gate/hash.js +46 -0
- package/dist/hooks/review-gate/index.d.ts +21 -0
- package/dist/hooks/review-gate/index.js +21 -0
- package/dist/hooks/review-gate/metadata.d.ts +98 -0
- package/dist/hooks/review-gate/metadata.js +158 -0
- package/dist/hooks/review-gate/policy.d.ts +55 -0
- package/dist/hooks/review-gate/policy.js +71 -0
- package/dist/hooks/review-gate/protected-paths.d.ts +46 -0
- package/dist/hooks/review-gate/protected-paths.js +76 -0
- package/dist/registry/tofu-gate.js +4 -1
- package/hooks/_lib/push-review-core.sh +121 -25
- package/package.json +1 -1
|
@@ -0,0 +1,172 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache-key computation. This module pins the contract between the push-
|
|
3
|
+
* review gate and the review-cache (`rea cache check` / `rea cache set`).
|
|
4
|
+
*
|
|
5
|
+
* ## The 0.10.1 revert constraint (design §8)
|
|
6
|
+
*
|
|
7
|
+
* An earlier attempt at defect N changed the cache-key input in the bash
|
|
8
|
+
* core and silently invalidated every consumer's existing cache. The
|
|
9
|
+
* failure was: the bash resolver started using the refspec target ref in
|
|
10
|
+
* the cache-key input rather than the merge-base-anchor SHA, so a
|
|
11
|
+
* legitimate cache entry from before the change produced a different key
|
|
12
|
+
* after the upgrade.
|
|
13
|
+
*
|
|
14
|
+
* The TS port makes the contract explicit:
|
|
15
|
+
*
|
|
16
|
+
* cache_key = sha256_hex( full_git_diff_output )
|
|
17
|
+
*
|
|
18
|
+
* where `full_git_diff_output` is the UTF-8 string returned by
|
|
19
|
+
* `git diff <merge_base>..<source_sha>` with NO added framing. The key
|
|
20
|
+
* is NOT a function of ref names, branch names, or target labels — those
|
|
21
|
+
* are stored in the cache entry as context but do not participate in key
|
|
22
|
+
* derivation.
|
|
23
|
+
*
|
|
24
|
+
* ## Fixture-backed compatibility test
|
|
25
|
+
*
|
|
26
|
+
* `__fixtures__/cache-keys.json` records six scenarios captured from the
|
|
27
|
+
* 0.10.1 bash core (bare push, multi-refspec, force-push, deletion,
|
|
28
|
+
* new-branch, cross-repo). `cache-key.test.ts` asserts byte-exact
|
|
29
|
+
* `computeCacheKey(input) === expected` across all scenarios. Any phase
|
|
30
|
+
* that changes this module without updating the fixture fails the suite.
|
|
31
|
+
*/
|
|
32
|
+
import { type HexSha256 } from './hash.js';
|
|
33
|
+
export interface CacheKeyInput {
|
|
34
|
+
/** Full `git diff <merge_base>..<source_sha>` output. */
|
|
35
|
+
diff: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Compute the cache key for a push-review entry. Stable, deterministic,
|
|
39
|
+
* pure. The key is the SHA-256 of the UTF-8 bytes of the diff string.
|
|
40
|
+
*
|
|
41
|
+
* @returns the 64-char lowercase hex digest.
|
|
42
|
+
*/
|
|
43
|
+
export declare function computeCacheKey(input: CacheKeyInput): HexSha256;
|
|
44
|
+
/**
|
|
45
|
+
* Input shape for a cache-lookup call. The key itself is the diff digest
|
|
46
|
+
* from `computeCacheKey`; branch + base are context fields that select
|
|
47
|
+
* which entry within the key-bucket to return. The bash core and the
|
|
48
|
+
* existing `src/cache/review-cache.ts` both key lookups on
|
|
49
|
+
* `(sha, branch, base)`, and the TS port keeps that contract.
|
|
50
|
+
*/
|
|
51
|
+
export interface CacheLookupContext {
|
|
52
|
+
key: HexSha256;
|
|
53
|
+
branch: string;
|
|
54
|
+
base: string;
|
|
55
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache-key computation. This module pins the contract between the push-
|
|
3
|
+
* review gate and the review-cache (`rea cache check` / `rea cache set`).
|
|
4
|
+
*
|
|
5
|
+
* ## The 0.10.1 revert constraint (design §8)
|
|
6
|
+
*
|
|
7
|
+
* An earlier attempt at defect N changed the cache-key input in the bash
|
|
8
|
+
* core and silently invalidated every consumer's existing cache. The
|
|
9
|
+
* failure was: the bash resolver started using the refspec target ref in
|
|
10
|
+
* the cache-key input rather than the merge-base-anchor SHA, so a
|
|
11
|
+
* legitimate cache entry from before the change produced a different key
|
|
12
|
+
* after the upgrade.
|
|
13
|
+
*
|
|
14
|
+
* The TS port makes the contract explicit:
|
|
15
|
+
*
|
|
16
|
+
* cache_key = sha256_hex( full_git_diff_output )
|
|
17
|
+
*
|
|
18
|
+
* where `full_git_diff_output` is the UTF-8 string returned by
|
|
19
|
+
* `git diff <merge_base>..<source_sha>` with NO added framing. The key
|
|
20
|
+
* is NOT a function of ref names, branch names, or target labels — those
|
|
21
|
+
* are stored in the cache entry as context but do not participate in key
|
|
22
|
+
* derivation.
|
|
23
|
+
*
|
|
24
|
+
* ## Fixture-backed compatibility test
|
|
25
|
+
*
|
|
26
|
+
* `__fixtures__/cache-keys.json` records six scenarios captured from the
|
|
27
|
+
* 0.10.1 bash core (bare push, multi-refspec, force-push, deletion,
|
|
28
|
+
* new-branch, cross-repo). `cache-key.test.ts` asserts byte-exact
|
|
29
|
+
* `computeCacheKey(input) === expected` across all scenarios. Any phase
|
|
30
|
+
* that changes this module without updating the fixture fails the suite.
|
|
31
|
+
*/
|
|
32
|
+
import { sha256Hex } from './hash.js';
|
|
33
|
+
/**
|
|
34
|
+
* Compute the cache key for a push-review entry. Stable, deterministic,
|
|
35
|
+
* pure. The key is the SHA-256 of the UTF-8 bytes of the diff string.
|
|
36
|
+
*
|
|
37
|
+
* @returns the 64-char lowercase hex digest.
|
|
38
|
+
*/
|
|
39
|
+
export function computeCacheKey(input) {
|
|
40
|
+
return sha256Hex(input.diff);
|
|
41
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constants shared across the review-gate modules. Kept in one file so the
|
|
3
|
+
* values that a reader cares about (the all-zeros SHA, the empty-tree SHA,
|
|
4
|
+
* the protected-path set) are trivially grep-able.
|
|
5
|
+
*/
|
|
6
|
+
/** The git-native "null" SHA — a deletion on the pre-push contract. */
|
|
7
|
+
export declare const ZERO_SHA = "0000000000000000000000000000000000000000";
|
|
8
|
+
/**
|
|
9
|
+
* The canonical empty-tree SHA-1. Used as the merge-base baseline when a
|
|
10
|
+
* new-branch push has no remote-tracking ref to anchor on — `git diff
|
|
11
|
+
* <empty-tree>..<local_sha>` gives the full push content, and the gate
|
|
12
|
+
* treats it as a diff against nothing (which is what it is, operationally).
|
|
13
|
+
*/
|
|
14
|
+
export declare const EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
15
|
+
/**
|
|
16
|
+
* The protected-path set. A push that touches any file under one of these
|
|
17
|
+
* prefixes requires a matching `codex.review` audit record with verdict in
|
|
18
|
+
* {pass, concerns} AND emission_source in {rea-cli, codex-cli}. See the
|
|
19
|
+
* THREAT_MODEL for the full threat statement, and the design doc §9 for
|
|
20
|
+
* the carry-forward from the bash core.
|
|
21
|
+
*
|
|
22
|
+
* Pattern format: leading/trailing slashes stripped; matched as directory
|
|
23
|
+
* prefixes by `protected-paths.ts`. Order is not significant (a hit on any
|
|
24
|
+
* entry is sufficient), but we keep the list sorted for grep-ability.
|
|
25
|
+
*/
|
|
26
|
+
export declare const PROTECTED_PATH_PREFIXES: readonly string[];
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constants shared across the review-gate modules. Kept in one file so the
|
|
3
|
+
* values that a reader cares about (the all-zeros SHA, the empty-tree SHA,
|
|
4
|
+
* the protected-path set) are trivially grep-able.
|
|
5
|
+
*/
|
|
6
|
+
/** The git-native "null" SHA — a deletion on the pre-push contract. */
|
|
7
|
+
export const ZERO_SHA = '0000000000000000000000000000000000000000';
|
|
8
|
+
/**
|
|
9
|
+
* The canonical empty-tree SHA-1. Used as the merge-base baseline when a
|
|
10
|
+
* new-branch push has no remote-tracking ref to anchor on — `git diff
|
|
11
|
+
* <empty-tree>..<local_sha>` gives the full push content, and the gate
|
|
12
|
+
* treats it as a diff against nothing (which is what it is, operationally).
|
|
13
|
+
*/
|
|
14
|
+
export const EMPTY_TREE_SHA = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
|
|
15
|
+
/**
|
|
16
|
+
* The protected-path set. A push that touches any file under one of these
|
|
17
|
+
* prefixes requires a matching `codex.review` audit record with verdict in
|
|
18
|
+
* {pass, concerns} AND emission_source in {rea-cli, codex-cli}. See the
|
|
19
|
+
* THREAT_MODEL for the full threat statement, and the design doc §9 for
|
|
20
|
+
* the carry-forward from the bash core.
|
|
21
|
+
*
|
|
22
|
+
* Pattern format: leading/trailing slashes stripped; matched as directory
|
|
23
|
+
* prefixes by `protected-paths.ts`. Order is not significant (a hit on any
|
|
24
|
+
* entry is sufficient), but we keep the list sorted for grep-ability.
|
|
25
|
+
*/
|
|
26
|
+
export const PROTECTED_PATH_PREFIXES = [
|
|
27
|
+
'.claude/hooks/',
|
|
28
|
+
'.github/workflows/',
|
|
29
|
+
'.husky/',
|
|
30
|
+
'.rea/',
|
|
31
|
+
'hooks/',
|
|
32
|
+
'src/gateway/middleware/',
|
|
33
|
+
'src/policy/',
|
|
34
|
+
];
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public entry point for the review-gate TypeScript port (G).
|
|
3
|
+
*
|
|
4
|
+
* Phase 1 scope: expose the pure primitives (args, hash, banner, metadata,
|
|
5
|
+
* policy, protected-paths, cache-key, errors, constants) so they can be
|
|
6
|
+
* unit-tested and composed by phase 2's `runPushReviewGate` /
|
|
7
|
+
* `runCommitReviewGate`. No behavioral surface is registered here yet —
|
|
8
|
+
* the bash core in `hooks/_lib/push-review-core.sh` continues to run in
|
|
9
|
+
* production until phase 4.
|
|
10
|
+
*
|
|
11
|
+
* See `docs/design/push-review-ts-port.md` for the full plan.
|
|
12
|
+
*/
|
|
13
|
+
export * from './args.js';
|
|
14
|
+
export * from './banner.js';
|
|
15
|
+
export * from './cache-key.js';
|
|
16
|
+
export * from './constants.js';
|
|
17
|
+
export * from './errors.js';
|
|
18
|
+
export * from './hash.js';
|
|
19
|
+
export * from './metadata.js';
|
|
20
|
+
export * from './policy.js';
|
|
21
|
+
export * from './protected-paths.js';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public entry point for the review-gate TypeScript port (G).
|
|
3
|
+
*
|
|
4
|
+
* Phase 1 scope: expose the pure primitives (args, hash, banner, metadata,
|
|
5
|
+
* policy, protected-paths, cache-key, errors, constants) so they can be
|
|
6
|
+
* unit-tested and composed by phase 2's `runPushReviewGate` /
|
|
7
|
+
* `runCommitReviewGate`. No behavioral surface is registered here yet —
|
|
8
|
+
* the bash core in `hooks/_lib/push-review-core.sh` continues to run in
|
|
9
|
+
* production until phase 4.
|
|
10
|
+
*
|
|
11
|
+
* See `docs/design/push-review-ts-port.md` for the full plan.
|
|
12
|
+
*/
|
|
13
|
+
export * from './args.js';
|
|
14
|
+
export * from './banner.js';
|
|
15
|
+
export * from './cache-key.js';
|
|
16
|
+
export * from './constants.js';
|
|
17
|
+
export * from './errors.js';
|
|
18
|
+
export * from './hash.js';
|
|
19
|
+
export * from './metadata.js';
|
|
20
|
+
export * from './policy.js';
|
|
21
|
+
export * from './protected-paths.js';
|