Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ Use `docs/codex-review-protocol.md` as the shared review protocol for every repo

Before reviewing a branch or PR:

- Run `npm run ledger:lookup -- <branch-or-ref> --scope "<scope>"`. It resolves the HEAD, matches the abbreviated SHAs older records used, and prints an explicit verdict. Do not read `docs/branch-review-ledger.md` by eye — it is over a thousand rows, and eyeballing it is how repeat reviews slipped through.
- Run `npm run ledger:lookup -- <branch-or-ref> --scope "<scope>"`. It resolves the HEAD, matches the abbreviated SHAs older records used, and prints an explicit verdict. Do not read `docs/branch-review-ledger.md` by eye — live + archive rows are many, and eyeballing is how repeat reviews slipped through. `ledger:lookup` reads archives under `docs/archive/branch-review-ledger-*.md` too.
- On `ALREADY REVIEWED`, summarize the prior ledger outcome and skip the repeat review unless the user explicitly requests a fresh pass.
- On `NOT REVIEWED at this HEAD`, review only the changed scope and append a record after the review.

Expand Down
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ npm run docs:check-links
- [codex-review-protocol.md](codex-review-protocol.md) — shared review protocol for all review skills
- [codex-prompt-playbook.md](codex-prompt-playbook.md) — copy/paste prompts for common repo work
- [branch-cleanup-guide.md](branch-cleanup-guide.md) — branch hygiene workflow
- [branch-review-ledger.md](branch-review-ledger.md) — reviewed branch/SHA ledger; read it with `npm run ledger:lookup`, write it with `npm run ledger:append` (never by hand)
- [branch-review-ledger.md](branch-review-ledger.md) — reviewed branch/SHA ledger; read with `npm run ledger:lookup` (live + `docs/archive/branch-review-ledger-*.md`), write with `npm run ledger:append`, rotate older rows with `npm run ledger:rotate`

## Plans and workstreams (living)

Expand Down
1,214 changes: 1,214 additions & 0 deletions docs/archive/branch-review-ledger-2026-q3.md

Large diffs are not rendered by default.

1,221 changes: 12 additions & 1,209 deletions docs/branch-review-ledger.md

Large diffs are not rendered by default.

13 changes: 8 additions & 5 deletions docs/maturity-backlog-workorders.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,11 +210,14 @@ structural change, not a single mixed PR.
- **Files:** `scripts/check-gate-manifest.mjs`, `package.json`, `.github/workflows/ci.yml`.
- **Verification:** the self-test passes (20 gates) and fails if the two lists diverge.

### L4 · Rotate the branch-review ledger — `OPEN`

- **Outcome:** `docs/branch-review-ledger.md` (361 KB, append-only) stays navigable.
- **Approach:** archive entries older than a quarter into `docs/archive/branch-review-ledger-<q>.md`;
keep the live ledger to the current quarter.
### L4 · Rotate the branch-review ledger — `DONE`

- **Outcome:** `docs/branch-review-ledger.md` stays navigable; historical rows live under
`docs/archive/branch-review-ledger-<yyyy-qN>.md`.
- **Approach:** `npm run ledger:rotate` moves rows older than a cutoff (default: current
calendar-quarter start); `ledger:lookup` / `sweep:branch-ledger` read live + archives;
`check:branch-review-ledger` validates archive structure. Bootstrap on 2026-07-30 used
`--before 2026-07-29` because the whole history was one month old.
- **Risk:** low.

