fix(redaction): omit remaining configuration values from refusals (#1873)

This commit is contained in:
jason.woltje committed 2026-11-10 19:56:05 -06:00
1 parent ddf4f895cd
commit c8ff8ea128
34 files changed
+638 -198

No files matched your search

+58 -58
View File
@@ -99,18 +99,18 @@ export function checkCellText(v, what, opts) {
// An op id a caller chose: 8 to 72 characters, not ending in `.outcome`.
export function checkCallerOpId(v, what = "op id") {
if (typeof v !== "string" || !CALLER_OP_RE.test(v)) throw refuse(`${what} ${JSON.stringify(v)} must match ${CALLER_OP_RE.source} (8 to 72 characters)`);
if (v.endsWith(".outcome")) throw refuse(`${what} ${JSON.stringify(v)} ends in .outcome, which is reserved`);
if (typeof v !== "string" || !CALLER_OP_RE.test(v)) throw refuse(`${what} must match ${CALLER_OP_RE.source} (8 to 72 characters)`);
if (v.endsWith(".outcome")) throw refuse(`${what} ends in .outcome, which is reserved`);
return v;
}
export function checkName(v, what) {
if (typeof v !== "string" || !NAME_RE.test(v)) throw refuse(`${what} must be a seat name (lowercase letters, digits, hyphens): ${JSON.stringify(v)}`);
if (typeof v !== "string" || !NAME_RE.test(v)) throw refuse(`${what} must be a seat name (lowercase letters, digits, hyphens)`);
return v;
}
function checkId(v, what = "row id") {
if (!Number.isInteger(v) || v < 1 || v > 1_000_000) throw refuse(`${what} must be a positive integer: ${JSON.stringify(v)}`);
if (!Number.isInteger(v) || v < 1 || v > 1_000_000) throw refuse(`${what} must be a positive integer`);
return v;
}
@@ -148,14 +148,14 @@ function checkTime(v, what, { unknown = false, date = false } = {}) {
// as March 2).
const t = Date.parse(iso ? v : `${v}T00:00:00.000Z`);
if (Number.isFinite(t) && new Date(t).toISOString().slice(0, v.length) === v) return v;
throw refuse(`${what} is not a calendar ${iso ? "time" : "date"}: ${JSON.stringify(v)}`);
throw refuse(`${what} is not a calendar ${iso ? "time" : "date"}`);
}
throw refuse(`${what} must be an ISO time${date ? " or date" : ""}${unknown ? " or \"unknown\"" : ""}: ${JSON.stringify(v)}`);
throw refuse(`${what} must be an ISO time${date ? " or date" : ""}${unknown ? " or \"unknown\"" : ""}`);
}
export function checkRepoPath(v, what) {
if (typeof v !== "string" || !PATH_RE.test(v) || v.split("/").some((s) => s === "." || s === "..")) {
throw refuse(`${what} must be a repo-relative path: ${JSON.stringify(v)}`);
throw refuse(`${what} must be a repo-relative path`);
}
return v;
}
@@ -163,7 +163,7 @@ export function checkRepoPath(v, what) {
export function parseBriefSpec(spec) {
if (typeof spec !== "string") throw refuse("brief must be PATH#ANCHOR");
const i = spec.indexOf("#");
if (i < 0) throw refuse(`brief must be PATH#ANCHOR, naming the heading: ${JSON.stringify(spec)}`);
if (i < 0) throw refuse(`brief must be PATH#ANCHOR, naming the heading`);
const path = checkRepoPath(spec.slice(0, i), "brief path");
const anchor = checkCellText(spec.slice(i + 1), "brief anchor", { max: 200 });
return { path, anchor };
@@ -197,9 +197,9 @@ export function parseManifest(text) {
const lines = text.slice(0, -1).split("\n");
for (const line of lines) {
const m = /^([0-9a-f]{64}) {2}(.+)$/.exec(line);
if (!m) throw refuse(`candidate manifest line is not "<sha256> <path>": ${JSON.stringify(line)}`);
if (!m) throw refuse(`candidate manifest line is not "<sha256> <path>"`);
checkRepoPath(m[2], "candidate manifest path");
if (seen.has(m[2])) throw refuse(`candidate manifest names ${m[2]} twice`);
if (seen.has(m[2])) throw refuse(`candidate manifest names a path twice`);
seen.add(m[2]);
}
return lines.length;
@@ -222,7 +222,7 @@ export function attemptOf(row, op) {
}
function checkCommentId(v, what = "comment id") {
if (!Number.isSafeInteger(v) || v < 1) throw refuse(`${what} must be a positive integer: ${JSON.stringify(v)}`);
if (!Number.isSafeInteger(v) || v < 1) throw refuse(`${what} must be a positive integer`);
return v;
}
@@ -250,7 +250,7 @@ function checkAttempt(a) {
checkCallerOpId(a.op, "review attempt op");
checkName(a.by, "review attempt by");
checkTime(a.at, "review attempt at");
if (!ATTEMPT_STATES.includes(a.state)) throw refuse(`review attempt state ${JSON.stringify(a.state)} is not one of ${ATTEMPT_STATES.join(", ")}`);
if (!ATTEMPT_STATES.includes(a.state)) throw refuse(`review attempt state is not one of ${ATTEMPT_STATES.join(", ")}`);
if (a.comment !== null) checkCommentId(a.comment);
if ((a.state === "posted") !== (a.comment !== null)) throw refuse("a review attempt carries a comment id exactly when it is posted");
if (a.transport !== null) checkTransport(a.transport);
@@ -311,7 +311,7 @@ function checkRound(v, n) {
}
export function validateRow(row) {
keysExactly(row, ROW_KEYS, `row ${row?.id}`);
keysExactly(row, ROW_KEYS, `row id`);
const w = `row ${row.id}`;
checkId(row.id);
checkCellText(row.piece, `${w} piece`, { max: 300 });
@@ -319,7 +319,7 @@ export function validateRow(row) {
checkIssues(row.issues, `${w} issues`);
checkIssues(row.closes, `${w} closes`);
for (const n of row.closes) if (!row.issues.includes(n)) throw refuse(`${w} closes #${n}, which is not in its issues`);
if (!STATES.includes(row.state)) throw refuse(`${w} state ${JSON.stringify(row.state)} is not a state`);
if (!STATES.includes(row.state)) throw refuse(`${w} state is not a state (expected one of ${STATES.join(", ")})`);
if (row.state === "blocked") {
if (!NON_TERMINAL.has(row.previousState)) throw refuse(`${w} is blocked and needs the non-terminal state it returns to`);
checkCellText(row.blockedReason, `${w} blockedReason`);
@@ -346,7 +346,7 @@ export function validateRow(row) {
}
if (row.claim !== null) {
keysExactly(row.claim, ["seat", "op"], `${w} claim`);
if (row.claim.seat !== row.owner) throw refuse(`${w} claim seat ${row.claim.seat} is not the owner ${row.owner}`);
if (row.claim.seat !== row.owner) throw refuse(`${w} claim seat is not the owner`);
checkCallerOpId(row.claim.op, `${w} claim op`);
const s = row.state === "blocked" ? row.previousState : row.state;
if (!CLAIM_KEPT.has(s)) throw refuse(`${w} is claimed but ${row.state}`);
@@ -398,7 +398,7 @@ function nullable(v, fn) {
function sortedUnique(list, cmp) {
const out = [...list].sort(cmp);
for (let i = 1; i < out.length; i++) if (cmp(out[i], out[i - 1]) === 0) throw refuse(`repeated value ${JSON.stringify(out[i])}`);
for (let i = 1; i < out.length; i++) if (cmp(out[i], out[i - 1]) === 0) throw refuse(`repeated value in list`);
return out;
}
@@ -463,7 +463,7 @@ export function canonArgs(verb, a) {
};
}
case "move":
if (!STATES.includes(a.to)) throw refuse(`unknown state ${JSON.stringify(a.to)}; states: ${STATES.join(", ")}`);
if (!STATES.includes(a.to)) throw refuse(`unknown state; states: ${STATES.join(", ")}`);
return {
id: checkId(a.id),
to: a.to,
@@ -503,7 +503,7 @@ export function canonArgs(verb, a) {
if (a.verdict !== "approve" && a.verdict !== "changes") throw refuse("--verdict must be approve or changes");
return { id: checkId(a.id), verdict: a.verdict, comment: checkCommentId(a.comment), candidate: checkDigest(a.candidate, "candidate") };
default:
throw refuse(`unknown verb ${JSON.stringify(verb)}`);
throw refuse(`unknown verb; expected a queue operation`);
}
}
@@ -517,8 +517,8 @@ const isPriv = (by) => PRIVILEGED.has(by);
function ownerOrPriv(row, by, doing) {
if (by === row.owner || isPriv(by)) return;
const claimed = row.claim !== null ? `row ${row.id} is claimed by ${row.claim.seat}; ` : "";
throw refuse(`${claimed}only the owner (${row.owner}) or a privileged actor may ${doing} row ${row.id}`);
const claimed = row.claim !== null ? `row ${row.id} is claimed; ` : "";
throw refuse(`${claimed}only the owner or a privileged actor may ${doing} row ${row.id}`);
}
function requirePriv(by, doing) {
@@ -580,7 +580,7 @@ function unresolvedAttempts(row) {
function refuseUnresolved(row, doing) {
const open = unresolvedAttempts(row);
if (open.length) throw refuse(`row ${row.id} has an unresolved review request: ${open.join(", ")}; resolve or abandon it before ${doing}`);
if (open.length) throw refuse(`row ${row.id} has an unresolved review request; resolve or abandon it before ${doing}`);
}
// A comment round leaves in-review, for done or for Jason's gate, only on
@@ -589,7 +589,7 @@ function requireApprovals(row, cur, to) {
if (row.reviewers.length === 0) throw refuse(`row ${row.id} lists no reviewers now; a privileged actor sets them before it ${to === "done" ? "closes" : `moves to ${to}`}`);
const approved = new Set(cur.receipts.filter((r) => r.verdict === "approve").map((r) => r.reviewer));
const missing = row.reviewers.filter((s) => !approved.has(s));
if (missing.length) throw refuse(`row ${row.id} round ${cur.n} has no approval recorded by ${missing.join(", ")}`);
if (missing.length) throw refuse(`row ${row.id} round ${cur.n} has no approval recorded by every listed reviewer`);
}
// The request round an attempt belongs to, and the attempt.
@@ -599,7 +599,7 @@ function findAttempt(row, op) {
const i = r.attempts.findIndex((x) => x.op === op);
if (i >= 0) return { round: r, index: i, attempt: r.attempts[i] };
}
throw refuse(`row ${row.id} has no review request ${op}`);
throw refuse(`row ${row.id} has no review request matching the attempt op`);
}
// A copy of `row` with one attempt of one round replaced.
@@ -632,20 +632,20 @@ function outcomeState(attempt, t) {
return t.outcome === "posted" && t.comment === attempt.comment ? "posted" : "conflict";
}
if (attempt.state === "abandoned") return t.outcome === "posted" ? "conflict" : "abandoned";
throw refuse(`attempt ${attempt.op} is ${attempt.state}; it already has its outcome`);
throw refuse(`attempt is ${attempt.state}; it already has its outcome`);
}
function applyReview(rows, row, entry) {
const a = entry.args;
const by = entry.by;
if (!row.review && entry.verb !== "review-request" && entry.verb !== "review-record") throw refuse(`row ${row.id} has no review request ${a.attempt}`);
if (!row.review && entry.verb !== "review-request" && entry.verb !== "review-record") throw refuse(`row ${row.id} has no review request matching the attempt op`);
switch (entry.verb) {
case "review-request": {
ownerOrPriv(row, by, "request review of");
const cur = currentCommentRound(row, "review request");
const live = cur.attempts.filter((x) => x.state !== "failed" && x.state !== "abandoned");
refuseUnresolved(row, "a new request");
if (live.length) throw refuse(`row ${row.id} round ${cur.n} already has a posted request (${live[0].op}, comment ${live[0].comment})`);
if (live.length) throw refuse(`row ${row.id} round ${cur.n} already has a posted request`);
const rounds = row.review.rounds.map((r) => (r.n === cur.n ? { ...r, attempts: [...r.attempts, newAttempt(entry)] } : r));
return {
row: { ...row, review: { rounds } },
@@ -654,11 +654,11 @@ function applyReview(rows, row, entry) {
};
}
case "review-outcome": {
if (entry.op !== `${a.attempt}.outcome`) throw refuse(`an outcome's op is its attempt's op plus .outcome (${a.attempt}.outcome)`);
if (entry.op !== `${a.attempt}.outcome`) throw refuse(`an outcome's op is its attempt's op plus .outcome`);
const found = findAttempt(row, a.attempt);
const att = found.attempt;
if (by !== att.by) throw refuse(`only ${att.by}, who made request ${att.op}, records its outcome`);
if (att.transport !== null) throw refuse(`request ${att.op} already has its outcome`);
if (by !== att.by) throw refuse(`only the actor who made the request records its outcome`);
if (att.transport !== null) throw refuse(`request already has its outcome`);
const t = { outcome: a.outcome, status: a.status, comment: a.comment, detail: a.detail };
const state = outcomeState(att, t);
const comment = state === "posted" ? (att.comment ?? t.comment) : null;
@@ -678,7 +678,7 @@ function applyReview(rows, row, entry) {
else requirePriv(by, "abandon a review request");
const found = findAttempt(row, a.attempt);
const att = found.attempt;
if (!UNRESOLVED.has(att.state)) throw refuse(`request ${att.op} is ${att.state}; only a requesting, uncertain or conflict request can be ${resolve ? "resolved" : "abandoned"}`);
if (!UNRESOLVED.has(att.state)) throw refuse(`request is ${att.state}; only a requesting, uncertain or conflict request can be ${resolve ? "resolved" : "abandoned"}`);
const res = { verb: resolve ? "resolve" : "abandon", op: entry.op, by, at: entry.at, comment: resolve ? a.comment : null, reason: resolve ? null : a.reason };
const state = resolve ? "posted" : "abandoned";
const next = withAttempt(row, found, { ...att, state, comment: resolve ? a.comment : null, resolutions: [...att.resolutions, res] }, resolve ? {} : { duplicateRisk: true });
@@ -689,11 +689,11 @@ function applyReview(rows, row, entry) {
};
}
case "review-record": {
if (!row.reviewers.includes(by) || by === row.owner) throw refuse(`only a listed reviewer other than the owner may record a verdict on row ${row.id} (reviewers: ${fmt(row.reviewers)})`);
if (!row.reviewers.includes(by) || by === row.owner) throw refuse(`only a listed reviewer other than the owner may record a verdict on row ${row.id}`);
if (!row.review) throw refuse(`row ${row.id} has no review round`);
const cur = currentCommentRound(row, "review record");
if (a.candidate !== cur.candidate.digest) throw refuse(`candidate ${a.candidate} is not round ${cur.n}'s candidate ${cur.candidate.digest}`);
if (cur.receipts.some((r) => r.reviewer === by)) throw refuse(`${by} already recorded a verdict for row ${row.id} round ${cur.n}`);
if (a.candidate !== cur.candidate.digest) throw refuse(`candidate is not round ${cur.n}'s candidate`);
if (cur.receipts.some((r) => r.reviewer === by)) throw refuse(`reviewer already recorded a verdict for row ${row.id} round ${cur.n}`);
const rec = { reviewer: by, op: entry.op, at: entry.at, verdict: a.verdict, comment: a.comment, candidate: a.candidate };
const rounds = row.review.rounds.map((r) => (r.n === cur.n ? { ...r, receipts: [...r.receipts, rec] } : r));
return {
@@ -703,7 +703,7 @@ function applyReview(rows, row, entry) {
};
}
default:
throw refuse(`unknown verb ${entry.verb}`);
throw refuse(`unknown verb; expected a queue operation`);
}
}
@@ -759,14 +759,14 @@ function applyMove(rows, row, entry, resolved) {
} else if (from === "queued" && to === "briefed") {
requirePriv(by, "accept a brief (queued→briefed)");
} else if (from === "briefed" && to === "in-progress") {
if (by !== row.owner) throw refuse(`only the owner (${row.owner}) may start row ${row.id}`);
if (by !== row.owner) throw refuse(`only the owner may start row ${row.id}`);
const missing = afterSatisfied(rows, row);
if (missing.length) throw refuse(`row ${row.id} waits on ${missing.join(", ")}`);
next.claim = { seat: row.owner, op: entry.op };
} else if (from === "in-progress" && to === "briefed") {
throw refuse(`use \`queue release ${row.id}\` to give up a claim`);
} else if (from === "in-progress" && to === "in-review") {
if (row.claim === null || by !== row.claim.seat) throw refuse(`only the claimant (${row.claim?.seat ?? "nobody"}) may request review of row ${row.id}`);
if (row.claim === null || by !== row.claim.seat) throw refuse(`only the claimant may request review of row ${row.id}`);
if (candidate === null) throw refuse("in-progress→in-review needs --candidate <commit|manifest>");
revIssue = reviewIssue(row, issue);
cand = checkCandidate(resolved.candidate);
@@ -797,7 +797,7 @@ function applyMove(rows, row, entry, resolved) {
next.claim = null;
} else if (from === "in-review" && to === "done") {
if (row.gateOwner === "jason") throw refuse(`row ${row.id}'s gate is Jason's; it goes through waiting-on-jason`);
if (by !== row.gateOwner && !isPriv(by)) throw refuse(`only the gate owner (${row.gateOwner}) or a privileged actor may close row ${row.id}`);
if (by !== row.gateOwner && !isPriv(by)) throw refuse(`only the gate owner or a privileged actor may close row ${row.id}`);
refuseUnresolved(row, "closing it");
const cur = row.review?.rounds.at(-1);
if (cur?.request === "comment") {
@@ -808,7 +808,7 @@ function applyMove(rows, row, entry, resolved) {
const ev = parseReviewEvidence(evidence);
if (!cur) throw refuse(`row ${row.id} has no review round to cite`);
if (ev.round !== cur.n) throw refuse(`evidence names round ${ev.round}; row ${row.id} is in round ${cur.n}`);
if (ev.candidate !== cur.candidate.digest) throw refuse(`evidence candidate ${ev.candidate} is not round ${cur.n}'s candidate ${cur.candidate.digest}`);
if (ev.candidate !== cur.candidate.digest) throw refuse(`evidence candidate is not round ${cur.n}'s candidate`);
}
round = cur.n;
next.claim = null;
@@ -834,7 +834,7 @@ function applySet(rows, row, entry, resolved) {
switch (field) {
case "piece": case "gate": case "reviewers": case "issues":
requirePriv(by, `change ${field}`);
if (field === "reviewers" && value.includes(row.owner)) throw refuse(`row ${row.id} owner ${row.owner} can't be a listed reviewer`);
if (field === "reviewers" && value.includes(row.owner)) throw refuse(`row ${row.id} owner can't be a listed reviewer`);
next[field] = value;
// N10: closes follows the issues only if nobody narrowed it. A logged
// narrowing keeps its intersection with the new issues.
@@ -860,7 +860,7 @@ function applySet(rows, row, entry, resolved) {
requirePriv(by, "re-pin a brief");
next.brief = checkBrief(resolved.brief);
if (next.brief.path !== parseBriefSpec(value).path || next.brief.anchor !== parseBriefSpec(value).anchor) throw refuse("resolved brief does not match the brief argument");
if (row.brief !== null && next.brief.blob === row.brief.blob && sameJson(next.brief, row.brief)) throw refuse(`row ${row.id} brief is already pinned to ${row.brief.blob}`);
if (row.brief !== null && next.brief.blob === row.brief.blob && sameJson(next.brief, row.brief)) throw refuse(`row ${row.id} brief is already pinned to that blob`);
break;
case "required":
if (value) {
@@ -877,7 +877,7 @@ function applySet(rows, row, entry, resolved) {
throw refuse(`set cannot change ${field}`);
}
if (sameJson(next[key], row[key]) && (field !== "issues" || sameJson(next.closes, row.closes))) {
throw refuse(`row ${row.id} ${key} is already ${fmt(row[key])}`);
throw refuse(`row ${row.id} ${key} is already set to that value`);
}
const result = { row: row.id, field: key, from: row[key], to: next[key] };
if (field === "issues") result.closes = { from: row.closes, to: next.closes, narrowed: !sameJson(row.closes, row.issues) };
@@ -908,7 +908,7 @@ export function applyEntry(state, entry, resolved) {
const id = highWater + 1;
const owner = a.owner ?? by;
const reviewers = a.reviewers ?? [];
if (reviewers.includes(owner)) throw refuse(`row ${id} owner ${owner} can't be a listed reviewer`);
if (reviewers.includes(owner)) throw refuse(`row ${id} owner can't be a listed reviewer`);
highWater = id;
const required = a.required ?? false;
const row = {
@@ -947,7 +947,7 @@ export function applyEntry(state, entry, resolved) {
case "release": {
const row = getRow(rows, a.id);
if (row.state !== "in-progress") throw refuse(`row ${row.id} is ${row.state}; release applies to in-progress rows`);
if (!(row.claim && by === row.claim.seat) && !isPriv(by)) throw refuse(`only the claimant (${row.claim?.seat}) or a privileged actor may release row ${row.id}`);
if (!(row.claim && by === row.claim.seat) && !isPriv(by)) throw refuse(`only the claimant or a privileged actor may release row ${row.id}`);
rows.set(row.id, touch({ ...row, state: "briefed", claim: null }, entry));
result = { row: row.id, from: "in-progress", to: "briefed", receipt: receipt(entry, rev, `row ${row.id} in-progress→briefed`) };
break;
@@ -956,8 +956,8 @@ export function applyEntry(state, entry, resolved) {
const row = getRow(rows, a.id);
requirePriv(by, "assign a row");
if (row.state === "done") throw refuse(`row ${row.id} is done; done rows never change`);
if (row.owner === a.seat) throw refuse(`row ${row.id} is already owned by ${a.seat}`);
if (row.reviewers.includes(a.seat)) throw refuse(`can't assign row ${row.id} to ${a.seat}: ${a.seat} is one of its reviewers`);
if (row.owner === a.seat) throw refuse(`row ${row.id} is already owned by that seat`);
if (row.reviewers.includes(a.seat)) throw refuse(`can't assign row ${row.id}: seat is one of its reviewers`);
const claim = row.claim ? { seat: a.seat, op: entry.op } : null;
rows.set(row.id, touch({ ...row, owner: a.seat, claim }, entry));
result = { row: row.id, field: "owner", from: row.owner, to: a.seat, receipt: receipt(entry, rev, `row ${row.id} owner: ${row.owner}→${a.seat}`) };
@@ -966,7 +966,7 @@ export function applyEntry(state, entry, resolved) {
case "note": {
const row = getRow(rows, a.id);
if (row.state === "done" || row.state === "parked") throw refuse(`row ${row.id} is ${row.state}; notes are closed`);
if (by !== row.owner && !row.reviewers.includes(by) && !isPriv(by)) throw refuse(`only the owner (${row.owner}), a listed reviewer or a privileged actor may note row ${row.id}`);
if (by !== row.owner && !row.reviewers.includes(by) && !isPriv(by)) throw refuse(`only the owner, a listed reviewer or a privileged actor may note row ${row.id}`);
// On a blocked row the note is the reason (8.7: "update the reason
// with note"); elsewhere it is the row's note, and "" clears it.
const field = row.state === "blocked" ? "blockedReason" : "note";
@@ -1003,7 +1003,7 @@ export function applyEntry(state, entry, resolved) {
case "genesis":
throw refuse("genesis is only log[0]");
default:
throw refuse(`unknown verb ${entry.verb}`);
throw refuse(`unknown verb; expected a queue operation`);
}
validateRows([...rows.values()]);
return { state: { rows, highWater, retired: state.retired, revision: rev }, result };
@@ -1029,11 +1029,11 @@ export function parseMigrationMap(text) {
keysExactly(map, ["rows", "retired", "highWater"], "migration map");
if (!Array.isArray(map.rows)) throw refuse("migration map rows must be a list");
map.rows.forEach((r) => {
keysExactly(r, MAP_ROW_KEYS, `migration map row ${r?.id}`);
if (r.brief !== null) keysExactly(r.brief, ["path", "anchor"], `migration map row ${r.id} brief`);
keysExactly(r, MAP_ROW_KEYS, `migration map row`);
if (r.brief !== null) keysExactly(r.brief, ["path", "anchor"], `migration map row brief`);
// The rule add, set and assign apply. Here, not in checkGenesis, so a
// log written before the rule still replays.
if (Array.isArray(r.reviewers) && r.reviewers.includes(r.owner)) throw refuse(`migration map row ${r.id} owner ${r.owner} can't be a listed reviewer`);
if (Array.isArray(r.reviewers) && r.reviewers.includes(r.owner)) throw refuse(`migration map row owner can't be a listed reviewer`);
});
if (!Array.isArray(map.retired)) throw refuse("migration map retired must be a list");
map.retired.forEach((id) => checkId(id, "retired id"));
@@ -1102,8 +1102,8 @@ export function rowsArray(state) {
function checkEntryShape(e, i) {
keysExactly(e, ENTRY_KEYS, `log entry ${i}`);
if (e.rev !== i) throw refuse(`log entry ${i} has rev ${e.rev}`);
if (!VERBS.includes(e.verb)) throw refuse(`log entry ${i} verb ${JSON.stringify(e.verb)} is unknown`);
if (e.rev !== i) throw refuse(`log entry ${i} has rev that must equal its index`);
if (!VERBS.includes(e.verb)) throw refuse(`log entry ${i} verb is unknown`);
if (e.verb === "review-outcome") {
if (typeof e.op !== "string" || !LOG_OP_RE.test(e.op) || !isObj(e.args) || e.op !== `${e.args.attempt}.outcome`) throw refuse(`log entry ${i} op must be its attempt's op plus .outcome`);
} else {
@@ -1111,7 +1111,7 @@ function checkEntryShape(e, i) {
}
checkName(e.by, `log entry ${i} by`);
checkTime(e.at, `log entry ${i} at`);
if (!Number.isInteger(e.semantics) || e.semantics < 1 || e.semantics > SEMANTICS) throw refuse(`log entry ${i} semantics ${e.semantics} is not 1 to ${SEMANTICS}`);
if (!Number.isInteger(e.semantics) || e.semantics < 1 || e.semantics > SEMANTICS) throw refuse(`log entry ${i} semantics is not 1 to ${SEMANTICS}`);
if (REVIEW_VERBS.includes(e.verb) && e.semantics < 2) throw refuse(`log entry ${i} ${e.verb} needs semantics 2`);
if (typeof e.viewSha !== "string" || !SHA256_RE.test(e.viewSha)) throw refuse(`log entry ${i} viewSha is not a SHA-256`);
}
@@ -1129,16 +1129,16 @@ export function loadDoc(bytes) {
try { doc = JSON.parse(text); } catch { throw refuse(`queue.json is not JSON`); }
keysExactly(doc, DOC_KEYS, "queue.json");
if (serialize(doc) !== text) throw refuse("queue.json does not re-serialize byte for byte (a hand edit?)");
if (doc.version !== VERSION) throw refuse(`queue.json version ${doc.version} is not ${VERSION}`);
if (doc.version !== VERSION) throw refuse(`queue.json version is not ${VERSION}`);
if (typeof doc.canonicalRoot !== "string" || !doc.canonicalRoot.startsWith("/")) throw refuse("canonicalRoot must be an absolute path");
if (!Array.isArray(doc.rows) || !Array.isArray(doc.log) || doc.log.length === 0) throw refuse("queue.json needs rows and a log starting at genesis");
doc.log.forEach(checkEntryShape);
const ops = new Set();
for (const e of doc.log) {
if (ops.has(e.op)) throw refuse(`op id ${e.op} appears twice in the log`);
if (ops.has(e.op)) throw refuse(`op id appears twice in the log`);
ops.add(e.op);
}
if (doc.revision !== doc.log.length - 1) throw refuse(`revision ${doc.revision} is not the last rev ${doc.log.length - 1}`);
if (doc.revision !== doc.log.length - 1) throw refuse(`revision is not the last rev ${doc.log.length - 1}`);
const state = replay(doc.log);
if (!sameJson(rowsArray(state), doc.rows)) throw refuse("rows do not equal the replay of the log (a hand edit?)");
if (doc.canonicalRoot !== doc.log[0].args.root) throw refuse("canonicalRoot differs from the genesis root");
@@ -1158,10 +1158,10 @@ export function replay(log) {
try {
out = applyEntry(state, e, resolvedFromResult(e.verb, e.args, e.result));
} catch (err) {
if (err instanceof QueueError) throw refuse(`log entry ${i} (${e.op}) does not replay: ${err.message}`);
if (err instanceof QueueError) throw refuse(`log entry ${i} does not replay: ${err.message}`);
throw err;
}
if (!sameJson(out.result, e.result)) throw refuse(`log entry ${i} (${e.op}) result differs from its replay`);
if (!sameJson(out.result, e.result)) throw refuse(`log entry ${i} result differs from its replay`);
state = out.state;
if (e.viewSha !== sha256(render(rowsArray(state), i))) throw refuse(`log entry ${i} viewSha is not the render of its rows`);
}