feat(queue): queue as data A2, migration, render and dispatch (#1508)
Filbert approved round 1 (f167b85e). Manifest 782bcb62, 21 files, plus the QUEUE.md markers and the TOOLS.md section. Lead decision 35. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -20,16 +20,16 @@ function brief(path = "docs/plans/brief-b.md", anchor = "Queue", blob = BLOB) {
|
||||
}
|
||||
|
||||
// A genesis document built the way store.mjs builds one.
|
||||
function genesisDoc(rows = MAP_ROWS) {
|
||||
function genesisDoc(rows = MAP_ROWS, op = "genesis-op-0001") {
|
||||
const map = parseMigrationMap(mapText(rows));
|
||||
const blobs = new Map(map.rows.filter((r) => r.brief).map((r) => [r.id, BLOB]));
|
||||
const t = at();
|
||||
const grows = genesisRows(map, blobs, "genesis-op-0001", t, "sage");
|
||||
const grows = genesisRows(map, blobs, op, t, "sage");
|
||||
const body = render(grows, 0);
|
||||
const entry = {
|
||||
rev: 0, op: "genesis-op-0001", verb: "genesis", args: { root: ROOT, branch: "refactor", map: "agents/sage/work/queue-migration-map.md" },
|
||||
rev: 0, op, verb: "genesis", args: { root: ROOT, branch: "refactor", map: "agents/sage/work/queue-migration-map.md" },
|
||||
by: "sage", at: t, semantics: 1,
|
||||
result: { mapBlob: BLOB2, highWater: map.highWater, retired: map.retired, rows: grows, legacyView: "legacy\n", receipt: genesisReceipt("genesis-op-0001", grows.length) },
|
||||
result: { mapBlob: BLOB2, highWater: map.highWater, retired: map.retired, rows: grows, legacyView: "legacy\n", receipt: genesisReceipt(op, grows.length) },
|
||||
viewSha: sha256(body),
|
||||
};
|
||||
return { version: 1, canonicalRoot: ROOT, revision: 0, rows: grows, log: [entry] };
|
||||
@@ -154,8 +154,8 @@ test("matrix: release, review round, changes requested and waiting-on-jason", ()
|
||||
refused(() => step(d, "move", mv(9, "in-review", { candidate: "x.sha256" }), "dewey", { candidate: CAND }), /only the claimant/);
|
||||
d = step(d, "move", mv(9, "in-review", { candidate: "x.sha256" }), "darkwing", { candidate: CAND });
|
||||
const rv = row(d, 9).review;
|
||||
assert.equal(rv.issue, 1508);
|
||||
assert.deepEqual(rv.rounds.map((r) => [r.n, r.request, r.candidate.digest]), [[1, "none", CAND.digest]]);
|
||||
assert.deepEqual(Object.keys(rv), ["rounds"]);
|
||||
assert.deepEqual(rv.rounds.map((r) => [r.n, r.issue, r.request, r.candidate.digest]), [[1, 1508, "none", CAND.digest]]);
|
||||
assert.match(d.log.at(-1).result.receipt, /in-progress→in-review round 1 on #1508$/);
|
||||
refused(() => step(d, "move", mv(9, "in-progress"), "dewey"), /claimed by darkwing/);
|
||||
d = step(d, "move", mv(9, "in-progress"), "darkwing");
|
||||
@@ -209,13 +209,15 @@ function row12Started(issues, { gateOwner = "filbert" } = {}) {
|
||||
|
||||
const review = (d, extra = {}) => step(d, "move", mv(12, "in-review", { candidate: "x", ...extra }), "darkwing", { candidate: CAND });
|
||||
const again = (d) => step(d, "move", mv(12, "in-progress"), "darkwing");
|
||||
// The issue the next round keeps: the last round's own (P2).
|
||||
const kept = (d) => row(d, 12).review.rounds.at(-1).issue;
|
||||
|
||||
test("review issue, lead decision 23: none refuses, one is used, several need --issue, later rounds keep it", () => {
|
||||
// No issues: refused before the round opens.
|
||||
refused(() => review(row12Started([])), /row 12 lists no issues; a privileged actor sets one before review/);
|
||||
// One issue: used without --issue; --issue may name it; any other refuses.
|
||||
const one = review(row12Started([1508]));
|
||||
assert.equal(row(one, 12).review.issue, 1508);
|
||||
assert.equal(kept(one), 1508);
|
||||
assert.match(one.log.at(-1).result.receipt, /round 1 on #1508$/);
|
||||
assert.equal(one.log.at(-1).result.issue, 1508);
|
||||
refused(() => review(row12Started([1508]), { issue: 1495 }), /--issue #1495 is not one of row 12's issues \(#1508\)/);
|
||||
@@ -225,25 +227,36 @@ test("review issue, lead decision 23: none refuses, one is used, several need --
|
||||
refused(() => review(several), /row 12 lists several issues \(#1495, #1508\); name the review's issue with --issue/);
|
||||
refused(() => review(several, { issue: 1600 }), /--issue #1600 is not one of row 12's issues \(#1495, #1508\)/);
|
||||
let d = review(several, { issue: 1508 });
|
||||
assert.equal(row(d, 12).review.issue, 1508);
|
||||
assert.equal(kept(d), 1508);
|
||||
// Later rounds keep the previous round's issue unless --issue names another.
|
||||
d = review(again(d));
|
||||
assert.deepEqual([row(d, 12).review.issue, row(d, 12).review.rounds.length], [1508, 2]);
|
||||
assert.deepEqual([kept(d), row(d, 12).review.rounds.length], [1508, 2]);
|
||||
d = review(again(d), { issue: 1495 });
|
||||
assert.deepEqual([row(d, 12).review.issue, row(d, 12).review.rounds.length], [1495, 3]);
|
||||
assert.deepEqual([kept(d), row(d, 12).review.rounds.length], [1495, 3]);
|
||||
// P2: each round keeps the issue it posted to; a later --issue rewrites none.
|
||||
assert.deepEqual(row(d, 12).review.rounds.map((r) => [r.n, r.issue]), [[1, 1508], [2, 1508], [3, 1495]]);
|
||||
d = review(again(d));
|
||||
assert.deepEqual(row(d, 12).review.rounds.map((r) => r.issue), [1508, 1508, 1495, 1495]);
|
||||
// A kept issue the row no longer lists refuses until --issue names one.
|
||||
d = step(again(d), "set", { id: 12, field: "issues", value: [1508, 1600] }, "sage");
|
||||
refused(() => review(d), /review issue #1495 is no longer one of its issues \(#1508, #1600\); name one with --issue/);
|
||||
assert.equal(row(review(d, { issue: 1600 }), 12).review.issue, 1600);
|
||||
assert.equal(kept(review(d, { issue: 1600 })), 1600);
|
||||
// --issue belongs to the review request only.
|
||||
refused(() => step(several, "move", mv(12, "blocked", { reason: "x", issue: 1508 }), "darkwing"), /--issue applies only to in-progress→in-review/);
|
||||
});
|
||||
|
||||
test("the row schema refuses a review with a null issue", () => {
|
||||
test("the row schema refuses a round with a null issue, and the A1 review shape (P2)", () => {
|
||||
const r = structuredClone(row(review(row12Started([1508])), 12));
|
||||
validateRow(r);
|
||||
r.review.issue = null;
|
||||
refused(() => validateRow(r), /review issue must be a positive integer/);
|
||||
const bad = structuredClone(r);
|
||||
bad.review.rounds[0].issue = null;
|
||||
refused(() => validateRow(bad), /review round issue must be a positive integer/);
|
||||
const noIssue = structuredClone(r);
|
||||
delete noIssue.review.rounds[0].issue;
|
||||
refused(() => validateRow(noIssue), /review round keys must be exactly n, op, by, at, issue, candidate, request/);
|
||||
const a1 = structuredClone(r);
|
||||
a1.review = { issue: 1508, rounds: a1.review.rounds };
|
||||
refused(() => validateRow(a1), /review keys must be exactly rounds in that order/);
|
||||
});
|
||||
|
||||
// R1: every state × target × actor class against 8.7's table, written from
|
||||
@@ -392,7 +405,7 @@ test("field edits: who may change what", () => {
|
||||
refused(() => step(d, "set", set(9, "closes", [1]), "sage", {}), /--reason|not one of/);
|
||||
refused(() => step(d, "set", set(9, "closes", [1], "r"), "sage"), /not one of row 9's issues/);
|
||||
const issues = step(narrowed, "set", set(9, "issues", [1508, 1510]), "sage");
|
||||
assert.deepEqual(row(issues, 9).closes, [1508, 1510]);
|
||||
assert.deepEqual(row(issues, 9).closes, []);
|
||||
refused(() => step(d, "set", set(9, "piece", "Queue as data"), "sage"), /already/);
|
||||
refused(() => step(d, "set", set(1, "piece", "x"), "jason"), /done rows never change/);
|
||||
const repin = step(d, "set", set(9, "brief", "docs/plans/brief-b.md#Queue"), "sage", { brief: brief(undefined, undefined, BLOB2) });
|
||||
@@ -401,6 +414,123 @@ test("field edits: who may change what", () => {
|
||||
refused(() => step(d, "set", set(9, "brief", "docs/plans/brief-b.md#Queue"), "darkwing", { brief: brief() }), /privileged/);
|
||||
});
|
||||
|
||||
test("set issues keeps a logged narrowing of closes (N10)", () => {
|
||||
const set = (id, field, value, reason = null) => ({ id, field, value, reason });
|
||||
const last = (d) => d.log.at(-1).result;
|
||||
// Not narrowed: closes follows the issues.
|
||||
const wide = step(genesisDoc(), "set", set(9, "issues", [1508, 1510]), "sage");
|
||||
assert.deepEqual(row(wide, 9).closes, [1508, 1510]);
|
||||
assert.deepEqual(last(wide).closes, { from: [1508], to: [1508, 1510], narrowed: false });
|
||||
assert.match(last(wide).receipt, /row 9 issues: #1508→#1508, #1510; closes: #1508→#1508, #1510$/);
|
||||
// Narrowed: closes keeps its intersection with the new issues.
|
||||
const narrowed = step(wide, "set", set(9, "closes", [1510], "1508 closes elsewhere"), "sage");
|
||||
const more = step(narrowed, "set", set(9, "issues", [1508, 1510, 1600]), "sage");
|
||||
assert.deepEqual(row(more, 9).closes, [1510]);
|
||||
assert.deepEqual(last(more).closes, { from: [1510], to: [1510], narrowed: true });
|
||||
assert.match(last(more).receipt, /; closes: #1510→#1510 \(kept narrowed\)$/);
|
||||
const fewer = step(more, "set", set(9, "issues", [1508, 1600]), "sage");
|
||||
assert.deepEqual(row(fewer, 9).closes, []);
|
||||
assert.match(last(fewer).receipt, /; closes: #1510→none \(kept narrowed\)$/);
|
||||
refused(() => step(narrowed, "set", set(9, "issues", [1508, 1510]), "sage"), /row 9 issues is already/);
|
||||
// The results replay, and other fields carry no closes.
|
||||
loadDoc(Buffer.from(serialize(fewer)));
|
||||
assert.equal("closes" in last(step(genesisDoc(), "set", set(11, "piece", "Renamed"), "sage")), false);
|
||||
});
|
||||
|
||||
// GFM's cell split: a backslash escapes the next character, and a pipe not
|
||||
// escaped separates cells. The outer pipes are dropped first.
|
||||
function gfmCells(line) {
|
||||
const inner = line.trim().replace(/^\|/, "");
|
||||
const cells = [""];
|
||||
for (let i = 0; i < inner.length; i++) {
|
||||
if (inner[i] === "\\" && i + 1 < inner.length) { cells[cells.length - 1] += inner[i] + inner[i + 1]; i++; continue; }
|
||||
if (inner[i] === "|") cells.push("");
|
||||
else cells[cells.length - 1] += inner[i];
|
||||
}
|
||||
if (cells.at(-1).trim() === "") cells.pop();
|
||||
return cells.length;
|
||||
}
|
||||
|
||||
test("text the table shows refuses \\ and <, everywhere it enters (N8)", () => {
|
||||
const d = genesisDoc();
|
||||
const bad = ["a\\| done | x", "back\\slash", "<b>bold</b>", "a < b"];
|
||||
const set = (field, value) => ({ id: 11, field, value, reason: null });
|
||||
for (const text of bad) {
|
||||
const re = /must not contain \\ or </;
|
||||
refused(() => canonArgs("add", { piece: text, gate: "g", brief: "docs/plans/brief-b.md#Queue" }), re);
|
||||
refused(() => canonArgs("add", { piece: "p", gate: text, brief: "docs/plans/brief-b.md#Queue" }), re);
|
||||
refused(() => canonArgs("add", { piece: "p", gate: "g", note: text, brief: "docs/plans/brief-b.md#Queue" }), re);
|
||||
refused(() => canonArgs("add", { piece: "p", gate: "g", brief: `docs/plans/brief-b.md#${text}` }), re);
|
||||
refused(() => canonArgs("move", mv(9, "blocked", { reason: text })), re);
|
||||
refused(() => canonArgs("note", { id: 9, text }), re);
|
||||
refused(() => canonArgs("set", set("piece", text)), re);
|
||||
refused(() => canonArgs("set", set("gate", text)), re);
|
||||
refused(() => canonArgs("set", set("brief", `docs/plans/brief-b.md#${text}`)), re);
|
||||
// A hand-built row or log entry is refused the same way.
|
||||
const r = row(d, 9);
|
||||
for (const [k, v] of [["piece", text], ["gate", text], ["note", text], ["brief", { ...r.brief, anchor: text }]]) {
|
||||
refused(() => validateRow({ ...r, [k]: v }), re);
|
||||
}
|
||||
refused(() => validateRow({ ...r, state: "blocked", previousState: "briefed", blockedReason: text }), re);
|
||||
refused(() => loadDoc(Buffer.from(serialize(genesisDoc(MAP_ROWS.map((m) => (m.id === 9 ? { ...m, piece: text } : m)))))), re);
|
||||
}
|
||||
});
|
||||
|
||||
test("every accepted text renders to nine cells on every row (N8)", () => {
|
||||
const base = row(genesisDoc(), 9);
|
||||
const chars = [];
|
||||
for (let c = 0x20; c < 0x7f; c++) chars.push(String.fromCharCode(c));
|
||||
chars.push("é", "§", "—", String.fromCharCode(0xa0), "\u{1f600}");
|
||||
const texts = ["a\\| done | x", "|", "||", "`|`", "a | b", "x\\", "\\|\\|", "<!-- x -->", "|"];
|
||||
for (const c of chars) texts.push(`x${c}y`, `x${c}|${c}y`, `x${c}${c}|`);
|
||||
const fields = [
|
||||
(t) => ({ piece: t }), (t) => ({ gate: t }), (t) => ({ note: t }), (t) => ({ brief: { ...base.brief, anchor: t } }),
|
||||
(t) => ({ state: "blocked", previousState: "briefed", blockedReason: t }),
|
||||
];
|
||||
const accepted = new Set();
|
||||
for (const t of texts) {
|
||||
for (const f of fields) {
|
||||
const r = { ...base, ...f(t) };
|
||||
try { validateRow(r); } catch (err) { if (err instanceof QueueError) continue; throw err; }
|
||||
accepted.add(t);
|
||||
const lines = render([r], 1).split("\n").filter((l) => l.startsWith("|"));
|
||||
for (const l of lines) assert.equal(gfmCells(l), 9, `${JSON.stringify(t)} renders ${JSON.stringify(l)}`);
|
||||
}
|
||||
}
|
||||
// Not vacuous: pipes and backticks pass; the two refused characters don't.
|
||||
for (const t of ["|", "a | b", "`|`", "x|y"]) assert.ok(accepted.has(t), t);
|
||||
for (const t of ["a\\| done | x", "x<y", "x\\y"]) assert.ok(!accepted.has(t), t);
|
||||
});
|
||||
|
||||
test("replay holds every op id to the caller's rule (N11)", () => {
|
||||
const load = (d) => loadDoc(Buffer.from(serialize(d)));
|
||||
const d = genesisDoc();
|
||||
// 72 characters replay; 73 to 80 no longer do.
|
||||
load(step(d, "note", { id: 9, text: "x" }, "darkwing", {}, "a".repeat(72)));
|
||||
for (const n of [73, 80]) {
|
||||
refused(() => load(step(d, "note", { id: 9, text: "x" }, "darkwing", {}, "a".repeat(n))), /log entry 1 op .* must match/);
|
||||
}
|
||||
// .outcome is reserved on every verb, accept-history included.
|
||||
refused(() => load(step(d, "accept-history", { reason: "r" }, "sage", { oldWitness: null }, "accept-history-1.outcome")), /log entry 1 op .* ends in \.outcome/);
|
||||
refused(() => load(step(d, "note", { id: 9, text: "x" }, "darkwing", {}, "note-row9.outcome")), /\.outcome, which is reserved/);
|
||||
// The genesis op is checked too.
|
||||
load(genesisDoc(MAP_ROWS, "genesis-ok-0001"));
|
||||
for (const op of ["short", "Genesis-op-0001", "genesis-op-0001.outcome", "g".repeat(73)]) {
|
||||
refused(() => load(genesisDoc(MAP_ROWS, op)), /log entry 0 op/);
|
||||
}
|
||||
// The ops a row carries, a claim and a review round, follow the same rule.
|
||||
const claimed = structuredClone(row(row9Started(), 9));
|
||||
const reviewed = structuredClone(row(review(row12Started([1508])), 12));
|
||||
validateRow(claimed);
|
||||
validateRow(reviewed);
|
||||
for (const op of ["a".repeat(73), "start-row9.outcome"]) {
|
||||
refused(() => validateRow({ ...claimed, claim: { ...claimed.claim, op } }), /claim op/);
|
||||
const r = structuredClone(reviewed);
|
||||
r.review.rounds[0].op = op;
|
||||
refused(() => validateRow(r), /review round op/);
|
||||
}
|
||||
});
|
||||
|
||||
test("note: owner, listed reviewer or privileged; empty clears", () => {
|
||||
const d = genesisDoc();
|
||||
const n = step(d, "note", { id: 9, text: "from the reviewer" }, "filbert");
|
||||
|
||||
Reference in New Issue
Block a user