feat(queue): refuse a row's owner as its reviewer at add, set reviewers and assign (row 31, #1508)

Built by rocko (Gate G run), approved by filbert in round 1 (#1508 comment
26629). Manifest 35199daf. Suites green on an index export.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-04 00:16:41 -05:00
co-authored by Claude Opus 5.5
parent 1649f5e2a3
commit bb2fa9704c
5 changed files with 62 additions and 5 deletions
+7 -2
View File
@@ -826,6 +826,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`);
next[field] = value;
// N10: closes follows the issues only if nobody narrowed it. A logged
// narrowing keeps its intersection with the new issues.
@@ -897,12 +898,15 @@ export function applyEntry(state, entry, resolved) {
const brief = checkBrief(resolved.brief);
if (brief.path !== parseBriefSpec(a.brief).path || brief.anchor !== parseBriefSpec(a.brief).anchor) throw refuse("resolved brief does not match the brief argument");
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`);
highWater = id;
const required = a.required ?? false;
const row = {
id, piece: a.piece, owner: a.owner ?? by, issues: a.issues, closes: a.issues, state: "queued", previousState: null,
id, piece: a.piece, owner, issues: a.issues, closes: a.issues, state: "queued", previousState: null,
required, requiredSince: required ? entry.at : null, gate: a.gate, gateOwner: a.gateOwner ?? "jason", brief,
after: a.after ?? [], reviewers: a.reviewers ?? [], review: null, claim: null, note: a.note, blockedReason: null,
after: a.after ?? [], reviewers, review: null, claim: null, note: a.note, blockedReason: null,
createdAt: entry.at, updatedAt: entry.at, updatedBy: by,
};
rows.set(id, row);
@@ -945,6 +949,7 @@ export function applyEntry(state, entry, resolved) {
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(`row ${row.id} owner ${a.seat} can't be a listed reviewer`);
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}`) };