### L5 · Documentation quick-wins — `DONE` / `SATISFIED`
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
"check:outstanding-issues": "node scripts/check-outstanding-issues.mjs --self-test && node scripts/check-outstanding-issues.mjs",
"ledger:lookup": "node scripts/branch-review-ledger.mjs lookup",
"ledger:append": "node scripts/branch-review-ledger.mjs append",
"ledger:rotate": "node scripts/branch-review-ledger.mjs rotate",
"check:pr-policy": "node scripts/pr-policy.mjs --self-test && node scripts/check-pr-policy-workflow.mjs",
"check:env-parity": "node scripts/check-env-parity.mjs",
"sweep:branch-ledger": "node scripts/sweep-branch-ledger.mjs",
Expand Down
218 changes: 208 additions & 10 deletions scripts/branch-review-ledger.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,151 @@
*
* node scripts/branch-review-ledger.mjs lookup <branch-or-ref> [--head <sha>] [--scope <text>] [--json]
* node scripts/branch-review-ledger.mjs append --ref <x> --head <sha> --scope <s> --outcome <o> --checks <c>
* node scripts/branch-review-ledger.mjs rotate [--before YYYY-MM-DD] [--dry-run]
* node scripts/branch-review-ledger.mjs --self-test
*
* `lookup` answers the only question the ledger is for: has this exact ref at this exact
* HEAD already been reviewed for this scope? `append` writes one valid row, in UTF-8,
* with the cells escaped and the HEAD resolved to a full SHA.
* HEAD already been reviewed for this scope? It reads the live ledger plus any
* `docs/archive/branch-review-ledger-*.md` files. `append` writes one valid row to the
* live file only. `rotate` moves dated records older than a cutoff into a quarterly
* archive file so the live table stays navigable (maturity L4).
*
* Read-only except for `append`, which only ever adds one line at the end of the file.
* Never calls a provider or a network service; `git rev-parse` is local.
* Read-only except for `append` / `rotate`. Never calls a provider or a network service;
* `git rev-parse` is local.
*/
import { execFileSync } from "node:child_process";
import { appendFileSync, readFileSync } from "node:fs";
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

const root = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
const LEDGER_PATH = "docs/branch-review-ledger.md";
const ARCHIVE_DIR = "docs/archive";
const ARCHIVE_PREFIX = "branch-review-ledger-";

/** Cell separator: a literal `|` that was not escaped as `\|` inside prose. */
export const CELL_SPLIT = /(?<!\\)\|/;
export const RECORD_START = /^\| \d{4}-\d{2}-\d{2} \|/;
export const COLUMNS = ["date", "ref", "head", "scope", "outcome", "checks"];

/** UTC calendar-quarter start (YYYY-MM-DD) for a Date or YYYY-MM-DD string. */
export function calendarQuarterStart(value = new Date()) {
const date =
typeof value === "string"
? new Date(`${value.slice(0, 10)}T00:00:00.000Z`)
: new Date(Date.UTC(value.getUTCFullYear(), value.getUTCMonth(), value.getUTCDate()));
const quarterMonth = Math.floor(date.getUTCMonth() / 3) * 3;
return `${date.getUTCFullYear()}-${String(quarterMonth + 1).padStart(2, "0")}-01`;
}

/** Archive slug for a YYYY-MM-DD date, e.g. 2026-07-15 → `2026-q3`. */
export function archiveQuarterLabel(dateStr) {
const [year, month] = dateStr.slice(0, 10).split("-").map(Number);
const quarter = Math.floor((month - 1) / 3) + 1;
return `${year}-q${quarter}`;
}

/** Live ledger first, then sorted archive files. */
export function listLedgerPaths() {
const paths = [LEDGER_PATH];
const absArchive = path.join(root, ARCHIVE_DIR);
if (!existsSync(absArchive)) return paths;
for (const name of readdirSync(absArchive).sort()) {
if (name.startsWith(ARCHIVE_PREFIX) && name.endsWith(".md")) {
paths.push(path.join(ARCHIVE_DIR, name));
}
}
return paths;
}

/** Concatenate every ledger source for consumers that parse one markdown blob. */
export function readLedgerCorpus() {
return listLedgerPaths()
.map((relative) => readFileSync(path.join(root, relative), "utf8"))
.join("\n");
}

function splitLedgerMarkdown(markdown) {
const lines = markdown.split(/\r?\n/);
const preamble = [];
const records = [];
const trailing = [];
let seenRecord = false;
for (const line of lines) {
if (RECORD_START.test(line)) {
seenRecord = true;
records.push(line);
continue;
}
if (!seenRecord) preamble.push(line);
else trailing.push(line);
}
return { preamble, records, trailing };
}

function archivePreamble(label) {
return [
`# Branch Review Ledger Archive — ${label}`,
"",
"Historical review records rotated out of `docs/branch-review-ledger.md` so the live",
"table stays navigable. Do not hand-edit. `npm run ledger:lookup` reads this file",
"together with the live ledger. New reviews append only to the live file.",
"",
"| Date | Branch or ref | Reviewed HEAD | Scope | Outcome | Checks |",
"| --- | --- | --- | --- | --- | --- |",
].join("\n");
}

