@bookedsolid/rea 0.10.3 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.husky/pre-push +48 -162
- package/README.md +834 -552
- package/agents/codex-adversarial.md +5 -3
- package/commands/codex-review.md +3 -5
- package/dist/audit/append.d.ts +7 -32
- package/dist/audit/append.js +7 -35
- package/dist/cli/audit.d.ts +0 -31
- package/dist/cli/audit.js +5 -74
- package/dist/cli/doctor.d.ts +12 -0
- package/dist/cli/doctor.js +96 -17
- package/dist/cli/hook.d.ts +55 -0
- package/dist/cli/hook.js +138 -0
- package/dist/cli/index.js +5 -80
- package/dist/cli/init.js +1 -1
- package/dist/cli/install/gitignore.d.ts +2 -2
- package/dist/cli/install/gitignore.js +3 -3
- package/dist/cli/install/pre-push.d.ts +158 -272
- package/dist/cli/install/pre-push.js +491 -2633
- package/dist/cli/install/settings-merge.d.ts +17 -0
- package/dist/cli/install/settings-merge.js +48 -1
- package/dist/cli/upgrade.js +131 -3
- package/dist/config/tier-map.js +18 -25
- package/dist/hooks/push-gate/base.d.ts +104 -0
- package/dist/hooks/push-gate/base.js +198 -0
- package/dist/hooks/push-gate/codex-runner.d.ts +126 -0
- package/dist/hooks/push-gate/codex-runner.js +223 -0
- package/dist/hooks/push-gate/findings.d.ts +68 -0
- package/dist/hooks/push-gate/findings.js +142 -0
- package/dist/hooks/push-gate/halt.d.ts +28 -0
- package/dist/hooks/push-gate/halt.js +49 -0
- package/dist/hooks/push-gate/index.d.ts +98 -0
- package/dist/hooks/push-gate/index.js +416 -0
- package/dist/hooks/push-gate/policy.d.ts +55 -0
- package/dist/hooks/push-gate/policy.js +64 -0
- package/dist/hooks/push-gate/report.d.ts +89 -0
- package/dist/hooks/push-gate/report.js +140 -0
- package/dist/policy/loader.d.ts +15 -10
- package/dist/policy/loader.js +8 -6
- package/dist/policy/types.d.ts +73 -22
- package/package.json +1 -1
- package/scripts/tarball-smoke.sh +7 -2
- package/dist/cache/review-cache.d.ts +0 -115
- package/dist/cache/review-cache.js +0 -200
- package/dist/cli/cache.d.ts +0 -84
- package/dist/cli/cache.js +0 -150
- package/dist/hooks/review-gate/args.d.ts +0 -126
- package/dist/hooks/review-gate/args.js +0 -315
- package/dist/hooks/review-gate/audit.d.ts +0 -131
- package/dist/hooks/review-gate/audit.js +0 -181
- package/dist/hooks/review-gate/banner.d.ts +0 -97
- package/dist/hooks/review-gate/banner.js +0 -172
- package/dist/hooks/review-gate/base-resolve.d.ts +0 -155
- package/dist/hooks/review-gate/base-resolve.js +0 -247
- package/dist/hooks/review-gate/cache-key.d.ts +0 -55
- package/dist/hooks/review-gate/cache-key.js +0 -41
- package/dist/hooks/review-gate/cache.d.ts +0 -108
- package/dist/hooks/review-gate/cache.js +0 -120
- package/dist/hooks/review-gate/constants.d.ts +0 -26
- package/dist/hooks/review-gate/constants.js +0 -34
- package/dist/hooks/review-gate/diff.d.ts +0 -181
- package/dist/hooks/review-gate/diff.js +0 -232
- package/dist/hooks/review-gate/errors.d.ts +0 -72
- package/dist/hooks/review-gate/errors.js +0 -100
- package/dist/hooks/review-gate/hash.d.ts +0 -43
- package/dist/hooks/review-gate/hash.js +0 -46
- package/dist/hooks/review-gate/index.d.ts +0 -31
- package/dist/hooks/review-gate/index.js +0 -35
- package/dist/hooks/review-gate/metadata.d.ts +0 -98
- package/dist/hooks/review-gate/metadata.js +0 -158
- package/dist/hooks/review-gate/policy.d.ts +0 -55
- package/dist/hooks/review-gate/policy.js +0 -71
- package/dist/hooks/review-gate/protected-paths.d.ts +0 -46
- package/dist/hooks/review-gate/protected-paths.js +0 -76
- package/hooks/_lib/push-review-core.sh +0 -1250
- package/hooks/commit-review-gate.sh +0 -330
- package/hooks/push-review-gate-git.sh +0 -94
- package/hooks/push-review-gate.sh +0 -92
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Review cache (BUG-009). The push-review-gate hook (`hooks/push-review-gate.sh`)
|
|
3
|
-
* has shipped since 0.3.x calling `rea cache check <sha>` to skip re-review on
|
|
4
|
-
* a previously-approved diff, and `rea cache set <sha> pass ...` as the
|
|
5
|
-
* operator's advertised way to complete the gate. Neither subcommand existed
|
|
6
|
-
* in the CLI through 0.4.0. Once BUG-008's pre-push stdin adapter lands and
|
|
7
|
-
* the gate actually fires, a protected-path push has no completion path
|
|
8
|
-
* without this cache — hence "paired ship blocker."
|
|
9
|
-
*
|
|
10
|
-
* ## File layout
|
|
11
|
-
*
|
|
12
|
-
* `.rea/review-cache.jsonl` — one JSON object per line, terminated with `\n`.
|
|
13
|
-
* Each entry:
|
|
14
|
-
*
|
|
15
|
-
* {
|
|
16
|
-
* "sha": "<diff-sha256>",
|
|
17
|
-
* "branch": "<feature-branch>",
|
|
18
|
-
* "base": "<target-branch>",
|
|
19
|
-
* "result": "pass" | "fail",
|
|
20
|
-
* "recorded_at": "<ISO-8601>",
|
|
21
|
-
* "reason"?: "<free text>" // optional, populated on fail or on skip
|
|
22
|
-
* }
|
|
23
|
-
*
|
|
24
|
-
* The `sha` is whatever the caller supplies — the hook happens to use a
|
|
25
|
-
* SHA-256 of the full diff, but the cache does not interpret or validate the
|
|
26
|
-
* value. Hash-chained is intentionally NOT required: this is a keyed cache,
|
|
27
|
-
* not an append-only integrity log. The audit log at `.rea/audit.jsonl`
|
|
28
|
-
* remains the integrity story.
|
|
29
|
-
*
|
|
30
|
-
* ## Concurrency
|
|
31
|
-
*
|
|
32
|
-
* Every write takes the same `proper-lockfile` lock on the `.rea/` parent
|
|
33
|
-
* directory that the audit helpers use (`withAuditLock`). This means a
|
|
34
|
-
* concurrent audit append and cache write serialize against each other — a
|
|
35
|
-
* negligible cost given cache writes happen once per push gate completion.
|
|
36
|
-
*
|
|
37
|
-
* ## Idempotency
|
|
38
|
-
*
|
|
39
|
-
* `appendEntry` writes a new line unconditionally. `lookup` returns the most
|
|
40
|
-
* recent entry matching `(sha, branch, base)`. This "last write wins" keeps
|
|
41
|
-
* the write path O(1) and the read path O(n) over the file; n is bounded by
|
|
42
|
-
* typical review frequency (dozens per week, not millions). If a future
|
|
43
|
-
* operator needs a compact file, `rea cache clear <sha>` drops matching
|
|
44
|
-
* entries and a separate `rea cache compact` (not in 0.5.0) could rewrite.
|
|
45
|
-
*
|
|
46
|
-
* ## TTL
|
|
47
|
-
*
|
|
48
|
-
* `lookup` honors `review.cache_max_age_seconds` (default 3600). Entries
|
|
49
|
-
* older than the window are treated as a miss. Expired entries are not
|
|
50
|
-
* garbage-collected on read — `rea cache clear` or `rea cache compact`
|
|
51
|
-
* is the operator tool for shrinking.
|
|
52
|
-
*/
|
|
53
|
-
import fs from 'node:fs/promises';
|
|
54
|
-
import path from 'node:path';
|
|
55
|
-
import { withAuditLock } from '../audit/fs.js';
|
|
56
|
-
/** Default TTL when policy does not supply one. */
|
|
57
|
-
export const DEFAULT_CACHE_MAX_AGE_SECONDS = 3600;
|
|
58
|
-
/**
|
|
59
|
-
* Tolerated clock skew for future-dated entries. A `recorded_at` more than
|
|
60
|
-
* this far in the future relative to `nowMs` is treated as tampered or
|
|
61
|
-
* severely-drifted and forces a miss (re-review). 60s covers NTP jitter on
|
|
62
|
-
* well-synced hosts; anything beyond that is noise we do not trust.
|
|
63
|
-
*/
|
|
64
|
-
const FUTURE_SKEW_ALLOWANCE_MS = 60_000;
|
|
65
|
-
const CACHE_FILENAME = 'review-cache.jsonl';
|
|
66
|
-
const REA_DIRNAME = '.rea';
|
|
67
|
-
export function resolveCacheFile(baseDir) {
|
|
68
|
-
return path.join(baseDir, REA_DIRNAME, CACHE_FILENAME);
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Load every entry from the cache file. Returns `[]` when the file does not
|
|
72
|
-
* exist or is empty. Malformed lines are skipped — we never throw on a
|
|
73
|
-
* corrupt line, because the cache is advisory and a bad write (e.g. a
|
|
74
|
-
* half-written line from a crashed host) must not block a subsequent push.
|
|
75
|
-
*/
|
|
76
|
-
async function loadEntries(cacheFile) {
|
|
77
|
-
let raw;
|
|
78
|
-
try {
|
|
79
|
-
raw = await fs.readFile(cacheFile, 'utf8');
|
|
80
|
-
}
|
|
81
|
-
catch (err) {
|
|
82
|
-
if (err.code === 'ENOENT')
|
|
83
|
-
return [];
|
|
84
|
-
throw err;
|
|
85
|
-
}
|
|
86
|
-
if (raw.length === 0)
|
|
87
|
-
return [];
|
|
88
|
-
const entries = [];
|
|
89
|
-
for (const line of raw.split('\n')) {
|
|
90
|
-
if (line.length === 0)
|
|
91
|
-
continue;
|
|
92
|
-
try {
|
|
93
|
-
const parsed = JSON.parse(line);
|
|
94
|
-
if (typeof parsed.sha === 'string' &&
|
|
95
|
-
typeof parsed.branch === 'string' &&
|
|
96
|
-
typeof parsed.base === 'string' &&
|
|
97
|
-
(parsed.result === 'pass' || parsed.result === 'fail') &&
|
|
98
|
-
typeof parsed.recorded_at === 'string') {
|
|
99
|
-
entries.push(parsed);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
catch {
|
|
103
|
-
// Skip malformed line.
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
return entries;
|
|
107
|
-
}
|
|
108
|
-
/**
|
|
109
|
-
* Append an entry to the cache. Writes are serialized through the shared
|
|
110
|
-
* `.rea/` directory lock so audit writes and cache writes do not interleave.
|
|
111
|
-
*/
|
|
112
|
-
export async function appendEntry(baseDir, input) {
|
|
113
|
-
const cacheFile = resolveCacheFile(baseDir);
|
|
114
|
-
await fs.mkdir(path.dirname(cacheFile), { recursive: true });
|
|
115
|
-
const entry = {
|
|
116
|
-
sha: input.sha,
|
|
117
|
-
branch: input.branch,
|
|
118
|
-
base: input.base,
|
|
119
|
-
result: input.result,
|
|
120
|
-
recorded_at: input.timestamp ?? new Date().toISOString(),
|
|
121
|
-
...(input.reason !== undefined && input.reason.length > 0
|
|
122
|
-
? { reason: input.reason }
|
|
123
|
-
: {}),
|
|
124
|
-
};
|
|
125
|
-
await withAuditLock(cacheFile, async () => {
|
|
126
|
-
const line = JSON.stringify(entry) + '\n';
|
|
127
|
-
await fs.appendFile(cacheFile, line);
|
|
128
|
-
});
|
|
129
|
-
return entry;
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* Find the most-recent entry matching `(sha, branch, base)` within the TTL
|
|
133
|
-
* window. Idempotent and side-effect free.
|
|
134
|
-
*/
|
|
135
|
-
export async function lookup(baseDir, input) {
|
|
136
|
-
const cacheFile = resolveCacheFile(baseDir);
|
|
137
|
-
const entries = await loadEntries(cacheFile);
|
|
138
|
-
if (entries.length === 0)
|
|
139
|
-
return { hit: false, missReason: 'empty-file' };
|
|
140
|
-
// Walk from the tail so the first match is the newest.
|
|
141
|
-
let matched;
|
|
142
|
-
for (let i = entries.length - 1; i >= 0; i--) {
|
|
143
|
-
const e = entries[i];
|
|
144
|
-
if (e.sha === input.sha && e.branch === input.branch && e.base === input.base) {
|
|
145
|
-
matched = e;
|
|
146
|
-
break;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
if (matched === undefined)
|
|
150
|
-
return { hit: false, missReason: 'no-entry' };
|
|
151
|
-
const nowMs = input.nowMs ?? Date.now();
|
|
152
|
-
const maxAgeSeconds = input.maxAgeSeconds ?? DEFAULT_CACHE_MAX_AGE_SECONDS;
|
|
153
|
-
const recordedMs = Date.parse(matched.recorded_at);
|
|
154
|
-
if (Number.isNaN(recordedMs)) {
|
|
155
|
-
// Corrupt timestamp — treat as an expired miss so the caller re-reviews.
|
|
156
|
-
return { hit: false, missReason: 'expired', entry: matched };
|
|
157
|
-
}
|
|
158
|
-
if (recordedMs > nowMs + FUTURE_SKEW_ALLOWANCE_MS) {
|
|
159
|
-
return { hit: false, missReason: 'expired', entry: matched };
|
|
160
|
-
}
|
|
161
|
-
if ((nowMs - recordedMs) / 1000 > maxAgeSeconds) {
|
|
162
|
-
return { hit: false, missReason: 'expired', entry: matched };
|
|
163
|
-
}
|
|
164
|
-
return { hit: true, entry: matched };
|
|
165
|
-
}
|
|
166
|
-
/**
|
|
167
|
-
* Remove every entry matching `sha`. Returns the count removed. A `0` return
|
|
168
|
-
* is a valid outcome (sha not present). Writes back via the same lock as
|
|
169
|
-
* `appendEntry`, so concurrent sets do not lose entries.
|
|
170
|
-
*
|
|
171
|
-
* Writes use temp-file + `fs.rename` (atomic within a single directory on
|
|
172
|
-
* POSIX) so unlocked readers (`lookup`, `list`) can never observe a torn or
|
|
173
|
-
* empty intermediate state. Codex F4 on the 0.5.0 PR1 review.
|
|
174
|
-
*/
|
|
175
|
-
export async function clear(baseDir, sha) {
|
|
176
|
-
const cacheFile = resolveCacheFile(baseDir);
|
|
177
|
-
return withAuditLock(cacheFile, async () => {
|
|
178
|
-
const entries = await loadEntries(cacheFile);
|
|
179
|
-
const kept = entries.filter((e) => e.sha !== sha);
|
|
180
|
-
const removed = entries.length - kept.length;
|
|
181
|
-
if (removed === 0)
|
|
182
|
-
return 0;
|
|
183
|
-
const out = kept.length === 0 ? '' : kept.map((e) => JSON.stringify(e)).join('\n') + '\n';
|
|
184
|
-
const tmpFile = `${cacheFile}.tmp.${process.pid}.${Date.now()}`;
|
|
185
|
-
await fs.writeFile(tmpFile, out);
|
|
186
|
-
await fs.rename(tmpFile, cacheFile);
|
|
187
|
-
return removed;
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
/**
|
|
191
|
-
* Return every entry, optionally filtered by branch. Entries are returned in
|
|
192
|
-
* file order (oldest first). Callers that want "newest first" should reverse.
|
|
193
|
-
*/
|
|
194
|
-
export async function list(baseDir, options = {}) {
|
|
195
|
-
const cacheFile = resolveCacheFile(baseDir);
|
|
196
|
-
const entries = await loadEntries(cacheFile);
|
|
197
|
-
if (options.branch === undefined)
|
|
198
|
-
return entries;
|
|
199
|
-
return entries.filter((e) => e.branch === options.branch);
|
|
200
|
-
}
|
package/dist/cli/cache.d.ts
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `rea cache` — push-review cache operator subcommands (BUG-009).
|
|
3
|
-
*
|
|
4
|
-
* Four verbs:
|
|
5
|
-
* - `check <sha> --branch <b> --base <b>` — JSON to stdout ONLY; never
|
|
6
|
-
* diagnostics. `hooks/push-review-gate.sh` reads this via
|
|
7
|
-
* `printf '%s' "$CACHE_RESULT" | jq -e '.hit == true'`, so any stray
|
|
8
|
-
* text on stdout would poison the hook's JSON parse.
|
|
9
|
-
* - `set <sha> pass|fail --branch <b> --base <b> [--reason <s>]` — record
|
|
10
|
-
* a review outcome.
|
|
11
|
-
* - `clear <sha>` — drop every entry for a sha (dev convenience).
|
|
12
|
-
* - `list [--branch <b>]` — pretty-print entries.
|
|
13
|
-
*
|
|
14
|
-
* The TTL used by `check` reads `review.cache_max_age_seconds` from
|
|
15
|
-
* `.rea/policy.yaml` when present, falling back to
|
|
16
|
-
* {@link DEFAULT_CACHE_MAX_AGE_SECONDS} (1 hour) when the policy file or
|
|
17
|
-
* field is absent. An unreadable/malformed policy file is NOT fatal for
|
|
18
|
-
* `check` — it degrades to the default so a broken policy never deadlocks
|
|
19
|
-
* the push gate; other commands that don't consume the TTL ignore the policy
|
|
20
|
-
* entirely.
|
|
21
|
-
*/
|
|
22
|
-
import { type CacheResult } from '../cache/review-cache.js';
|
|
23
|
-
import type { CodexVerdict } from '../audit/codex-event.js';
|
|
24
|
-
export interface CacheCheckOptions {
|
|
25
|
-
sha: string;
|
|
26
|
-
branch: string;
|
|
27
|
-
base: string;
|
|
28
|
-
}
|
|
29
|
-
export interface CacheSetOptions {
|
|
30
|
-
sha: string;
|
|
31
|
-
result: CacheResult;
|
|
32
|
-
branch: string;
|
|
33
|
-
base: string;
|
|
34
|
-
reason?: string;
|
|
35
|
-
}
|
|
36
|
-
export interface CacheClearOptions {
|
|
37
|
-
sha: string;
|
|
38
|
-
}
|
|
39
|
-
export interface CacheListOptions {
|
|
40
|
-
branch?: string;
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Print the cache-check JSON to stdout. Hook contract: stdout is ONLY JSON.
|
|
44
|
-
* On a miss we still exit 0 with `{"hit":false}` — the hook interprets
|
|
45
|
-
* non-zero as "rea broken, force re-review" via its `|| echo '{"hit":false}'`
|
|
46
|
-
* fallback.
|
|
47
|
-
*/
|
|
48
|
-
export declare function runCacheCheck(options: CacheCheckOptions): Promise<void>;
|
|
49
|
-
export declare function runCacheSet(options: CacheSetOptions): Promise<void>;
|
|
50
|
-
export declare function runCacheClear(options: CacheClearOptions): Promise<void>;
|
|
51
|
-
export declare function runCacheList(options: CacheListOptions): Promise<void>;
|
|
52
|
-
/** Parse-and-validate helper for `set` — surfaces a clean error on bad input.
|
|
53
|
-
*
|
|
54
|
-
* Accepts the two historical cache values (`pass`, `fail`) AND the four
|
|
55
|
-
* canonical Codex verdicts (`pass`, `concerns`, `blocking`, `error`) per
|
|
56
|
-
* Defect D (rea#77). Codex verdicts are mapped to cache semantics at the CLI
|
|
57
|
-
* boundary: `pass|concerns` → gate-satisfying `pass`; `blocking|error` →
|
|
58
|
-
* gate-failing `fail`. The cache internal vocabulary stays binary
|
|
59
|
-
* (`pass`/`fail` = "gate-satisfying?") while the CLI accepts the full Codex
|
|
60
|
-
* vocabulary so agents can copy the `/codex-review` verdict verbatim.
|
|
61
|
-
*/
|
|
62
|
-
export declare function parseCacheResult(raw: string): CacheResult;
|
|
63
|
-
/** Shape returned by {@link codexVerdictToCacheResult}: the binary cache result
|
|
64
|
-
* plus an optional machine-readable `reason` string that records the source
|
|
65
|
-
* Codex verdict. `reason` is populated for non-`pass` verdicts so downstream
|
|
66
|
-
* listings expose WHY a cache fail was recorded. */
|
|
67
|
-
export interface CodexVerdictCacheEffect {
|
|
68
|
-
result: CacheResult;
|
|
69
|
-
reason?: string | undefined;
|
|
70
|
-
}
|
|
71
|
-
/** Map a Codex verdict to the binary cache result the gate compares against.
|
|
72
|
-
*
|
|
73
|
-
* Mapping rationale:
|
|
74
|
-
* - `pass` → cache `pass` (clean review, gate should pass)
|
|
75
|
-
* - `concerns` → cache `pass` (non-blocking findings, gate should pass;
|
|
76
|
-
* reviewer captured concerns in the audit record `metadata.summary`)
|
|
77
|
-
* - `blocking` → cache `fail` (must address findings before merge)
|
|
78
|
-
* - `error` → cache `fail` (Codex itself errored; no clean-bill-of-health)
|
|
79
|
-
*
|
|
80
|
-
* Kept separate from `parseCacheResult` so callers that already have a typed
|
|
81
|
-
* `CodexVerdict` (e.g. `rea audit record codex-review --also-set-cache`) don't
|
|
82
|
-
* round-trip through string parsing.
|
|
83
|
-
*/
|
|
84
|
-
export declare function codexVerdictToCacheResult(verdict: CodexVerdict): CodexVerdictCacheEffect;
|
package/dist/cli/cache.js
DELETED
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `rea cache` — push-review cache operator subcommands (BUG-009).
|
|
3
|
-
*
|
|
4
|
-
* Four verbs:
|
|
5
|
-
* - `check <sha> --branch <b> --base <b>` — JSON to stdout ONLY; never
|
|
6
|
-
* diagnostics. `hooks/push-review-gate.sh` reads this via
|
|
7
|
-
* `printf '%s' "$CACHE_RESULT" | jq -e '.hit == true'`, so any stray
|
|
8
|
-
* text on stdout would poison the hook's JSON parse.
|
|
9
|
-
* - `set <sha> pass|fail --branch <b> --base <b> [--reason <s>]` — record
|
|
10
|
-
* a review outcome.
|
|
11
|
-
* - `clear <sha>` — drop every entry for a sha (dev convenience).
|
|
12
|
-
* - `list [--branch <b>]` — pretty-print entries.
|
|
13
|
-
*
|
|
14
|
-
* The TTL used by `check` reads `review.cache_max_age_seconds` from
|
|
15
|
-
* `.rea/policy.yaml` when present, falling back to
|
|
16
|
-
* {@link DEFAULT_CACHE_MAX_AGE_SECONDS} (1 hour) when the policy file or
|
|
17
|
-
* field is absent. An unreadable/malformed policy file is NOT fatal for
|
|
18
|
-
* `check` — it degrades to the default so a broken policy never deadlocks
|
|
19
|
-
* the push gate; other commands that don't consume the TTL ignore the policy
|
|
20
|
-
* entirely.
|
|
21
|
-
*/
|
|
22
|
-
import { loadPolicy } from '../policy/loader.js';
|
|
23
|
-
import { DEFAULT_CACHE_MAX_AGE_SECONDS, appendEntry, clear as clearEntries, list as listEntries, lookup, } from '../cache/review-cache.js';
|
|
24
|
-
import { err, log } from './utils.js';
|
|
25
|
-
function resolveMaxAgeSeconds(baseDir) {
|
|
26
|
-
try {
|
|
27
|
-
const policy = loadPolicy(baseDir);
|
|
28
|
-
const configured = policy.review?.cache_max_age_seconds;
|
|
29
|
-
if (typeof configured === 'number' && configured > 0)
|
|
30
|
-
return configured;
|
|
31
|
-
return DEFAULT_CACHE_MAX_AGE_SECONDS;
|
|
32
|
-
}
|
|
33
|
-
catch {
|
|
34
|
-
// Missing or malformed policy must not block the push gate — degrade to
|
|
35
|
-
// the default. `rea doctor` is the canonical surface for flagging a
|
|
36
|
-
// broken policy file; the cache is not the place to re-diagnose it.
|
|
37
|
-
return DEFAULT_CACHE_MAX_AGE_SECONDS;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Print the cache-check JSON to stdout. Hook contract: stdout is ONLY JSON.
|
|
42
|
-
* On a miss we still exit 0 with `{"hit":false}` — the hook interprets
|
|
43
|
-
* non-zero as "rea broken, force re-review" via its `|| echo '{"hit":false}'`
|
|
44
|
-
* fallback.
|
|
45
|
-
*/
|
|
46
|
-
export async function runCacheCheck(options) {
|
|
47
|
-
const baseDir = process.cwd();
|
|
48
|
-
const maxAgeSeconds = resolveMaxAgeSeconds(baseDir);
|
|
49
|
-
const result = await lookup(baseDir, {
|
|
50
|
-
sha: options.sha,
|
|
51
|
-
branch: options.branch,
|
|
52
|
-
base: options.base,
|
|
53
|
-
maxAgeSeconds,
|
|
54
|
-
});
|
|
55
|
-
if (result.hit && result.entry !== undefined) {
|
|
56
|
-
const payload = {
|
|
57
|
-
hit: true,
|
|
58
|
-
result: result.entry.result,
|
|
59
|
-
branch: result.entry.branch,
|
|
60
|
-
base: result.entry.base,
|
|
61
|
-
recorded_at: result.entry.recorded_at,
|
|
62
|
-
...(result.entry.reason !== undefined ? { reason: result.entry.reason } : {}),
|
|
63
|
-
};
|
|
64
|
-
process.stdout.write(JSON.stringify(payload) + '\n');
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
process.stdout.write(JSON.stringify({ hit: false }) + '\n');
|
|
68
|
-
}
|
|
69
|
-
export async function runCacheSet(options) {
|
|
70
|
-
const baseDir = process.cwd();
|
|
71
|
-
const entry = await appendEntry(baseDir, {
|
|
72
|
-
sha: options.sha,
|
|
73
|
-
branch: options.branch,
|
|
74
|
-
base: options.base,
|
|
75
|
-
result: options.result,
|
|
76
|
-
...(options.reason !== undefined && options.reason.length > 0
|
|
77
|
-
? { reason: options.reason }
|
|
78
|
-
: {}),
|
|
79
|
-
});
|
|
80
|
-
log(`Recorded ${entry.result} for ${entry.sha.slice(0, 12)} (${entry.branch} → ${entry.base}).`);
|
|
81
|
-
}
|
|
82
|
-
export async function runCacheClear(options) {
|
|
83
|
-
const baseDir = process.cwd();
|
|
84
|
-
const removed = await clearEntries(baseDir, options.sha);
|
|
85
|
-
if (removed === 0) {
|
|
86
|
-
log(`No entries found for ${options.sha.slice(0, 12)}.`);
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
log(`Cleared ${removed} entr${removed === 1 ? 'y' : 'ies'} for ${options.sha.slice(0, 12)}.`);
|
|
90
|
-
}
|
|
91
|
-
export async function runCacheList(options) {
|
|
92
|
-
const baseDir = process.cwd();
|
|
93
|
-
const entries = await listEntries(baseDir, {
|
|
94
|
-
...(options.branch !== undefined ? { branch: options.branch } : {}),
|
|
95
|
-
});
|
|
96
|
-
if (entries.length === 0) {
|
|
97
|
-
log('No review-cache entries.');
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
for (const e of entries) {
|
|
101
|
-
const shortSha = e.sha.slice(0, 12);
|
|
102
|
-
const reason = e.reason !== undefined ? ` — ${e.reason}` : '';
|
|
103
|
-
console.log(`${e.recorded_at} ${e.result.padEnd(4)} ${shortSha} ${e.branch} → ${e.base}${reason}`);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
/** Parse-and-validate helper for `set` — surfaces a clean error on bad input.
|
|
107
|
-
*
|
|
108
|
-
* Accepts the two historical cache values (`pass`, `fail`) AND the four
|
|
109
|
-
* canonical Codex verdicts (`pass`, `concerns`, `blocking`, `error`) per
|
|
110
|
-
* Defect D (rea#77). Codex verdicts are mapped to cache semantics at the CLI
|
|
111
|
-
* boundary: `pass|concerns` → gate-satisfying `pass`; `blocking|error` →
|
|
112
|
-
* gate-failing `fail`. The cache internal vocabulary stays binary
|
|
113
|
-
* (`pass`/`fail` = "gate-satisfying?") while the CLI accepts the full Codex
|
|
114
|
-
* vocabulary so agents can copy the `/codex-review` verdict verbatim.
|
|
115
|
-
*/
|
|
116
|
-
export function parseCacheResult(raw) {
|
|
117
|
-
if (raw === 'pass' || raw === 'fail')
|
|
118
|
-
return raw;
|
|
119
|
-
if (raw === 'concerns')
|
|
120
|
-
return 'pass';
|
|
121
|
-
if (raw === 'blocking' || raw === 'error')
|
|
122
|
-
return 'fail';
|
|
123
|
-
err(`result must be 'pass', 'fail', 'concerns', 'blocking', or 'error'; got ${JSON.stringify(raw)}`);
|
|
124
|
-
process.exit(1);
|
|
125
|
-
}
|
|
126
|
-
/** Map a Codex verdict to the binary cache result the gate compares against.
|
|
127
|
-
*
|
|
128
|
-
* Mapping rationale:
|
|
129
|
-
* - `pass` → cache `pass` (clean review, gate should pass)
|
|
130
|
-
* - `concerns` → cache `pass` (non-blocking findings, gate should pass;
|
|
131
|
-
* reviewer captured concerns in the audit record `metadata.summary`)
|
|
132
|
-
* - `blocking` → cache `fail` (must address findings before merge)
|
|
133
|
-
* - `error` → cache `fail` (Codex itself errored; no clean-bill-of-health)
|
|
134
|
-
*
|
|
135
|
-
* Kept separate from `parseCacheResult` so callers that already have a typed
|
|
136
|
-
* `CodexVerdict` (e.g. `rea audit record codex-review --also-set-cache`) don't
|
|
137
|
-
* round-trip through string parsing.
|
|
138
|
-
*/
|
|
139
|
-
export function codexVerdictToCacheResult(verdict) {
|
|
140
|
-
switch (verdict) {
|
|
141
|
-
case 'pass':
|
|
142
|
-
return { result: 'pass' };
|
|
143
|
-
case 'concerns':
|
|
144
|
-
return { result: 'pass', reason: 'codex:concerns' };
|
|
145
|
-
case 'blocking':
|
|
146
|
-
return { result: 'fail', reason: 'codex:blocking' };
|
|
147
|
-
case 'error':
|
|
148
|
-
return { result: 'fail', reason: 'codex:error' };
|
|
149
|
-
}
|
|
150
|
-
}
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Refspec parsing. Two input shapes the gate must accept:
|
|
3
|
-
*
|
|
4
|
-
* 1. Git pre-push hook stdin — one line per refspec, fields:
|
|
5
|
-
* `<local_ref> <local_sha> <remote_ref> <remote_sha>`
|
|
6
|
-
* (https://git-scm.com/docs/githooks#_pre_push)
|
|
7
|
-
*
|
|
8
|
-
* 2. Claude-Code `Bash`-PreToolUse command string — parse `git push [remote]
|
|
9
|
-
* [refspec...]` out of the command, synthesize refspec records against
|
|
10
|
-
* the caller's HEAD/@{upstream}.
|
|
11
|
-
*
|
|
12
|
-
* Shape 1 is authoritative when present; shape 2 is a fallback for the
|
|
13
|
-
* Claude-Code adapter (BUG-008 sniff). See design §3.2 for the adapter
|
|
14
|
-
* split and §5.1 for the scenarios covered by unit tests.
|
|
15
|
-
*
|
|
16
|
-
* ## Defect J — mixed-push deletion guard
|
|
17
|
-
*
|
|
18
|
-
* A push like `git push origin safe:safe :main` contains both a push refspec
|
|
19
|
-
* and a deletion refspec. The bash core has been burned twice by nesting the
|
|
20
|
-
* deletion check inside the "no SOURCE_SHA resolved" fallback branch, which
|
|
21
|
-
* lets the deletion slip through whenever a sibling refspec DID resolve.
|
|
22
|
-
* This module exposes `hasDeletion()` as a separate predicate so the caller
|
|
23
|
-
* can fail-closed on deletions up front, before any refspec-selection logic.
|
|
24
|
-
*/
|
|
25
|
-
/**
|
|
26
|
-
* One parsed refspec record. Either a push (local_sha != ZERO_SHA) or a
|
|
27
|
-
* deletion (local_sha === ZERO_SHA). `source_is_head` flags the
|
|
28
|
-
* argv-fallback case where no explicit source ref was named and the parser
|
|
29
|
-
* substituted HEAD.
|
|
30
|
-
*/
|
|
31
|
-
export interface RefspecRecord {
|
|
32
|
-
local_sha: string;
|
|
33
|
-
remote_sha: string;
|
|
34
|
-
local_ref: string;
|
|
35
|
-
remote_ref: string;
|
|
36
|
-
/** True when the parser had to fall back to HEAD for the source ref. */
|
|
37
|
-
source_is_head: boolean;
|
|
38
|
-
/** True when `local_sha === ZERO_SHA` (this refspec is a branch deletion). */
|
|
39
|
-
is_deletion: boolean;
|
|
40
|
-
}
|
|
41
|
-
export interface ParseStdinResult {
|
|
42
|
-
records: RefspecRecord[];
|
|
43
|
-
/** The parser accepted at least one well-formed line from stdin. */
|
|
44
|
-
matched: boolean;
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Parse the git pre-push stdin contract.
|
|
48
|
-
*
|
|
49
|
-
* Returns `{ records, matched: true }` when at least one refspec line
|
|
50
|
-
* parsed cleanly; `{ records: [], matched: false }` otherwise.
|
|
51
|
-
*
|
|
52
|
-
* ## Bash-core parity (push-review-core.sh §45-69)
|
|
53
|
-
*
|
|
54
|
-
* The bash parser uses `read -r local_ref local_sha remote_ref remote_sha rest`
|
|
55
|
-
* against each line, so:
|
|
56
|
-
* - Lines with fewer than the required fields leave some vars empty and
|
|
57
|
-
* the loop `continue`s via the `-z` check (line 54-56). Parser does NOT
|
|
58
|
-
* abort the overall parse — subsequent lines still get a chance.
|
|
59
|
-
* - Extra whitespace-separated fields collapse into `rest` and are
|
|
60
|
-
* silently dropped (line 53's `rest` capture absorbs everything past
|
|
61
|
-
* field four).
|
|
62
|
-
* - Only a 40-hex SHA failure on either sha triggers `return 1` (line
|
|
63
|
-
* 57-59), aborting the whole parse — the caller falls through to argv.
|
|
64
|
-
* - If no lines accept (`accepted=0` at line 63-65), the parser also
|
|
65
|
-
* returns 1.
|
|
66
|
-
*
|
|
67
|
-
* We mirror that exactly. Codex pass-1 on phase 1 flagged an earlier
|
|
68
|
-
* too-strict version that aborted on short/long lines and would have
|
|
69
|
-
* starved the authoritative stdin path when consumer pre-push wrappers
|
|
70
|
-
* emit extra trailing whitespace columns (e.g. a comment or a trailing
|
|
71
|
-
* remote-url duplicate).
|
|
72
|
-
*
|
|
73
|
-
* Empty / whitespace-only lines are skipped silently.
|
|
74
|
-
*
|
|
75
|
-
* @param raw the full stdin bytes as a string
|
|
76
|
-
*/
|
|
77
|
-
export declare function parsePrepushStdin(raw: string): ParseStdinResult;
|
|
78
|
-
/**
|
|
79
|
-
* Return true iff any refspec in the list is a branch deletion (defect J).
|
|
80
|
-
* Callers must check this before any refspec-selection pass; the bash core
|
|
81
|
-
* pre-0.9.4 nested the check inside the "no SOURCE_SHA resolved" branch and
|
|
82
|
-
* let mixed pushes bypass the gate.
|
|
83
|
-
*/
|
|
84
|
-
export declare function hasDeletion(records: RefspecRecord[]): boolean;
|
|
85
|
-
/**
|
|
86
|
-
* A `ResolveHead` callback returns the SHA of a source ref, or `null` when
|
|
87
|
-
* the ref is unknown. Injected here (rather than shelling out to `git`
|
|
88
|
-
* directly) so `args.ts` stays pure and unit-testable without a git repo.
|
|
89
|
-
* The real implementation lives in `diff.ts` / `base-resolve.ts`.
|
|
90
|
-
*/
|
|
91
|
-
export type ResolveHead = (ref: string) => string | null;
|
|
92
|
-
export interface ArgvFallbackDeps {
|
|
93
|
-
/** Resolve a source ref (e.g. `feature/foo`) to a commit SHA, or null. */
|
|
94
|
-
resolveHead: ResolveHead;
|
|
95
|
-
/** Current HEAD SHA for bare `git push` with no explicit refspec. */
|
|
96
|
-
headSha: string;
|
|
97
|
-
/** `@{upstream}` short name (e.g. `origin/main`) or null. */
|
|
98
|
-
upstream: string | null;
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* Parse refspecs out of a `git push [remote] [refspec...]` command string.
|
|
102
|
-
* Used only when stdin-parsing returned `matched: false` (Claude-Code
|
|
103
|
-
* adapter path).
|
|
104
|
-
*
|
|
105
|
-
* Behavior mirrors the bash core's `pr_resolve_argv_refspecs` exactly:
|
|
106
|
-
* - Bare `git push` with no explicit refspec → synthesize a single record
|
|
107
|
-
* against `@{upstream}` (or `main` when no upstream), local_sha = HEAD.
|
|
108
|
-
* - `git push origin foo` → source=foo, dest=foo.
|
|
109
|
-
* - `git push origin src:dst` → source=src, dest=dst.
|
|
110
|
-
* - `git push origin :main` → deletion record.
|
|
111
|
-
* - `git push origin --delete main` → deletion record.
|
|
112
|
-
* - `git push origin HEAD:main` → resolves via `resolveHead('HEAD')`;
|
|
113
|
-
* the bash core rejects HEAD only when it lands on the DESTINATION
|
|
114
|
-
* side of the refspec (dst == 'HEAD'), not the source side. We match.
|
|
115
|
-
* - `git push origin HEAD` → HeadRefspecBlockedError (dst resolves to
|
|
116
|
-
* HEAD because src==dst when no colon is present).
|
|
117
|
-
*
|
|
118
|
-
* Throws `BlockedError` subclasses for operator-error conditions so the
|
|
119
|
-
* caller can translate them to exit 2 + banner identical to the bash core.
|
|
120
|
-
*/
|
|
121
|
-
export declare function resolveArgvRefspecs(cmd: string, deps: ArgvFallbackDeps): RefspecRecord[];
|
|
122
|
-
/**
|
|
123
|
-
* Strip `refs/heads/` or `refs/for/` prefixes so caller-facing code sees a
|
|
124
|
-
* bare branch name. Exported for unit tests in `args.test.ts`.
|
|
125
|
-
*/
|
|
126
|
-
export declare function stripRefsPrefix(ref: string): string;
|