@bookedsolid/rea 0.10.2 → 0.10.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/hooks/review-gate/audit.d.ts +131 -0
- package/dist/hooks/review-gate/audit.js +181 -0
- package/dist/hooks/review-gate/base-resolve.d.ts +155 -0
- package/dist/hooks/review-gate/base-resolve.js +247 -0
- package/dist/hooks/review-gate/cache.d.ts +108 -0
- package/dist/hooks/review-gate/cache.js +120 -0
- package/dist/hooks/review-gate/diff.d.ts +181 -0
- package/dist/hooks/review-gate/diff.js +232 -0
- package/dist/hooks/review-gate/index.d.ts +16 -6
- package/dist/hooks/review-gate/index.js +20 -6
- package/package.json +1 -1
|
@@ -0,0 +1,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
|
+
}
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Public entry point for the review-gate TypeScript port (G).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* ## Scope after Phase 2a (0.10.3)
|
|
5
|
+
*
|
|
6
|
+
* - Phase 1 primitives (args, banner, cache-key, constants, errors,
|
|
7
|
+
* hash, metadata, policy, protected-paths) — pure, dependency-free.
|
|
8
|
+
* - Phase 2a supporting modules (base-resolve, diff, audit, cache) —
|
|
9
|
+
* wrap git subprocesses, wrap audit append/scan, wrap the review-
|
|
10
|
+
* cache lookup. No top-level gate yet; composition lands in Phase 2b.
|
|
11
|
+
*
|
|
12
|
+
* The bash core in `hooks/_lib/push-review-core.sh` continues to run in
|
|
13
|
+
* production until phase 4. These exports are library-level primitives
|
|
14
|
+
* that tests and Phase 2b compose; no behavioral surface is registered
|
|
15
|
+
* for external callers here.
|
|
10
16
|
*
|
|
11
17
|
* See `docs/design/push-review-ts-port.md` for the full plan.
|
|
12
18
|
*/
|
|
@@ -19,3 +25,7 @@ export * from './hash.js';
|
|
|
19
25
|
export * from './metadata.js';
|
|
20
26
|
export * from './policy.js';
|
|
21
27
|
export * from './protected-paths.js';
|
|
28
|
+
export { currentBranch, diffNameStatus, fullDiff, gitCommonDir, hasCommitLocally, mergeBase, readGitActor, readGitConfig, refExists, resolveHead, resolveRefToSha, resolveRemoteDefaultRef, resolveUpstream, revListCount, spawnGit, type DiffResult, type GitRunResult, type GitRunner, type NameStatusResult, } from './diff.js';
|
|
29
|
+
export { computeInitialTargetLabel, resolveBaseForRefspec, resolveNewBranchBase, stripRefsHeadsOnly, type ResolveBaseDeps, type ResolvedBase, } from './base-resolve.js';
|
|
30
|
+
export { CODEX_REVIEW_SKIPPED_TOOL, ESCAPE_HATCH_SERVER, PUSH_REVIEW_CACHE_ERROR_TOOL, PUSH_REVIEW_CACHE_HIT_TOOL, PUSH_REVIEW_SERVER, PUSH_REVIEW_SKIPPED_TOOL, emitCodexReviewSkipped, emitPushReviewSkipped, hasValidCodexReview, isQualifyingCodexReview, type SkipCodexReviewAuditInput, type SkipPushReviewAuditInput, } from './audit.js';
|
|
31
|
+
export { checkReviewCache, type CacheOutcome, type CheckReviewCacheInput, } from './cache.js';
|
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Public entry point for the review-gate TypeScript port (G).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* ## Scope after Phase 2a (0.10.3)
|
|
5
|
+
*
|
|
6
|
+
* - Phase 1 primitives (args, banner, cache-key, constants, errors,
|
|
7
|
+
* hash, metadata, policy, protected-paths) — pure, dependency-free.
|
|
8
|
+
* - Phase 2a supporting modules (base-resolve, diff, audit, cache) —
|
|
9
|
+
* wrap git subprocesses, wrap audit append/scan, wrap the review-
|
|
10
|
+
* cache lookup. No top-level gate yet; composition lands in Phase 2b.
|
|
11
|
+
*
|
|
12
|
+
* The bash core in `hooks/_lib/push-review-core.sh` continues to run in
|
|
13
|
+
* production until phase 4. These exports are library-level primitives
|
|
14
|
+
* that tests and Phase 2b compose; no behavioral surface is registered
|
|
15
|
+
* for external callers here.
|
|
10
16
|
*
|
|
11
17
|
* See `docs/design/push-review-ts-port.md` for the full plan.
|
|
12
18
|
*/
|
|
19
|
+
// Phase 1 primitives
|
|
13
20
|
export * from './args.js';
|
|
14
21
|
export * from './banner.js';
|
|
15
22
|
export * from './cache-key.js';
|
|
@@ -19,3 +26,10 @@ export * from './hash.js';
|
|
|
19
26
|
export * from './metadata.js';
|
|
20
27
|
export * from './policy.js';
|
|
21
28
|
export * from './protected-paths.js';
|
|
29
|
+
// Phase 2a supporting modules — re-export explicit names to avoid
|
|
30
|
+
// double-exporting `computeCacheKey` (which lives in both cache-key.ts
|
|
31
|
+
// and cache.ts; cache.ts's is a strict re-export of Phase 1's).
|
|
32
|
+
export { currentBranch, diffNameStatus, fullDiff, gitCommonDir, hasCommitLocally, mergeBase, readGitActor, readGitConfig, refExists, resolveHead, resolveRefToSha, resolveRemoteDefaultRef, resolveUpstream, revListCount, spawnGit, } from './diff.js';
|
|
33
|
+
export { computeInitialTargetLabel, resolveBaseForRefspec, resolveNewBranchBase, stripRefsHeadsOnly, } from './base-resolve.js';
|
|
34
|
+
export { CODEX_REVIEW_SKIPPED_TOOL, ESCAPE_HATCH_SERVER, PUSH_REVIEW_CACHE_ERROR_TOOL, PUSH_REVIEW_CACHE_HIT_TOOL, PUSH_REVIEW_SERVER, PUSH_REVIEW_SKIPPED_TOOL, emitCodexReviewSkipped, emitPushReviewSkipped, hasValidCodexReview, isQualifyingCodexReview, } from './audit.js';
|
|
35
|
+
export { checkReviewCache, } from './cache.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bookedsolid/rea",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.3",
|
|
4
4
|
"description": "Agentic governance layer for Claude Code — policy enforcement, hook-based safety gates, audit logging, and Codex-integrated adversarial review for AI-assisted projects",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Booked Solid Technology <oss@bookedsolid.tech> (https://bookedsolid.tech)",
|