/**
* Move live dated records with date < before into quarterly archive files.
* Returns counts; does not delete unique review content.
*/
export function rotateLedgerMarkdown(liveMarkdown, { before, existingArchives = new Map() } = {}) {
if (!/^\d{4}-\d{2}-\d{2}$/.test(before)) {
throw new Error(`before must be YYYY-MM-DD (got ${JSON.stringify(before)})`);
}
const { preamble, records, trailing } = splitLedgerMarkdown(liveMarkdown);
const keep = [];
const byLabel = new Map();
for (const line of records) {
const date = line.slice(2, 12);
if (date < before) {
const label = archiveQuarterLabel(date);
if (!byLabel.has(label)) byLabel.set(label, []);
byLabel.get(label).push(line);
} else {
keep.push(line);
}
}

const archives = [];
for (const [label, moved] of [...byLabel.entries()].sort(([a], [b]) => a.localeCompare(b))) {
const relative = path.join(ARCHIVE_DIR, `${ARCHIVE_PREFIX}${label}.md`);
const prior = existingArchives.get(relative) ?? "";
const priorRecords = prior ? splitLedgerMarkdown(prior).records : [];
const seen = new Set(priorRecords);
const mergedRecords = [...priorRecords];
for (const line of moved) {
if (seen.has(line)) continue;
seen.add(line);
mergedRecords.push(line);
}
const body = prior.trim() ? splitLedgerMarkdown(prior).preamble.join("\n") : archivePreamble(label);
let markdown = `${body}\n${mergedRecords.join("\n")}\n`;
if (!markdown.endsWith("\n")) markdown += "\n";
archives.push({ path: relative, label, markdown, added: moved.length, total: mergedRecords.length });
}

let live = [...preamble, ...keep, ...trailing].join("\n");
if (!live.endsWith("\n")) live += "\n";
return {
liveMarkdown: live,
archives,
moved: records.length - keep.length,
kept: keep.length,
};
}

