@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.
- package/dist/hooks/review-gate/args.d.ts +126 -0
- package/dist/hooks/review-gate/args.js +315 -0
- package/dist/hooks/review-gate/audit.d.ts +131 -0
- package/dist/hooks/review-gate/audit.js +181 -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/base-resolve.d.ts +155 -0
- package/dist/hooks/review-gate/base-resolve.js +247 -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/cache.d.ts +108 -0
- package/dist/hooks/review-gate/cache.js +120 -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/diff.d.ts +181 -0
- package/dist/hooks/review-gate/diff.js +232 -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 +31 -0
- package/dist/hooks/review-gate/index.js +35 -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/package.json +1 -1
|
@@ -0,0 +1,247 @@
|
|
|
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
|
+
}
|
|
@@ -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,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Review-cache adapter for the push-review gate.
|
|
3
|
+
*
|
|
4
|
+
* ## What this module owns
|
|
5
|
+
*
|
|
6
|
+
* 1. Compute the cache key from a diff. **Re-exports** `computeCacheKey`
|
|
7
|
+
* from the Phase-1 `cache-key.ts` module — this is the same function,
|
|
8
|
+
* not a reimplementation. The fixture suite (`cache.test.ts`) locks
|
|
9
|
+
* that invariant via a byte-exact re-run against
|
|
10
|
+
* `__fixtures__/cache-keys.json`. If the two modules ever diverge,
|
|
11
|
+
* the 0.10.x → 0.11.0 cache contract is broken and every consumer's
|
|
12
|
+
* on-disk cache becomes unreachable. See design §8.
|
|
13
|
+
*
|
|
14
|
+
* 2. Wrap `src/cache/review-cache.ts` so callers get a single entry
|
|
15
|
+
* point — `checkReviewCache({ diff, branch, base })` — that hides
|
|
16
|
+
* the TTL semantics, the `(sha, branch, base)` key tuple, and the
|
|
17
|
+
* cache-lookup result shape.
|
|
18
|
+
*
|
|
19
|
+
* 3. Translate the cache lookup into the same three-way outcome the
|
|
20
|
+
* bash core's §1203 `jq -e '.hit == true and .result == "pass"'`
|
|
21
|
+
* check emitted:
|
|
22
|
+
*
|
|
23
|
+
* - `hit_pass` — cache hit AND result === 'pass'. Gate passes.
|
|
24
|
+
* - `hit_fail` — cache hit AND result === 'fail'. Cached
|
|
25
|
+
* negative verdict; gate blocks. (Bash §1197-1202
|
|
26
|
+
* rejects this; we preserve.)
|
|
27
|
+
* - `miss` — no hit / expired / empty file. Gate falls
|
|
28
|
+
* through to the review-required banner.
|
|
29
|
+
* - `query_error` — cache lookup threw. Bash §1180-1196 treated
|
|
30
|
+
* this as a `{"hit":false,"reason":"query_error"}`
|
|
31
|
+
* cached result; we carry the error body so the
|
|
32
|
+
* caller can emit the CACHE CHECK FAILED banner
|
|
33
|
+
* with the SANITIZED stderr (defect C0/C1 strip).
|
|
34
|
+
*
|
|
35
|
+
* ## Phase 2a scope
|
|
36
|
+
*
|
|
37
|
+
* This file exports pure functions over the already-TS
|
|
38
|
+
* `review-cache.ts`. No subprocess `spawn`, no CLI fork. The bash
|
|
39
|
+
* core's `rea cache check` subprocess hop is obviated entirely —
|
|
40
|
+
* once Phase 3 swaps the shim we no longer fork/exec node for the
|
|
41
|
+
* cache lookup at all.
|
|
42
|
+
*
|
|
43
|
+
* ## Phase 2b composition
|
|
44
|
+
*
|
|
45
|
+
* `runPushReviewGate` in `index.ts` calls `computeCacheKeyFromDiff` +
|
|
46
|
+
* `checkReviewCache` sequentially; the latter returns a discriminated
|
|
47
|
+
* outcome the gate branches on. No new behavior lands here — that's
|
|
48
|
+
* the `codex-gate.ts` / composition step.
|
|
49
|
+
*/
|
|
50
|
+
import type { HexSha256 } from './hash.js';
|
|
51
|
+
/**
|
|
52
|
+
* Compute the cache key for a diff. This is a thin re-export of Phase 1's
|
|
53
|
+
* `computeCacheKey` — exposed on this module so callers can use a single
|
|
54
|
+
* import when they need both the key AND the lookup.
|
|
55
|
+
*
|
|
56
|
+
* The function is UNCHANGED from Phase 1. The fixture suite in
|
|
57
|
+
* `cache.test.ts` proves byte-exact parity against
|
|
58
|
+
* `__fixtures__/cache-keys.json` for all six scenarios.
|
|
59
|
+
*/
|
|
60
|
+
export declare function computeCacheKey(diff: string): HexSha256;
|
|
61
|
+
/**
|
|
62
|
+
* Discriminated outcome of a cache lookup. Matches the three-way state
|
|
63
|
+
* the bash core's §1203 predicate surfaces, plus a `query_error` kind
|
|
64
|
+
* for the §1180-1196 case.
|
|
65
|
+
*/
|
|
66
|
+
export type CacheOutcome = {
|
|
67
|
+
kind: 'hit_pass';
|
|
68
|
+
key: HexSha256;
|
|
69
|
+
recorded_at: string;
|
|
70
|
+
} | {
|
|
71
|
+
kind: 'hit_fail';
|
|
72
|
+
key: HexSha256;
|
|
73
|
+
recorded_at: string;
|
|
74
|
+
reason?: string;
|
|
75
|
+
} | {
|
|
76
|
+
kind: 'miss';
|
|
77
|
+
key: HexSha256;
|
|
78
|
+
reason: 'no-entry' | 'expired' | 'empty-file';
|
|
79
|
+
} | {
|
|
80
|
+
kind: 'query_error';
|
|
81
|
+
key: HexSha256;
|
|
82
|
+
error: string;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Input for `checkReviewCache`. `diff` is the full `git diff` body; the
|
|
86
|
+
* cache key is derived from it via `computeCacheKey`. `branch` + `base`
|
|
87
|
+
* select which entry in the key-bucket is returned (most-recent match).
|
|
88
|
+
*/
|
|
89
|
+
export interface CheckReviewCacheInput {
|
|
90
|
+
baseDir: string;
|
|
91
|
+
diff: string;
|
|
92
|
+
branch: string;
|
|
93
|
+
base: string;
|
|
94
|
+
/** Optional override for `Date.now()`-driven TTL expiration — test hook only. */
|
|
95
|
+
nowMs?: number;
|
|
96
|
+
/** Optional override for the TTL (seconds). Defaults to cache-module default. */
|
|
97
|
+
maxAgeSeconds?: number;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Perform a cache lookup and translate into the discriminated outcome.
|
|
101
|
+
*
|
|
102
|
+
* Does NOT throw on a lookup failure — every known error path routes
|
|
103
|
+
* through the `query_error` variant so the caller's single `switch`
|
|
104
|
+
* handles all four outcomes. This mirrors the bash core's
|
|
105
|
+
* `CACHE_STDOUT || CACHE_EXIT != 0 → {"hit":false,...}` collapse at
|
|
106
|
+
* §1180-1196.
|
|
107
|
+
*/
|
|
108
|
+
export declare function checkReviewCache(input: CheckReviewCacheInput): Promise<CacheOutcome>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Review-cache adapter for the push-review gate.
|
|
3
|
+
*
|
|
4
|
+
* ## What this module owns
|
|
5
|
+
*
|
|
6
|
+
* 1. Compute the cache key from a diff. **Re-exports** `computeCacheKey`
|
|
7
|
+
* from the Phase-1 `cache-key.ts` module — this is the same function,
|
|
8
|
+
* not a reimplementation. The fixture suite (`cache.test.ts`) locks
|
|
9
|
+
* that invariant via a byte-exact re-run against
|
|
10
|
+
* `__fixtures__/cache-keys.json`. If the two modules ever diverge,
|
|
11
|
+
* the 0.10.x → 0.11.0 cache contract is broken and every consumer's
|
|
12
|
+
* on-disk cache becomes unreachable. See design §8.
|
|
13
|
+
*
|
|
14
|
+
* 2. Wrap `src/cache/review-cache.ts` so callers get a single entry
|
|
15
|
+
* point — `checkReviewCache({ diff, branch, base })` — that hides
|
|
16
|
+
* the TTL semantics, the `(sha, branch, base)` key tuple, and the
|
|
17
|
+
* cache-lookup result shape.
|
|
18
|
+
*
|
|
19
|
+
* 3. Translate the cache lookup into the same three-way outcome the
|
|
20
|
+
* bash core's §1203 `jq -e '.hit == true and .result == "pass"'`
|
|
21
|
+
* check emitted:
|
|
22
|
+
*
|
|
23
|
+
* - `hit_pass` — cache hit AND result === 'pass'. Gate passes.
|
|
24
|
+
* - `hit_fail` — cache hit AND result === 'fail'. Cached
|
|
25
|
+
* negative verdict; gate blocks. (Bash §1197-1202
|
|
26
|
+
* rejects this; we preserve.)
|
|
27
|
+
* - `miss` — no hit / expired / empty file. Gate falls
|
|
28
|
+
* through to the review-required banner.
|
|
29
|
+
* - `query_error` — cache lookup threw. Bash §1180-1196 treated
|
|
30
|
+
* this as a `{"hit":false,"reason":"query_error"}`
|
|
31
|
+
* cached result; we carry the error body so the
|
|
32
|
+
* caller can emit the CACHE CHECK FAILED banner
|
|
33
|
+
* with the SANITIZED stderr (defect C0/C1 strip).
|
|
34
|
+
*
|
|
35
|
+
* ## Phase 2a scope
|
|
36
|
+
*
|
|
37
|
+
* This file exports pure functions over the already-TS
|
|
38
|
+
* `review-cache.ts`. No subprocess `spawn`, no CLI fork. The bash
|
|
39
|
+
* core's `rea cache check` subprocess hop is obviated entirely —
|
|
40
|
+
* once Phase 3 swaps the shim we no longer fork/exec node for the
|
|
41
|
+
* cache lookup at all.
|
|
42
|
+
*
|
|
43
|
+
* ## Phase 2b composition
|
|
44
|
+
*
|
|
45
|
+
* `runPushReviewGate` in `index.ts` calls `computeCacheKeyFromDiff` +
|
|
46
|
+
* `checkReviewCache` sequentially; the latter returns a discriminated
|
|
47
|
+
* outcome the gate branches on. No new behavior lands here — that's
|
|
48
|
+
* the `codex-gate.ts` / composition step.
|
|
49
|
+
*/
|
|
50
|
+
import { lookup } from '../../cache/review-cache.js';
|
|
51
|
+
import { computeCacheKey as computePhase1CacheKey } from './cache-key.js';
|
|
52
|
+
/**
|
|
53
|
+
* Compute the cache key for a diff. This is a thin re-export of Phase 1's
|
|
54
|
+
* `computeCacheKey` — exposed on this module so callers can use a single
|
|
55
|
+
* import when they need both the key AND the lookup.
|
|
56
|
+
*
|
|
57
|
+
* The function is UNCHANGED from Phase 1. The fixture suite in
|
|
58
|
+
* `cache.test.ts` proves byte-exact parity against
|
|
59
|
+
* `__fixtures__/cache-keys.json` for all six scenarios.
|
|
60
|
+
*/
|
|
61
|
+
export function computeCacheKey(diff) {
|
|
62
|
+
return computePhase1CacheKey({ diff });
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Perform a cache lookup and translate into the discriminated outcome.
|
|
66
|
+
*
|
|
67
|
+
* Does NOT throw on a lookup failure — every known error path routes
|
|
68
|
+
* through the `query_error` variant so the caller's single `switch`
|
|
69
|
+
* handles all four outcomes. This mirrors the bash core's
|
|
70
|
+
* `CACHE_STDOUT || CACHE_EXIT != 0 → {"hit":false,...}` collapse at
|
|
71
|
+
* §1180-1196.
|
|
72
|
+
*/
|
|
73
|
+
export async function checkReviewCache(input) {
|
|
74
|
+
const key = computeCacheKey(input.diff);
|
|
75
|
+
let result;
|
|
76
|
+
try {
|
|
77
|
+
result = await lookup(input.baseDir, {
|
|
78
|
+
sha: key,
|
|
79
|
+
branch: input.branch,
|
|
80
|
+
base: input.base,
|
|
81
|
+
...(input.nowMs !== undefined ? { nowMs: input.nowMs } : {}),
|
|
82
|
+
...(input.maxAgeSeconds !== undefined ? { maxAgeSeconds: input.maxAgeSeconds } : {}),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
87
|
+
return { kind: 'query_error', key, error: msg };
|
|
88
|
+
}
|
|
89
|
+
if (!result.hit) {
|
|
90
|
+
// `result.missReason` is always set when `hit` is false (see
|
|
91
|
+
// review-cache.ts's CacheLookupResult contract). The fallback to
|
|
92
|
+
// 'no-entry' is defensive for an ill-formed result we shouldn't see.
|
|
93
|
+
return {
|
|
94
|
+
kind: 'miss',
|
|
95
|
+
key,
|
|
96
|
+
reason: result.missReason ?? 'no-entry',
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
// Hit. Branch on verdict — the bash core requires BOTH hit==true AND
|
|
100
|
+
// result==pass. A hit with result==fail is a cached negative verdict
|
|
101
|
+
// and must NOT unblock the push.
|
|
102
|
+
const entry = result.entry;
|
|
103
|
+
if (entry === undefined) {
|
|
104
|
+
// Defensive: `hit: true` without an entry is an ill-formed result.
|
|
105
|
+
return { kind: 'miss', key, reason: 'no-entry' };
|
|
106
|
+
}
|
|
107
|
+
if (entry.result === 'pass') {
|
|
108
|
+
return { kind: 'hit_pass', key, recorded_at: entry.recorded_at };
|
|
109
|
+
}
|
|
110
|
+
// result === 'fail'
|
|
111
|
+
const hitFail = {
|
|
112
|
+
kind: 'hit_fail',
|
|
113
|
+
key,
|
|
114
|
+
recorded_at: entry.recorded_at,
|
|
115
|
+
};
|
|
116
|
+
if (entry.reason !== undefined && entry.reason.length > 0) {
|
|
117
|
+
hitFail.reason = entry.reason;
|
|
118
|
+
}
|
|
119
|
+
return hitFail;
|
|
120
|
+
}
|
|
@@ -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
|
+
];
|