/** Parse the six cells of every review record. Non-record lines are ignored. */
export function parseLedgerRows(markdown) {
const rows = [];
Expand Down Expand Up @@ -195,7 +318,7 @@ function summarize(row) {
return ` ${row.date} ${row.head.replace(/`/g, "").slice(0, 12).padEnd(12)} ${row.scope}\n ${outcome}`;
}

function runLookup(markdown, argv) {
function runLookup(_liveMarkdown, argv) {
const { flags, positional } = parseFlags(argv);
const ref = positional[0] ?? flags.ref;
if (!ref || ref === true) {
Expand All @@ -205,16 +328,19 @@ function runLookup(markdown, argv) {
}
const head = flags.head && flags.head !== true ? resolveHead(flags.head) : resolveHead(ref);
const scope = flags.scope && flags.scope !== true ? flags.scope : "";
const sources = listLedgerPaths();
const markdown = readLedgerCorpus();
const { atHead, otherHead } = findReviews(markdown, { ref, head, scope });

if (flags.json) {
console.log(JSON.stringify({ ref, head, scope: scope || null, atHead, otherHead }, null, 2));
console.log(JSON.stringify({ ref, head, scope: scope || null, sources, atHead, otherHead }, null, 2));
return;
}

console.log(`ref: ${ref}`);
console.log(`head: ${head}${/^[0-9a-f]{40}$/.test(head) ? "" : " (not resolvable to a SHA here)"}`);
if (scope) console.log(`scope: exact ${JSON.stringify(scope)}`);
console.log(`files: ${sources.join(", ")}`);
console.log("");

if (atHead.length > 0) {
Expand Down Expand Up @@ -265,7 +391,8 @@ function runAppend(markdown, argv) {
normalizeScope(existing.scope) === normalizeScope(scopeValue) &&
[...appendTokens].some((token) => refTokens(existing.ref).has(token));

const rows = parseLedgerRows(markdown);
// Near-duplicate checks cover live + archives so a rotated row cannot be re-appended silently.
const rows = parseLedgerRows(readLedgerCorpus());
let scope = String(flags.scope);
const near = rows.filter((existing) => sameRefHeadScope(existing, scope));
if (near.length > 0) {
Expand Down Expand Up @@ -309,6 +436,52 @@ function runAppend(markdown, argv) {
console.log(`Appended review record to ${LEDGER_PATH}:\n${row}`);
}

function runRotate(liveMarkdown, argv) {
const { flags } = parseFlags(argv);
const before = flags.before && flags.before !== true ? String(flags.before) : calendarQuarterStart(new Date());
const dryRun = Boolean(flags["dry-run"]);
if (!/^\d{4}-\d{2}-\d{2}$/.test(before)) {
console.error(`refusing to rotate: --before must be YYYY-MM-DD (got ${JSON.stringify(before)})`);
process.exitCode = 1;
return;
}

const existingArchives = new Map();
for (const relative of listLedgerPaths().slice(1)) {
existingArchives.set(relative, readFileSync(path.join(root, relative), "utf8"));
}

let result;
try {
result = rotateLedgerMarkdown(liveMarkdown, { before, existingArchives });
} catch (error) {
console.error(`refusing to rotate: ${error.message}`);
process.exitCode = 1;
return;
}

if (result.moved === 0) {
console.log(`No live records dated before ${before}; kept ${result.kept}.`);
return;
}

console.log(
`${dryRun ? "Would move" : "Moving"} ${result.moved} record(s) dated before ${before}; ` +
`${result.kept} remain in the live ledger.`,
);
for (const archive of result.archives) {
console.log(` ${archive.path}: +${archive.added} (archive total ${archive.total})`);
}
if (dryRun) return;

mkdirSync(path.join(root, ARCHIVE_DIR), { recursive: true });
for (const archive of result.archives) {
writeFileSync(path.join(root, archive.path), archive.markdown, "utf8");
}
writeFileSync(path.join(root, LEDGER_PATH), result.liveMarkdown, "utf8");
console.log(`Rotated ${result.moved} record(s) out of ${LEDGER_PATH}.`);
}

function assert(condition, label) {
if (!condition) throw new Error(`self-test failed: ${label}`);
}
Expand Down Expand Up @@ -386,6 +559,25 @@ function selfTest() {
"documented n/a HEAD accepted",
);

assert(calendarQuarterStart("2026-07-30") === "2026-07-01", "Q3 start");
assert(calendarQuarterStart("2026-10-01") === "2026-10-01", "Q4 start");
assert(archiveQuarterLabel("2026-07-15") === "2026-q3", "July is q3");
const preamble = [
"# Ledger",
"",
"This file is append-only.",
"",
"| Date | Branch or ref | Reviewed HEAD | Scope | Outcome | Checks |",
"| --- | --- | --- | --- | --- | --- |",
].join("\n");
const oldRow = `| 2026-07-01 | old/x | ${"a".repeat(40)} | s | o | c |`;
const newRow = `| 2026-07-29 | new/x | ${"b".repeat(40)} | s | o | c |`;
const rotated = rotateLedgerMarkdown(`${preamble}\n${oldRow}\n${newRow}\n`, { before: "2026-07-29" });
assert(rotated.moved === 1 && rotated.kept === 1, "rotate moves only older rows");
assert(rotated.archives[0].path.endsWith("branch-review-ledger-2026-q3.md"), "archive path uses quarter label");
assert(rotated.liveMarkdown.includes(newRow) && !rotated.liveMarkdown.includes(oldRow), "live keeps new only");
assert(rotated.archives[0].markdown.includes(oldRow), "archive receives old row");

console.log("branch-review-ledger self-test passed.");
}

Expand All @@ -399,12 +591,18 @@ function main() {
const [command, ...rest] = argv;
if (command === "lookup") return runLookup(markdown, rest);
if (command === "append") return runAppend(markdown, rest);
console.error("usage: branch-review-ledger.mjs <lookup|append|--self-test> [...]");
if (command === "rotate") return runRotate(markdown, rest);
console.error("usage: branch-review-ledger.mjs <lookup|append|rotate|--self-test> [...]");
console.error(" lookup <branch-or-ref> [--head <sha>] [--scope <text>] [--json]");
console.error(
" append --ref <x> --head <sha> --scope <s> --outcome <o> --checks <c> [--date <YYYY-MM-DD>] [--supersede]",
);
console.error(" rotate [--before YYYY-MM-DD] [--dry-run]");
process.exitCode = 2;
}

if (process.argv[1]?.endsWith("branch-review-ledger.mjs")) main();
// Use a path-segment match: `check-branch-review-ledger.mjs`.endsWith("branch-review-ledger.mjs")
// is true and would wrongly run this CLI when the checker imports the module.
const isMain =
process.argv[1]?.endsWith("/branch-review-ledger.mjs") || process.argv[1]?.endsWith("\\branch-review-ledger.mjs");
if (isMain) main();
Loading
Loading