From bc12f2bb12f347b9ff9bb9ab913451263d0d55ee Mon Sep 17 00:00:00 2001 From: Heimdall Date: Tue, 10 Nov 2026 18:27:25 +0000 Subject: [PATCH 01/26] deploy plan: restore reads a postgres-readable copy of the root-only dump postgres cannot read the root-owned 0600 dump. Precondition 2 now lists as root, restores from a 0400 copy in a 0700 postgres-owned scratch dir, shreds it and reads back 'gone'; Rollback step 5 does the same. Co-Authored-By: Claude Sonnet 5.5 --- docs/deploy-main-to-live.md | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/docs/deploy-main-to-live.md b/docs/deploy-main-to-live.md index dfe6d1b..88997ec 100644 --- a/docs/deploy-main-to-live.md +++ b/docs/deploy-main-to-live.md @@ -75,12 +75,22 @@ All must hold before step 1. A missing one means the window is not used. 1. **Root window.** Someone with root on Yggdrasil is present for the whole deploy and the 30 minutes after (installing the unit, reading `journalctl`, restoring). The swarm brake is off for Bifrost, or the operator has said so. -2. **Verified database backup.** `pg_dump -Fc` as in the runbook section 8, taken inside the window, then verified: - `pg_restore --list | head` succeeds, and the dump restores into a scratch database, where the member count - equals the live count. The `bifrost` database role has no CREATEDB, so create and drop the scratch database as the - superuser: `sudo -n -u postgres createdb -O bifrost bifrost_rehearsal`, then - `sudo -n -u postgres pg_restore --no-owner --role=bifrost -d bifrost_rehearsal `, and at the end - `sudo -n -u postgres dropdb bifrost_rehearsal`. An unrestored dump is not a backup. +2. **Verified database backup.** `pg_dump -Fc` as in the runbook section 8, taken inside the window to + `/srv/bifrost/backups/bifrost-pre--.dump`, **root-owned, mode 0600**, then verified. The `postgres` user + cannot read that file, so each check says who runs it: + - `sudo -n pg_restore --list | head` succeeds, **as root** (it reads the file and touches no database). + - The restore reads a **postgres-readable copy**, never the real dump: with `S=/var/tmp/bifrost-restoretest-`, root runs + `sudo -n install -d -m 0700 -o postgres -g postgres $S` and + `sudo -n install -m 0400 -o postgres -g postgres $S/restore.dump`. The `bifrost` database role has no + CREATEDB, so create and drop the scratch database as the superuser: + `sudo -n -u postgres createdb -O bifrost bifrost_restoretest_`, then + `sudo -n -u postgres pg_restore --no-owner --role=bifrost -d bifrost_restoretest_ $S/restore.dump`. The scratch + member count must equal the live count. + - Cleanup, on success and on every stop: `sudo -n -u postgres dropdb bifrost_restoretest_`, then + `sudo -n sh -c "shred -u $S/restore.dump && rmdir $S"`; readback `test ! -e $S && echo gone` prints `gone`. The real + dump stays root-only and is kept. + + An unrestored dump is not a backup. 3. **Env, secrets and key-ring backup.** The runbook's `bifrost-env-*.tar`, plus the live ring **`/srv/bifrost/.aspnet/DataProtection-Keys`** (read from the host 2026-11-09: `DataProtection__KeysPath` in `bifrost.env` names it; three key files at that reading, modes 600, owner `bifrost`, dated 2026-04-22, 2026-07-22 and @@ -217,7 +227,9 @@ Rollback signs members out again (the old release reads the old cookie names); s 3. Re-run the "before" checks from step 1: revision, `active`, counts equal. 4. The two new migrations stay applied. They add tables the old release ignores, as rehearsed in Precondition 4. 5. Only if the schema or data is damaged (not merely new): the runbook section 8 restore, from the verified dump. This is - the last resort and needs the same root window. + the last resort and needs the same root window. The `postgres` superuser cannot read the root-owned 0600 dump, so make a + fresh postgres-readable copy as in Precondition 2 (`$S/restore.dump`, 0400, postgres-owned, new 0700 directory), run + `pg_restore` from it, then `shred -u` the copy and `rmdir $S`; readback `test ! -e $S`. The dump itself stays root-only. 6. Leave the failed release directory for diagnosis; do not delete it in the window. ## What this plan does not do From 752cd66686338654764d2d9c166db066e987e771 Mon Sep 17 00:00:00 2001 From: Heimdall Date: Tue, 10 Nov 2026 18:28:31 +0000 Subject: [PATCH 02/26] deploy plan: restore reads a postgres-readable copy of the root-only dump (#38) --- docs/deploy-main-to-live.md | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/docs/deploy-main-to-live.md b/docs/deploy-main-to-live.md index dfe6d1b..88997ec 100644 --- a/docs/deploy-main-to-live.md +++ b/docs/deploy-main-to-live.md @@ -75,12 +75,22 @@ All must hold before step 1. A missing one means the window is not used. 1. **Root window.** Someone with root on Yggdrasil is present for the whole deploy and the 30 minutes after (installing the unit, reading `journalctl`, restoring). The swarm brake is off for Bifrost, or the operator has said so. -2. **Verified database backup.** `pg_dump -Fc` as in the runbook section 8, taken inside the window, then verified: - `pg_restore --list | head` succeeds, and the dump restores into a scratch database, where the member count - equals the live count. The `bifrost` database role has no CREATEDB, so create and drop the scratch database as the - superuser: `sudo -n -u postgres createdb -O bifrost bifrost_rehearsal`, then - `sudo -n -u postgres pg_restore --no-owner --role=bifrost -d bifrost_rehearsal `, and at the end - `sudo -n -u postgres dropdb bifrost_rehearsal`. An unrestored dump is not a backup. +2. **Verified database backup.** `pg_dump -Fc` as in the runbook section 8, taken inside the window to + `/srv/bifrost/backups/bifrost-pre--.dump`, **root-owned, mode 0600**, then verified. The `postgres` user + cannot read that file, so each check says who runs it: + - `sudo -n pg_restore --list | head` succeeds, **as root** (it reads the file and touches no database). + - The restore reads a **postgres-readable copy**, never the real dump: with `S=/var/tmp/bifrost-restoretest-`, root runs + `sudo -n install -d -m 0700 -o postgres -g postgres $S` and + `sudo -n install -m 0400 -o postgres -g postgres $S/restore.dump`. The `bifrost` database role has no + CREATEDB, so create and drop the scratch database as the superuser: + `sudo -n -u postgres createdb -O bifrost bifrost_restoretest_`, then + `sudo -n -u postgres pg_restore --no-owner --role=bifrost -d bifrost_restoretest_ $S/restore.dump`. The scratch + member count must equal the live count. + - Cleanup, on success and on every stop: `sudo -n -u postgres dropdb bifrost_restoretest_`, then + `sudo -n sh -c "shred -u $S/restore.dump && rmdir $S"`; readback `test ! -e $S && echo gone` prints `gone`. The real + dump stays root-only and is kept. + + An unrestored dump is not a backup. 3. **Env, secrets and key-ring backup.** The runbook's `bifrost-env-*.tar`, plus the live ring **`/srv/bifrost/.aspnet/DataProtection-Keys`** (read from the host 2026-11-09: `DataProtection__KeysPath` in `bifrost.env` names it; three key files at that reading, modes 600, owner `bifrost`, dated 2026-04-22, 2026-07-22 and @@ -217,7 +227,9 @@ Rollback signs members out again (the old release reads the old cookie names); s 3. Re-run the "before" checks from step 1: revision, `active`, counts equal. 4. The two new migrations stay applied. They add tables the old release ignores, as rehearsed in Precondition 4. 5. Only if the schema or data is damaged (not merely new): the runbook section 8 restore, from the verified dump. This is - the last resort and needs the same root window. + the last resort and needs the same root window. The `postgres` superuser cannot read the root-owned 0600 dump, so make a + fresh postgres-readable copy as in Precondition 2 (`$S/restore.dump`, 0400, postgres-owned, new 0700 directory), run + `pg_restore` from it, then `shred -u` the copy and `rmdir $S`; readback `test ! -e $S`. The dump itself stays root-only. 6. Leave the failed release directory for diagnosis; do not delete it in the window. ## What this plan does not do From 73c6f5044065a95731942da2cccae12fed8fd94d Mon Sep 17 00:00:00 2001 From: Heimdall Date: Tue, 10 Nov 2026 18:35:51 +0000 Subject: [PATCH 03/26] persona mouth: open a direct room for a Persona; answer unknown kinds loudly (#39) --- ...at_open_direct_receipt.v1.declined.msgpack | 1 + ...at_open_direct_receipt.v1.existing.msgpack | 1 + ...chat_open_direct_receipt.v1.opened.msgpack | 1 + ...ersona.chat_open_direct_request.v1.msgpack | 1 + ...rsona.chat_refusal.v1.unknown-kind.msgpack | 1 + .../persona-matrix-mouth-open-direct.test.mjs | 344 ++++++++++++++++++ tests/persona-matrix-mouth-runtime.test.mjs | 83 +++++ tools/persona-chat-documents.mjs | 41 ++- tools/persona-matrix-mouth.mjs | 123 ++++++- 9 files changed, 589 insertions(+), 7 deletions(-) create mode 100644 tests/fixtures/persona-chat-v4/gamecult.persona.chat_open_direct_receipt.v1.declined.msgpack create mode 100644 tests/fixtures/persona-chat-v4/gamecult.persona.chat_open_direct_receipt.v1.existing.msgpack create mode 100644 tests/fixtures/persona-chat-v4/gamecult.persona.chat_open_direct_receipt.v1.opened.msgpack create mode 100644 tests/fixtures/persona-chat-v4/gamecult.persona.chat_open_direct_request.v1.msgpack create mode 100644 tests/fixtures/persona-chat-v4/gamecult.persona.chat_refusal.v1.unknown-kind.msgpack create mode 100644 tests/persona-matrix-mouth-open-direct.test.mjs diff --git a/tests/fixtures/persona-chat-v4/gamecult.persona.chat_open_direct_receipt.v1.declined.msgpack b/tests/fixtures/persona-chat-v4/gamecult.persona.chat_open_direct_receipt.v1.declined.msgpack new file mode 100644 index 0000000..46b2220 --- /dev/null +++ b/tests/fixtures/persona-chat-v4/gamecult.persona.chat_open_direct_receipt.v1.declined.msgpack @@ -0,0 +1 @@ +”¥req-3¨DeclinedÀÀ \ No newline at end of file diff --git a/tests/fixtures/persona-chat-v4/gamecult.persona.chat_open_direct_receipt.v1.existing.msgpack b/tests/fixtures/persona-chat-v4/gamecult.persona.chat_open_direct_receipt.v1.existing.msgpack new file mode 100644 index 0000000..2a6cf01 --- /dev/null +++ b/tests/fixtures/persona-chat-v4/gamecult.persona.chat_open_direct_receipt.v1.existing.msgpack @@ -0,0 +1 @@ +”¥req-2¨Existing±!dm1:gamecult.org¦$line2 \ No newline at end of file diff --git a/tests/fixtures/persona-chat-v4/gamecult.persona.chat_open_direct_receipt.v1.opened.msgpack b/tests/fixtures/persona-chat-v4/gamecult.persona.chat_open_direct_receipt.v1.opened.msgpack new file mode 100644 index 0000000..d8dcd38 --- /dev/null +++ b/tests/fixtures/persona-chat-v4/gamecult.persona.chat_open_direct_receipt.v1.opened.msgpack @@ -0,0 +1 @@ +”¥req-1¦Opened±!dm1:gamecult.org¦$line1 \ No newline at end of file diff --git a/tests/fixtures/persona-chat-v4/gamecult.persona.chat_open_direct_request.v1.msgpack b/tests/fixtures/persona-chat-v4/gamecult.persona.chat_open_direct_request.v1.msgpack new file mode 100644 index 0000000..fb97ef6 --- /dev/null +++ b/tests/fixtures/persona-chat-v4/gamecult.persona.chat_open_direct_request.v1.msgpack @@ -0,0 +1 @@ +”¥req-1¦eureka²@meta:gamecult.orgªhello Meta \ No newline at end of file diff --git a/tests/fixtures/persona-chat-v4/gamecult.persona.chat_refusal.v1.unknown-kind.msgpack b/tests/fixtures/persona-chat-v4/gamecult.persona.chat_refusal.v1.unknown-kind.msgpack new file mode 100644 index 0000000..41b7ade --- /dev/null +++ b/tests/fixtures/persona-chat-v4/gamecult.persona.chat_refusal.v1.unknown-kind.msgpack @@ -0,0 +1 @@ +’¦eureka«UnknownKind \ No newline at end of file diff --git a/tests/persona-matrix-mouth-open-direct.test.mjs b/tests/persona-matrix-mouth-open-direct.test.mjs new file mode 100644 index 0000000..cf5ee95 --- /dev/null +++ b/tests/persona-matrix-mouth-open-direct.test.mjs @@ -0,0 +1,344 @@ +// A Persona asks her mouth for a direct room with one person. The homeserver here keeps room state the way Synapse does +// (create event with its content, one member event per account, encryption event when set), so the mouth's lookup, +// the consent that lives in the person's own membership, and the order of its writes are exercised, not mocked. +import test from "node:test"; +import assert from "node:assert/strict"; +import { createServer } from "node:http"; +import { createMouth } from "../tools/persona-matrix-mouth.mjs"; + +const HS = "gamecult.org"; +const ME = `@eureka:${HS}`; +const META = `@meta:${HS}`; +const PROJECT = "!project:gamecult.org"; +const ALIAS = `#swarm:${HS}`; + +async function homeserver({ people = [META, `@nova:${HS}`], now = () => 1000, failInvite: initialFailInvite = false } = {}) { + const config = { failInvite: initialFailInvite }; + const rooms = new Map(); // room id -> { events } + const stateReads = []; + const log = []; // every write, in order, as "METHOD path" + let counter = 0; + const direct = { value: null }; + const member = (room, user, membership) => { + const events = rooms.get(room).events.filter(e => !(e.type === "m.room.member" && e.state_key === user)); + events.push({ type: "m.room.member", state_key: user, origin_server_ts: now(), content: { membership } }); + rooms.get(room).events = events; + }; + const server = createServer((req, res) => { + let raw = ""; + req.on("data", c => (raw += c)); + req.on("end", () => { + const url = new URL(req.url, "http://x"); + const path = decodeURIComponent(url.pathname).replace("/_matrix/client/v3", ""); + const body = raw ? JSON.parse(raw) : null; + const reply = (status, json) => { res.writeHead(status, { "content-type": "application/json" }); res.end(JSON.stringify(json)); }; + const write = () => { if (!path.startsWith("/presence/") && !path.includes("/account_data/")) log.push(`${req.method} ${path}`); }; + if (path === "/account/whoami") return reply(200, { user_id: ME }); + if (path === "/joined_rooms") return reply(200, { joined_rooms: [PROJECT, ...[...rooms].filter(([, r]) => r.events.some(e => e.type === "m.room.member" && e.state_key === ME && e.content.membership === "join")).map(([id]) => id)] }); + if (path.includes("/account_data/m.direct")) { + if (req.method === "PUT") { direct.value = body; return reply(200, {}); } + return direct.value ? reply(200, direct.value) : reply(404, { errcode: "M_NOT_FOUND" }); + } + if (path.startsWith("/directory/room/")) return reply(200, { room_id: PROJECT }); + if (path.startsWith("/profile/")) return people.includes(path.slice("/profile/".length)) ? reply(200, { displayname: "x" }) : reply(404, { errcode: "M_NOT_FOUND" }); + const state = path.match(/^\/rooms\/(.+)\/state$/); + if (state) stateReads.push(state[1]); + if (state) return rooms.has(state[1]) ? reply(200, rooms.get(state[1]).events) : reply(403, {}); + if (req.method === "POST" && path === "/createRoom") { + write(); + const id = `!dm${++counter}:${HS}`; + rooms.set(id, { events: [{ type: "m.room.create", state_key: "", origin_server_ts: now() + counter, content: { creator: ME, ...body.creation_content } }] }); + if (body.preset !== "private_chat") return reply(400, {}); + member(id, ME, "join"); + rooms.get(id).body = body; + if (body.initial_state?.some(e => e.type === "m.room.encryption")) rooms.get(id).events.push({ type: "m.room.encryption", state_key: "", content: {} }); + return reply(200, { room_id: id }); + } + const invite = path.match(/^\/rooms\/(.+)\/invite$/); + if (invite) { write(); if (config.failInvite) return reply(403, { errcode: "M_FORBIDDEN" }); member(invite[1], body.user_id, "invite"); rooms.get(invite[1]).invited = body; return reply(200, {}); } + const put = path.match(/^\/rooms\/(.+)\/send\/m\.room\.message\/(.+)$/); + if (put) { + write(); + const r = rooms.get(put[1]); + r.sent ??= new Map(); + if (!r.sent.has(put[2])) r.sent.set(put[2], { id: `$e${counter}-${r.sent.size + 1}`, body }); + return reply(200, { event_id: r.sent.get(put[2]).id }); + } + reply(404, { errcode: "M_NOT_FOUND" }); + }); + }); + await new Promise(ok => server.listen(0, "127.0.0.1", ok)); + return { + url: `http://127.0.0.1:${server.address().port}`, rooms, log, direct, stateReads, + set failInvite(v) { config.failInvite = v; }, + seedDm(id, user, membership, { encrypted = false, created = 5, marker } = {}) { + rooms.set(id, { events: [{ type: "m.room.create", state_key: "", origin_server_ts: created, content: { creator: ME, ...(marker ? { "org.gamecult.open_request": marker } : {}) } }] }); + member(id, ME, "join"); member(id, user, membership); + if (encrypted) rooms.get(id).events.push({ type: "m.room.encryption", state_key: "", content: {} }); + }, + set: member, + writes: pattern => log.filter(l => pattern.test(l)), + close: () => { server.closeAllConnections(); return new Promise(ok => server.close(ok)); }, + }; +} + +const memoryState = () => { const box = { value: null }; return { load: async () => box.value, save: async v => { box.value = v; } }; }; +const mouthFor = (hs, extra = {}) => createMouth({ persona: "eureka", homeserver: hs.url, serverName: HS, rooms: [ALIAS], token: "tok", state: memoryState(), brake: async () => true, syncTimeoutMs: 1, ...extra }); +const ask = (m, requestId, user = META, text = "hello Meta, may I ask you something?", persona = "eureka") => m.openDirect({ requestId, persona, userId: user, text }); +const outcome = r => r.outcome; + +test("a new person: creates an unencrypted private direct room, invites with the line as the reason, says the line with the not-E2EE notice, and answers Opened", async () => { + const hs = await homeserver(); + try { + const m = mouthFor(hs); + const r = await ask(m, "req-1"); + assert.equal(r.outcome, "Opened"); + const [roomId] = [...hs.rooms.keys()]; + assert.equal(r.roomId, roomId); + const room = hs.rooms.get(roomId); + assert.equal(room.body.preset, "private_chat"); + assert.equal(room.body.is_direct, true); + assert.equal(room.body.creation_content["m.federate"], false); + assert.equal(room.body.creation_content["org.gamecult.open_request"], "req-1"); + assert.ok(!JSON.stringify(room.body).includes("m.room.encryption"), "the room is created without encryption"); + assert.ok(!room.events.some(e => e.type === "m.room.encryption")); + assert.deepEqual(room.invited, { user_id: META, reason: "hello Meta, may I ask you something?" }); + const line = room.sent.get("req-1"); + assert.equal(r.eventId, line.id); + assert.match(line.body.body, /not end-to-end encrypted/, "the first message says so"); + assert.ok(line.body.body.endsWith("hello Meta, may I ask you something?")); + assert.deepEqual(hs.direct.value, { [META]: [roomId] }, "the room id is stored in m.direct"); + assert.deepEqual(hs.log.map(l => l.split(" ")[0] + " " + l.split("/").pop()), ["POST createRoom", "POST invite", "PUT req-1"], "order of writes"); + } finally { await hs.close(); } +}); + +test("the invite reason is the line cut to 300 bytes", async () => { + const hs = await homeserver(); + try { + await ask(mouthFor(hs), "req-1", META, "x".repeat(900)); + assert.equal(Buffer.byteLength([...hs.rooms.values()][0].invited.reason), 300); + } finally { await hs.close(); } +}); + +test("only her own request is served, and only for a person: mismatch, empty text, a foreign server, herself, another Mind, an unknown account", async () => { + const hs = await homeserver(); + try { + const nova = `@nova:${HS}`; + const m = mouthFor(hs, { isServed: id => id === nova }); + assert.equal(outcome(await ask(m, "r1", META, "hi", "nova")), "PersonaMismatch"); + assert.equal(outcome(await ask(m, "r2", META, " ")), "EmptyText"); + assert.equal(outcome(await ask(m, "r3", "@meta:matrix.org")), "ForeignUser"); + assert.equal(outcome(await ask(m, "r3b", "@meta:gamecult.org.evil.example")), "ForeignUser"); + assert.equal(outcome(await ask(m, "r4", ME)), "NotAPerson"); + assert.equal(outcome(await ask(m, "r5", nova)), "NotAPerson"); + assert.equal(outcome(await ask(m, "r6", `@nobody:${HS}`)), "NoSuchUser"); + assert.deepEqual(hs.log, [], "no write for any refusal"); + assert.equal(hs.rooms.size, 0); + } finally { await hs.close(); } +}); + +test("a held brake refuses before any Matrix call", async () => { + const hs = await homeserver(); + try { + const m = mouthFor(hs, { brake: async () => false }); + assert.equal(outcome(await ask(m, "r1")), "Brake"); + assert.deepEqual(hs.log, []); + assert.equal(hs.rooms.size, 0); + } finally { await hs.close(); } +}); + +test("the brake is read fresh before createRoom, before the invite and before the line: flipping it at any point stops every later write", async () => { + for (let free = 1; free <= 5; free++) { + const hs = await homeserver(); + try { + let reads = 0; + const events = []; + const m = mouthFor(hs, { brake: async () => { const ok = ++reads <= free; events.push(ok ? "free" : "held"); return ok; } }); + const originalLog = hs.log; + const push = originalLog.push.bind(originalLog); + originalLog.push = (...x) => { events.push(`write ${x[0]}`); return push(...x); }; + await ask(m, "r1"); + let held = false; + for (const e of events) { + if (e === "held") held = true; + else if (held && e.startsWith("write")) assert.fail(`free=${free}: a write after the brake was held: ${e}`); + } + const writes = events.filter(e => e.startsWith("write")); + for (let i = 0; i < events.length; i++) if (events[i].startsWith("write")) assert.equal(events[i - 1], "free", `free=${free}: ${events[i]} was not immediately preceded by a released brake read`); + assert.equal(writes.length, Math.min(3, free - 1), `free=${free}: reads 2, 3 and 4 each gate exactly the createRoom, the invite and the line`); + } finally { await hs.close(); } + } +}); + +test("a second request for a person who accepted says the line in the same room and answers Existing, with no new room", async () => { + const hs = await homeserver(); + try { + const m = mouthFor(hs); + const first = await ask(m, "r1"); + hs.set(first.roomId, META, "join"); // Meta accepts + const again = await ask(m, "r2", META, "second line"); + assert.deepEqual([again.outcome, again.roomId], ["Existing", first.roomId]); + assert.equal(hs.rooms.size, 1); + const sent = hs.rooms.get(first.roomId).sent.get("r2"); + assert.equal(again.eventId, sent.id); + assert.equal(sent.body.body, "second line", "no notice again after the first message"); + } finally { await hs.close(); } +}); + +test("a repeat of the same request id after a crash resumes in the room it made: one room, one line, Opened again", async () => { + const hs = await homeserver(); + try { + const first = await ask(mouthFor(hs), "r1"); + const again = await ask(mouthFor(hs), "r1"); // a restarted process: no memory of the first + assert.deepEqual([again.outcome, again.roomId, again.eventId], ["Opened", first.roomId, first.eventId]); + assert.equal(hs.rooms.size, 1); + assert.equal(hs.writes(/createRoom/).length, 1); + assert.equal(hs.writes(/\/invite/).length, 1); + } finally { await hs.close(); } +}); + +test("an invite the person has not answered is InvitePending: nothing new is written", async () => { + const hs = await homeserver(); + try { + const m = mouthFor(hs); + await ask(m, "r1"); + const before = hs.log.length; + const again = await ask(m, "r2"); + assert.equal(again.outcome, "InvitePending"); + assert.equal(hs.log.length, before); + } finally { await hs.close(); } +}); + +test("a decline stands: leaving or banning is Declined, for a restarted mouth too, until the person invites her", async () => { + for (const gone of ["leave", "ban"]) { + const hs = await homeserver(); + try { + const first = await ask(mouthFor(hs), "r1"); + hs.set(first.roomId, META, gone); + const before = hs.log.length; + for (const m of [mouthFor(hs), mouthFor(hs)]) { + assert.equal(outcome(await ask(m, `again-${Math.random()}`)), "Declined", gone); + } + assert.equal(hs.log.length, before, "no room, invite or line"); + // the person writes first: she is invited into a new room and joins; now there is a live DM + hs.seedDm("!theirs:gamecult.org", META, "join", { created: 99999 }); + const answer = await ask(mouthFor(hs), "r-after"); + assert.deepEqual([answer.outcome, answer.roomId], ["Existing", "!theirs:gamecult.org"]); + } finally { await hs.close(); } + } +}); + +test("an invite the homeserver refuses leaves the room made; the retry invites into that same room instead of making another", async () => { + const hs = await homeserver({ failInvite: true }); + try { + assert.equal(outcome(await ask(mouthFor(hs), "r1")), "SendFailed"); + assert.equal(hs.rooms.size, 1); + hs.failInvite = false; + const retry = await ask(mouthFor(hs), "r2"); + assert.equal(retry.outcome, "Opened"); + assert.equal(hs.rooms.size, 1, "no second room"); + assert.equal(retry.roomId, [...hs.rooms.keys()][0]); + assert.equal(hs.rooms.get(retry.roomId).invited.user_id, META); + } finally { await hs.close(); } +}); + +test("one creation per person per day: a second creation the same UTC day is DailyLimit, the next UTC day it is allowed", async () => { + let t = Date.parse("2026-11-10T10:00:00Z"); + const hs = await homeserver({ now: () => t }); + try { + const m = mouthFor(hs, { now: () => t }); + assert.equal(outcome(await ask(m, "r1")), "Opened"); + hs.rooms.clear(); // she left the room: no DM with them is found now, only the day's record stands + assert.equal(outcome(await ask(m, "r2")), "DailyLimit"); + assert.equal(hs.writes(/createRoom/).length, 1); + t += 24 * 3600 * 1000; + assert.equal(outcome(await ask(m, "r3")), "Opened"); + assert.equal(outcome(await ask(m, "r4", `@nova:${HS}`)), "Opened", "the limit is per person"); + } finally { await hs.close(); } +}); + +test("an encrypted direct room is not used: the line is never sent into it and a new unencrypted room is made", async () => { + const hs = await homeserver(); + try { + hs.seedDm("!old:gamecult.org", META, "join", { encrypted: true }); + const r = await ask(mouthFor(hs), "r1"); + assert.equal(r.outcome, "Opened"); + assert.notEqual(r.roomId, "!old:gamecult.org"); + assert.equal(hs.rooms.get("!old:gamecult.org").sent, undefined); + } finally { await hs.close(); } +}); + +test("a homeserver failure answers SendFailed and writes nothing the brake or a refusal should have stopped", async () => { + const hs = await homeserver(); + try { + const m = mouthFor(hs, { homeserver: "http://127.0.0.1:9" }); + assert.equal(outcome(await ask(m, "r1")), "SendFailed"); + } finally { await hs.close(); } +}); + +test("two requests for one person at once make one room: opens for a person run one at a time", async () => { + const hs = await homeserver(); + try { + const m = mouthFor(hs); + const answers = await Promise.all([ask(m, "a"), ask(m, "b")]); + assert.equal(hs.writes(/createRoom/).length, 1); + assert.deepEqual(answers.map(outcome).sort(), ["InvitePending", "Opened"]); + const other = await Promise.all([ask(m, "c", `@nova:${HS}`), ask(m, "d", META)]); + assert.equal(other[0].outcome, "Opened", "another person is not held behind"); + } finally { await hs.close(); } +}); + +test("a decline stands in an encrypted room too, and a pending invite there is still pending: no new room, no line", async () => { + for (const [membership, expected] of [["leave", "Declined"], ["invite", "InvitePending"]]) { + const hs = await homeserver(); + try { + hs.seedDm("!enc:gamecult.org", META, membership, { encrypted: true }); + assert.equal(outcome(await ask(mouthFor(hs), "r1")), expected); + assert.deepEqual(hs.log, []); + } finally { await hs.close(); } + } +}); + +test("the not-E2EE notice reaches the person even when the first line was held back: a later line into the room this path made carries it once", async () => { + const hs = await homeserver(); + try { + let reads = 0; + const m = mouthFor(hs, { brake: async () => ++reads <= 3 }); // top, createRoom, invite: then the brake is held before the line + assert.equal(outcome(await ask(m, "r1")), "Brake"); + const roomId = [...hs.rooms.keys()][0]; + assert.equal(hs.rooms.get(roomId).sent, undefined, "the invite went out, the line did not"); + hs.set(roomId, META, "join"); + const answer = await ask(mouthFor(hs), "r2", META, "second"); + assert.deepEqual([answer.outcome, answer.roomId], ["Existing", roomId]); + assert.match(hs.rooms.get(roomId).sent.get("r2").body.body, /^This room is not end-to-end encrypted.*\n\nsecond$/s); + await ask(mouthFor(hs), "r3", META, "third"); // another process: the notice is repeated at worst once, never lost + const sameProcess = mouthFor(hs); + await ask(sameProcess, "r4", META, "fourth"); + await ask(sameProcess, "r5", META, "fifth"); + assert.equal(hs.rooms.get(roomId).sent.get("r5").body.body, "fifth", "once the notice went out in this process it is not repeated"); + } finally { await hs.close(); } +}); + +test("the lookup finds a DM only: a group room with the person is not one, and project rooms are never scanned", async () => { + const hs = await homeserver(); + try { + hs.seedDm("!group:gamecult.org", META, "join"); + hs.set("!group:gamecult.org", `@third:${HS}`, "join"); // three members + const r = await ask(mouthFor(hs), "r1"); + assert.equal(r.outcome, "Opened"); + assert.notEqual(r.roomId, "!group:gamecult.org"); + assert.equal(hs.rooms.get("!group:gamecult.org").sent, undefined); + assert.ok(!hs.stateReads.includes("!project:gamecult.org"), "a configured project room is not read as a DM candidate"); + } finally { await hs.close(); } +}); + +test("another Mind this process serves is not a person, even before that Mind's own mouth has started", async () => { + const hs = await homeserver(); + try { + const nova = `@nova:${HS}`; + let asked = null; + const m = mouthFor(hs, { isServed: async id => { asked = id; return id === nova; } }); + assert.equal(outcome(await ask(m, "r1", nova)), "NotAPerson"); + assert.equal(asked, nova); + } finally { await hs.close(); } +}); diff --git a/tests/persona-matrix-mouth-runtime.test.mjs b/tests/persona-matrix-mouth-runtime.test.mjs index d53de8e..098a422 100644 --- a/tests/persona-matrix-mouth-runtime.test.mjs +++ b/tests/persona-matrix-mouth-runtime.test.mjs @@ -438,3 +438,86 @@ test("over the RUDP endpoint: pending and an ack post no receipt, a read report try { client.close(); } catch { /* already closed */ } } }); + +// The open-direct pair (v4): the bytes Huginn pins in its crates/mind-body/tests/fixtures/persona-chat-v4/. +const v4Dir = resolve(root, "tests", "fixtures", "persona-chat-v4"); +const v4Samples = { + "gamecult.persona.chat_open_direct_request.v1": { tuple: docs.openDirectRequestTuple({ requestId: "req-1", persona: "eureka", userId: "@meta:gamecult.org", text: "hello Meta" }), parse: docs.parseOpenDirectRequest }, + "gamecult.persona.chat_open_direct_receipt.v1.opened": { tuple: docs.openDirectReceiptTuple({ requestId: "req-1", outcome: "Opened", roomId: "!dm1:gamecult.org", eventId: "$line1" }), parse: docs.parseOpenDirectReceipt }, + "gamecult.persona.chat_open_direct_receipt.v1.existing": { tuple: docs.openDirectReceiptTuple({ requestId: "req-2", outcome: "Existing", roomId: "!dm1:gamecult.org", eventId: "$line2" }), parse: docs.parseOpenDirectReceipt }, + "gamecult.persona.chat_open_direct_receipt.v1.declined": { tuple: docs.openDirectReceiptTuple({ requestId: "req-3", outcome: "Declined" }), parse: docs.parseOpenDirectReceipt }, + "gamecult.persona.chat_refusal.v1.unknown-kind": { tuple: docs.refusalTuple({ persona: "eureka", refusal: "UnknownKind" }), parse: docs.parseRefusal }, +}; +for (const [name, { tuple, parse }] of Object.entries(v4Samples)) { + test(`golden fixture: ${name} is byte-stable and parses back`, async () => { + const bytes = Buffer.from(msgpack.encode(tuple)); + const file = resolve(v4Dir, `${name}.msgpack`); + if (process.env.UPDATE_FIXTURES === "1") { await mkdir(v4Dir, { recursive: true }); await writeFile(file, bytes); } + assert.deepEqual(bytes, await readFile(file), "wire bytes changed: Huginn decodes these fixtures"); + assert.deepEqual(parse(msgpack.decode(bytes)), parse(tuple)); + }); +} + +test("the open-direct documents are strict: room and event ids exactly for Opened and Existing, a known outcome, a bounded line", () => { + for (const outcome of docs.directOutcomes) { + const placed = outcome === "Opened" || outcome === "Existing"; + assert.doesNotThrow(() => docs.openDirectReceiptTuple(placed ? { requestId: "r", outcome, roomId: "!a:b", eventId: "$e" } : { requestId: "r", outcome }), outcome); + assert.throws(() => docs.openDirectReceiptTuple(placed ? { requestId: "r", outcome } : { requestId: "r", outcome, roomId: "!a:b", eventId: "$e" }), /exactly for Opened and Existing/, outcome); + assert.throws(() => docs.parseOpenDirectReceipt(["r", outcome, placed ? "!a:b" : null, placed ? null : "$e"]), /exactly for/, `${outcome}: event id alone is half a placement`); + } + assert.throws(() => docs.openDirectReceiptTuple({ requestId: "r", outcome: "Invented" }), /unknown open-direct outcome/); + assert.throws(() => docs.parseOpenDirectRequest(["r", "eureka", "@a:b", "x".repeat(4001)]), /byte bound/); + assert.throws(() => docs.parseOpenDirectRequest(["r", "eureka", "", "x"]), /user_id/); + assert.throws(() => docs.parseOpenDirectRequest(["r", "eureka", "@a:b"]), /4-field/); + assert.throws(() => docs.sendReceiptTuple({ requestId: "r", refusal: "UnknownKind" }), /unknown refusal/, "UnknownKind exists only in the typed refusal document"); +}); + +test("the RUDP endpoint answers an open-direct request with a receipt for its Persona, refuses another Persona's and answers a request of an unknown kind loudly", async () => { + const opened = []; + const mouth = { openDirect: async request => { opened.push(request); return { requestId: request.requestId, outcome: "Opened", roomId: "!dm1:gamecult.org", eventId: "$line1" }; } }; + const key = "connection-key-for-test"; + const logged = []; + const server = await startChatEndpoint({ mouths: new Map([["eureka", mouth]]), runtime, port: 0, connectionKey: key, runtimeId: "mouth-test", log: line => logged.push(line) }); + const client = dgram.createSocket("udp4"); + await new Promise(ok => client.bind(0, "127.0.0.1", ok)); + const connection = new runtime.cultnet.CultNetRudpSocketTransportConnection({ + runtimeId: "huginn-test", socket: client, mode: "client", remoteHost: "127.0.0.1", remotePort: server.address.port, + connectionId: connectionIdFor(key), initialSequence: 1, resendDelayMs: 50, resendPollMs: 10, maxFragmentBytes: 1200, + }); + const replies = []; + const waiting = []; + connection.on("frame", value => { + replies.push(runtime.cultnet.parseCultNetMessage(msgpack.decode(value.payload), "cultnet.schema.v0").document); + waiting.splice(0).forEach(w => w()); + }); + const nextReply = () => new Promise((ok, fail) => { const t = setTimeout(() => fail(new Error(`no reply; the mouth logged: ${logged.join(" | ")}`)), 5000); waiting.push(() => { clearTimeout(t); ok(replies.at(-1)); }); }); + const put = (kind, recordKey, tuple, agent = "eureka") => connection.send("schema", msgpack.encode(runtime.cultnet.encodeCultNetMessageForWire( + docs.envelope({ kind, recordKey, payload: msgpack.encode(tuple), sourceRuntimeId: "huginn-test", sourceAgentId: agent, sourceRole: "persona-organ" }), "cultnet.schema.v0"))); + try { + connection.connect(); + for (let i = 0; i < 100 && !connection.connected; i++) await new Promise(r => setTimeout(r, 20)); + assert.ok(connection.connected); + let reply = nextReply(); + put(docs.kinds.openDirectRequest, "req-1", docs.openDirectRequestTuple({ requestId: "req-1", persona: "eureka", userId: "@meta:gamecult.org", text: "hello" })); + let document = await reply; + assert.equal(document.schemaId, docs.kinds.openDirectReceipt); + assert.deepEqual(docs.parseOpenDirectReceipt(msgpack.decode(document.payload)), { requestId: "req-1", outcome: "Opened", roomId: "!dm1:gamecult.org", eventId: "$line1" }); + assert.deepEqual(opened, [{ requestId: "req-1", persona: "eureka", userId: "@meta:gamecult.org", text: "hello" }]); + + reply = nextReply(); + put(docs.kinds.openDirectRequest, "req-2", docs.openDirectRequestTuple({ requestId: "req-2", persona: "other", userId: "@meta:gamecult.org", text: "hello" }), "other"); + document = await reply; + assert.deepEqual(docs.parseOpenDirectReceipt(msgpack.decode(document.payload)), { requestId: "req-2", outcome: "UnknownPersona", roomId: null, eventId: null }); + assert.equal(opened.length, 1, "nothing reached a mouth for the unlisted Persona"); + + reply = nextReply(); + put("gamecult.persona.chat_invented_request.v9", "req-3", ["x"]); + document = await reply; + assert.deepEqual([document.schemaId, docs.parseRefusal(msgpack.decode(document.payload))], [docs.kinds.refusal, { persona: "eureka", refusal: "UnknownKind" }], "an unknown kind is answered, typed"); + assert.ok(logged.some(l => /unknown kind: gamecult\.persona\.chat_invented_request\.v9/.test(l)), "and logged"); + } finally { + server.close(); + connection.close(); + try { client.close(); } catch { /* the connection already closed its socket */ } + } +}); diff --git a/tools/persona-chat-documents.mjs b/tools/persona-chat-documents.mjs index f20cad4..f5c6e67 100644 --- a/tools/persona-chat-documents.mjs +++ b/tools/persona-chat-documents.mjs @@ -16,6 +16,8 @@ export const kinds = Object.freeze({ sendRequestV2: "gamecult.persona.chat_send_request.v2", sendReceiptV2: "gamecult.persona.chat_send_receipt.v2", refusal: "gamecult.persona.chat_refusal.v1", + openDirectRequest: "gamecult.persona.chat_open_direct_request.v1", + openDirectReceipt: "gamecult.persona.chat_open_direct_receipt.v1", }); export const refusals = Object.freeze(["Brake", "PersonaMismatch", "UnknownRoom", "EmptyText", "SendFailed", "UnknownPersona"]); @@ -23,6 +25,12 @@ export const refusals = Object.freeze(["Brake", "PersonaMismatch", "UnknownRoom" // RoomMoved exists only in the v2 receipt: the v1 tuple, a v1 parse and the typed refusal document do not know it. export const refusalsV2 = Object.freeze([...refusals, "RoomMoved"]); +// The typed refusal document also answers a request of a kind this mouth does not serve (no Persona is known then). +const refusalDocument = Object.freeze([...refusals, "UnknownKind"]); + +// One outcome word per open-direct receipt. room_id and event_id are present exactly for Opened and Existing. +export const directOutcomes = Object.freeze(["Opened", "Existing", "Brake", "PersonaMismatch", "UnknownPersona", "EmptyText", "ForeignUser", "NotAPerson", "NoSuchUser", "InvitePending", "Declined", "DailyLimit", "SendFailed"]); + const maxTextBytes = 4000; export const maxNewer = 50; const maxContext = 20; @@ -119,15 +127,41 @@ export function parseSendReceiptV2(t) { // A typed refusal for a request that names a Persona the mouth does not serve. export function refusalTuple({ persona, refusal }) { - if (!refusals.includes(refusal)) throw new Error(`unknown refusal ${refusal}`); + if (!refusalDocument.includes(refusal)) throw new Error(`unknown refusal ${refusal}`); return [text(persona, "persona"), refusal]; } export function parseRefusal(t) { const [persona, refusal] = tuple(t, 2, "chat_refusal"); - if (!refusals.includes(refusal)) throw new Error(`unknown refusal ${String(refusal)}`); + if (!refusalDocument.includes(refusal)) throw new Error(`unknown refusal ${String(refusal)}`); return { persona: text(persona, "persona"), refusal }; } +// A Persona asks her mouth to open a direct room with one person and say a first line there. user_id is a Matrix user id. +export function openDirectRequestTuple({ requestId, persona, userId, text: body }) { + return [text(requestId, "request_id"), text(persona, "persona"), text(userId, "user_id"), String(body)]; +} +export function parseOpenDirectRequest(t) { + const [requestId, persona, userId, body] = tuple(t, 4, "chat_open_direct_request"); + if (typeof body !== "string") throw new Error("chat_open_direct_request text must be a string"); + if (Buffer.byteLength(body, "utf8") > maxTextBytes) throw new Error("chat_open_direct_request text is over the byte bound"); + return { requestId: text(requestId, "request_id"), persona: text(persona, "persona"), userId: text(userId, "user_id"), text: body }; +} + +function checkDirectReceipt(outcome, roomId, eventId) { + if (!directOutcomes.includes(outcome)) throw new Error(`unknown open-direct outcome ${String(outcome)}`); + const placed = outcome === "Opened" || outcome === "Existing"; + if (placed !== (roomId !== null && eventId !== null) || (!placed && (roomId !== null || eventId !== null))) throw new Error("room_id and event_id are present exactly for Opened and Existing"); +} +export function openDirectReceiptTuple({ requestId, outcome, roomId = null, eventId = null }) { + checkDirectReceipt(outcome, roomId, eventId); + return [text(requestId, "request_id"), outcome, roomId === null ? null : text(roomId, "room_id"), eventId === null ? null : text(eventId, "event_id")]; +} +export function parseOpenDirectReceipt(t) { + const [requestId, outcome, roomId, eventId] = tuple(t, 4, "chat_open_direct_receipt"); + checkDirectReceipt(outcome, roomId, eventId); + return { requestId: text(requestId, "request_id"), outcome, roomId: roomId === null ? null : text(roomId, "room_id"), eventId: eventId === null ? null : text(eventId, "event_id") }; +} + export function deliveryTuple(d) { return [ seq(d.seq, "seq"), text(d.roomAlias, "room_alias"), text(d.roomId, "room_id"), text(d.eventId, "event_id"), @@ -190,10 +224,11 @@ export function envelope({ kind, recordKey, payload, sourceRuntimeId, sourceAgen }; } +// allowedKinds null: any kind passes (the caller answers a kind it does not serve). export function checkEnvelope(message, allowedKinds) { const d = message?.document; if (message?.schemaVersion !== "cultnet.document_put_raw.v0" || !d) throw new Error("not a document_put_raw message"); - if (!allowedKinds.includes(d.schemaId)) throw new Error(`document kind ${String(d.schemaId)} is not accepted here`); + if (allowedKinds !== null && !allowedKinds.includes(d.schemaId)) throw new Error(`document kind ${String(d.schemaId)} is not accepted here`); if (d.payloadEncoding !== "messagepack") throw new Error("chat documents are MessagePack"); if (!Array.isArray(d.tags) || d.tags.length !== 1 || d.tags[0] !== "cultnet.transport.rudp.v0") throw new Error("chat document tags substituted authority"); return d; diff --git a/tools/persona-matrix-mouth.mjs b/tools/persona-matrix-mouth.mjs index 658c728..1fca153 100644 --- a/tools/persona-matrix-mouth.mjs +++ b/tools/persona-matrix-mouth.mjs @@ -58,6 +58,9 @@ const SLUG = /^[A-Za-z0-9_-]{1,64}$/; const SEND_TIMEOUT_MS = 10000; // the head read and the PUT of one gated send const PRESENCE_GRACE_MS = 5 * 60 * 1000; // a turn's last sign of life keeps her online this long const DELIVERED_INDEX = 4000; // delivered event ids remembered so a read report can be checked against them +const DM_SCAN = 200; // joined rooms read when looking for an existing DM with a person +const NOT_E2EE = "This room is not end-to-end encrypted: the homeserver can read what is said here."; +const dayOf = ms => new Date(ms).toISOString().slice(0, 10); const HEAD_PAGE = 50; // the room head a gated send reads; also the most ids a RoomMoved receipt names // A message line: a text event with a sender, not an edit. @@ -114,6 +117,7 @@ export function createMouth({ now = Date.now, log = () => {}, syncTimeoutMs = 30000, maxPending = MAX_PENDING, gate = createRoomGate(), sendTimeoutMs = SEND_TIMEOUT_MS, presenceGraceMs = PRESENCE_GRACE_MS, images = null, // this Persona's image store (createImageStore), or null: images stay the typed text line + isServed = async () => false, // (async) whether a Matrix user id is an account this process speaks for: a Mind is not a person to open a DM with }) { if (!persona || !homeserver || !token || !rooms?.length) throw new Error("persona, homeserver, rooms and token are required"); const say = message => log(redact(String(message), token)); @@ -138,6 +142,9 @@ export function createMouth({ const directContent = {}; // the account's m.direct content: user id -> room ids const invites = new Map(); // room id -> { inviter, direct }: invitations not yet answered const processed = new Set(); // event ids read since the position last advanced + const noticed = new Set(); // rooms this mouth created where the not-E2EE first line has gone out + const personTails = new Map(); // person's user id -> the open in flight: opens for one person run one at a time + const openedOn = new Map(); // person's user id -> the UTC day this process last created a room for them: one creation per person per day async function matrix(method, path, body, prefix = API, signal) { let response; @@ -466,6 +473,103 @@ export function createMouth({ } } + // Every room this account shares with `userId` alone, newest first: { roomId, createdAt, marker, encrypted, theirs }, theirs the + // person's membership ("join" | "invite" | "leave" | "ban" | null when they were never invited). Project rooms are not DMs. A + // room qualifies when its members are exactly the two of us, or m.direct lists it for the person (a created room whose invite failed). + async function directsWith(userId) { + const joined = (await matrix("GET", "/joined_rooms")).joined_rooms ?? []; + const known = new Set(directContent[userId] ?? []); + const candidates = [...known, ...joined.filter(id => !known.has(id) && !aliasByRoomId.has(id))].slice(0, DM_SCAN); + const found = []; + for (const roomId of candidates) { + let events; + try { events = await matrix("GET", `/rooms/${encodeURIComponent(roomId)}/state`); } catch (error) { + if (/ 40[34] /.test(error.message)) continue; // a room m.direct still lists that this account has left or forgotten + throw error; + } + const members = events.filter(e => e.type === "m.room.member"); + const mine = members.find(e => e.state_key === selfId); + if (mine?.content?.membership !== "join") continue; + const others = members.filter(e => e.state_key !== selfId); + const theirs = others.find(e => e.state_key === userId); + if (!(known.has(roomId) || (others.length === 1 && theirs))) continue; + const create = events.find(e => e.type === "m.room.create"); + found.push({ + roomId, createdAt: create?.origin_server_ts ?? 0, marker: create?.content?.["org.gamecult.open_request"] ?? null, + encrypted: events.some(e => e.type === "m.room.encryption"), theirs: theirs?.content?.membership ?? null, + }); + } + return found.sort((a, b) => b.createdAt - a.createdAt); + } + + // The first line of an open: the invite is the ask and the room is not end-to-end encrypted, and the line says so. + // Every line into a room this path created carries the notice until one has gone out (a restart repeats it once at worst). + async function putLine(roomId, requestId, text, made) { + return gate.run(roomId, async () => { + if (!(await released())) return null; + const body = made && !noticed.has(roomId) ? `${NOT_E2EE}\n\n${text}` : text; + const sent = (await matrix("PUT", `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${encodeURIComponent(requestId)}`, { msgtype: "m.text", body }, API, AbortSignal.timeout(sendTimeoutMs))).event_id; + if (made) noticed.add(roomId); + return sent; + }); + } + + // A Persona's request for a direct room with one person. One outcome word and, for Opened and Existing, the room and the line's event. + function openDirect(request) { + const key = typeof request.userId === "string" ? request.userId : ""; + const run = () => openDirectNow(request); + const tail = (personTails.get(key) ?? Promise.resolve()).then(run, run); + personTails.set(key, tail); + const done = () => { if (personTails.get(key) === tail) personTails.delete(key); }; + tail.then(done, done); + return tail; + } + async function openDirectNow({ requestId, persona: asked, userId, text }) { + const answer = (outcome, roomId = null, eventId = null) => { say(`open direct ${requestId}: ${outcome}${roomId ? ` ${roomId}` : ""}`); return { requestId, outcome, roomId, eventId }; }; + if (asked !== persona) return answer("PersonaMismatch"); + if (!(await released())) return answer("Brake"); + if (typeof text !== "string" || text.trim() === "") return answer("EmptyText"); + try { + if (!started || unjoined()) await start(); + if (!serverName || typeof userId !== "string" || userId.slice(userId.indexOf(":") + 1) !== serverName) return answer("ForeignUser"); + if (userId === selfId || await isServed(userId)) return answer("NotAPerson"); + try { await matrix("GET", `/profile/${encodeURIComponent(userId)}`); } catch (error) { + if (/ 404 /.test(error.message)) return answer("NoSuchUser"); + throw error; + } + const rooms = await directsWith(userId); + const usable = rooms.filter(r => !r.encrypted); // a line is never sent into an encrypted room + const resumed = usable.find(r => r.marker === requestId); // a repeat of this very request after a crash + const live = resumed ?? usable.find(r => r.theirs === "join"); + if (live) { + const eventId = await putLine(live.roomId, requestId, text, live.marker !== null); + return eventId === null ? answer("Brake") : answer(resumed ? "Opened" : "Existing", live.roomId, eventId); + } + const latest = rooms.find(r => ["invite", "leave", "ban"].includes(r.theirs)); // an invite or a decline stands in an encrypted room too + if (latest?.theirs === "invite") return answer("InvitePending"); + if (latest) return answer("Declined"); // left or banned, and nothing newer: only the person reopens it, by inviting her + let roomId = usable.find(r => r.theirs === null && r.marker !== null)?.roomId ?? null; // made here earlier, the invite never went out + if (roomId === null) { + if (openedOn.get(userId) === dayOf(now())) return answer("DailyLimit"); + if (!(await released())) return answer("Brake"); + ({ room_id: roomId } = await matrix("POST", "/createRoom", { preset: "private_chat", is_direct: true, creation_content: { "m.federate": false, "org.gamecult.open_request": requestId } }, API, AbortSignal.timeout(sendTimeoutMs))); + openedOn.set(userId, dayOf(now())); + directRooms.add(roomId); + directContent[userId] = [...new Set([...(directContent[userId] ?? []), roomId])]; + await matrix("PUT", `/user/${encodeURIComponent(selfId)}/account_data/m.direct`, directContent).catch(error => say(`m.direct not saved: ${error.message}`)); + } + if (!(await released())) return answer("Brake"); + await matrix("POST", `/rooms/${encodeURIComponent(roomId)}/invite`, { user_id: userId, reason: docs.clip(text, 300) }, API, AbortSignal.timeout(sendTimeoutMs)); + const eventId = await putLine(roomId, requestId, text, true); + if (eventId === null) return answer("Brake"); + touch(); setPresence(true).catch(() => {}); + return answer("Opened", roomId, eventId); + } catch (error) { + say(`open direct failed: ${error.message}`); + return answer("SendFailed"); + } + } + async function run(signal) { const pause = ms => new Promise(r => { const t = setTimeout(r, ms); signal?.addEventListener("abort", () => { clearTimeout(t); r(); }, { once: true }); }); while (!signal?.aborted) { @@ -479,7 +583,7 @@ export function createMouth({ } } - return { start, syncOnce, ack, read, pendingAfter, send, run, get pendingCount() { return pending.length; } }; + return { start, syncOnce, ack, read, pendingAfter, send, openDirect, run, get userId() { return selfId; }, get pendingCount() { return pending.length; } }; } // Replace every secret in a message. `secrets` is one string or a list. @@ -557,13 +661,24 @@ export async function startChatEndpoint({ mouths, runtime, port, connectionKey, const respond = (peer, persona, kind, recordKey, tuple) => server.send(peer, "schema", msgpack.encode(cultnet.encodeCultNetMessageForWire( docs.envelope({ kind, recordKey, payload: msgpack.encode(tuple), sourceRuntimeId: runtimeId, sourceAgentId: persona, sourceRole: "bifrost-persona-matrix-mouth" }), "cultnet.schema.v0"))); const refuseUnknown = (peer, persona) => respond(peer, persona, docs.kinds.refusal, persona, docs.refusalTuple({ persona, refusal: "UnknownPersona" })); + const served = [docs.kinds.pendingRequest, docs.kinds.ack, docs.kinds.read, docs.kinds.sendRequest, docs.kinds.sendRequestV2, docs.kinds.openDirectRequest]; const handle = async ({ peer, channelId, payload }) => { try { if (channelId !== "schema") throw new Error("chat request arrived on a foreign channel"); const message = cultnet.parseCultNetMessage(msgpack.decode(payload), "cultnet.schema.v0"); - const document = docs.checkEnvelope(message, [docs.kinds.pendingRequest, docs.kinds.ack, docs.kinds.read, docs.kinds.sendRequest, docs.kinds.sendRequestV2]); + const document = docs.checkEnvelope(message, null); + if (!served.includes(document.schemaId)) { // a kind this mouth does not serve is answered, loudly, and logged: never dropped + log(`chat endpoint refused a request of an unknown kind: ${String(document.schemaId).slice(0, 120)}`); + const who = typeof document.sourceAgentId === "string" && SLUG.test(document.sourceAgentId) ? document.sourceAgentId : "unknown"; + return respond(peer, who, docs.kinds.refusal, who, docs.refusalTuple({ persona: who, refusal: "UnknownKind" })); + } const tuple = msgpack.decode(document.payload); - if (document.schemaId === docs.kinds.pendingRequest) { + if (document.schemaId === docs.kinds.openDirectRequest) { + const request = docs.parseOpenDirectRequest(tuple); + const mouth = mouths.get(request.persona); + const receipt = mouth ? await mouth.openDirect(request) : { requestId: request.requestId, outcome: "UnknownPersona", roomId: null, eventId: null }; + respond(peer, request.persona, docs.kinds.openDirectReceipt, receipt.requestId, docs.openDirectReceiptTuple(receipt)); + } else if (document.schemaId === docs.kinds.pendingRequest) { const request = docs.parsePendingRequest(tuple); const mouth = mouths.get(request.persona); if (!mouth) return refuseUnknown(peer, request.persona); @@ -669,7 +784,7 @@ export async function main(env = process.env) { const mouths = new Map(personas.map(persona => { mkdirSync(join(stateRoot, persona), { recursive: true }); return [persona, createMouth({ - persona, homeserver, serverName, rooms: roomsByPersona.get(persona), token: tokens.get(persona), gate, log: line => log(`${persona}: ${line}`), + persona, homeserver, serverName, rooms: roomsByPersona.get(persona), token: tokens.get(persona), gate, isServed: async id => { await Promise.all([...mouths.values()].map(m => m.start().catch(() => {}))); return [...mouths.values()].some(m => m.userId === id); }, log: line => log(`${persona}: ${line}`), images: imageRoot ? createImageStore({ root: join(imageRoot, persona), log: line => log(`${persona}: ${line}`) }) : null, state: cultcacheState({ runtime, path: join(stateRoot, persona, "cursor.cc") }), brake: createBrakeReader({ controlPath: join(controlRoot, persona, "control.cc"), readStore: runtime.readSingleFileStore, decode: runtime.msgpack.decode }), From 12a153f66f9933b9306867cba52bf5fdebf8778a Mon Sep 17 00:00:00 2001 From: Heimdall Date: Tue, 10 Nov 2026 21:06:56 +0000 Subject: [PATCH 04/26] Mill votes: ranked, paged read and not-yet-voted-by-this-network filter (#41) --- docs/concept-mill-votes-target.md | 1 + .../ConceptVotes/ConceptAppraisalService.cs | 80 +++ .../ConceptVotes/ConceptTallyExport.cs | 85 +++ .../Features/ConceptVotes/RankCursor.cs | 59 ++ src/Bifrost.Web/Program.cs | 1 + .../ConceptRankedExportTests.cs | 561 ++++++++++++++++++ 6 files changed, 787 insertions(+) create mode 100644 src/Bifrost.Web/Features/ConceptVotes/RankCursor.cs create mode 100644 tests/Bifrost.Web.Tests/ConceptRankedExportTests.cs diff --git a/docs/concept-mill-votes-target.md b/docs/concept-mill-votes-target.md index 91c92e6..c20525d 100644 --- a/docs/concept-mill-votes-target.md +++ b/docs/concept-mill-votes-target.md @@ -85,6 +85,7 @@ Derived from them, not asked separately: - **Network prefix.** Owner: one function, `NetworkPrefix`, mapping a client address to its configured IPv4/IPv6 prefix (IPv4-mapped IPv6 folded to IPv4). The audit `NetworkTag` (HMAC-SHA256 of the prefix under a configured secret) and the rate-limit partition both read it; no second prefix computation exists. - **Tallies.** Derived only: per live subject, per stance, all and members, computed from rows and never stored as counters. - **Public tally export.** The JSON xenos boundary: anonymous `GET /public/concepts/{collection}/tallies` returning `{ key: { up, down, members: { up, down } } }` for every live subject; no voter keys, account ids or tags; CORS for the configured origins only (`https://aetheria.gamecult.org`), `GET`, no credentials; `Cache-Control: public, max-age=60`. It owns nothing. +- **Ranked read.** Beside the map, anonymous `GET /public/concepts/{collection}/ranked?sort=net|total|newest&limit=1..100&cursor=&unvoted=network` returns `{ items: [{ key, title, up, down, members }], next_cursor }`, largest first, ties by key. The cursor is opaque, keyset and bound to its sort and filter; any other query shape or cursor is a 400. `unvoted=network` omits the concepts the caller's own network tag has voted on (tag derived from the connection, never accepted or returned) and is never cached. Same CORS and Vary policy as the map; the map is unchanged. - **The Mill page.** Read only. Its existing vote slot (fact 28) fills from the export and links to `https://bifrost.gamecult.org/mill/aetheria-concept-mill/`. Voting happens on Bifrost's own origin, which keeps the device cookie first-party. - **Client address.** `UseForwardedHeaders` trusting loopback only makes `RemoteIpAddress` the real client. It is a rate-limit bucket and an audit tag, never a vote key. - **Key ring.** Data Protection keys live at a configured path with a fixed application name, so tokens survive restarts and deploys; production refuses to start without the path. diff --git a/src/Bifrost.Web/Features/ConceptVotes/ConceptAppraisalService.cs b/src/Bifrost.Web/Features/ConceptVotes/ConceptAppraisalService.cs index 9e0132b..ac96eb8 100644 --- a/src/Bifrost.Web/Features/ConceptVotes/ConceptAppraisalService.cs +++ b/src/Bifrost.Web/Features/ConceptVotes/ConceptAppraisalService.cs @@ -23,6 +23,18 @@ public sealed record StanceCounts(int Up, int Down); public sealed record ConceptTally(int Up, int Down, StanceCounts Members); +public enum RankOrder +{ + Net, + Total, + Newest +} + +// One row of the ranked read: counts and the subject's title only, never a voter key, account id or network tag. +public sealed record RankedSubject(string Key, string Title, ConceptTally Tally); + +public sealed record RankedPage(IReadOnlyList Items, string? NextCursor); + public sealed record ConceptSubjectView( string Collection, string Key, @@ -189,6 +201,74 @@ public sealed class ConceptAppraisalService( StringComparer.Ordinal); } + // The ranked read: live subjects of one collection ordered by net (up minus down), total (up plus down) or + // registration time, largest first, ties by key ascending, so the order is total and a cursor names a position. + // The cursor is keyset: the next page is whatever sorts after the last row's (value, key), so a vote landing + // between pages can move rows but never makes a page repeat the cursor's own row. With unvoted and a client + // address, subjects whose stored tag equals the client's are left out (no address filters nothing) ("not yet voted by this network"); the tag is only compared + // inside the query and is never returned. Null when the collection has no live subject. + public async Task GetRankedAsync( + string collection, + RankOrder order, + int limit, + RankCursor? after, + bool unvoted, + IPAddress? client, + CancellationToken cancellationToken) + { + var live = await dbContext.ConceptSubjects.AsNoTracking() + .Where(x => x.Collection == collection && x.RetiredAtUtc == null) + .Select(x => new { x.Id, x.Key, x.Title, x.RegisteredAtUtc }) + .ToListAsync(cancellationToken); + if (live.Count == 0) + { + return null; + } + + var ids = live.Select(x => x.Id).ToList(); + var groups = await dbContext.ConceptAppraisals.AsNoTracking() + .Where(x => ids.Contains(x.SubjectId)) + .GroupBy(x => new { x.SubjectId, x.Stance, IsMember = x.VoterClass == VoterClass.Member }) + .Select(g => new { g.Key.SubjectId, g.Key.Stance, g.Key.IsMember, Count = g.Count() }) + .ToListAsync(cancellationToken); + + var voted = new HashSet(); + if (unvoted && client is not null) + { + var opts = options.Value; + var tag = NetworkPrefix.Tag(client, opts.IPv4PrefixLength, opts.IPv6PrefixLength, opts.NetworkTagSecret); + voted = (await dbContext.ConceptAppraisals.AsNoTracking() + .Where(x => ids.Contains(x.SubjectId) && x.NetworkTag == tag) + .Select(x => x.SubjectId) + .ToListAsync(cancellationToken)).ToHashSet(); + } + + var rows = live + .Where(s => !voted.Contains(s.Id)) + .Select(s => + { + var tally = Tally(groups.Where(g => g.SubjectId == s.Id).Select(g => (g.Stance, g.IsMember, g.Count))); + var value = order switch + { + RankOrder.Net => (long)tally.Up - tally.Down, + RankOrder.Total => (long)tally.Up + tally.Down, + _ => s.RegisteredAtUtc.UtcTicks + }; + return (Value: value, s.Key, s.Title, Tally: tally); + }) + .Where(r => after is null || r.Value < after.Value || (r.Value == after.Value && string.CompareOrdinal(r.Key, after.Key) > 0)) + .OrderByDescending(r => r.Value) + .ThenBy(r => r.Key, StringComparer.Ordinal) + .Take(limit + 1) + .ToList(); + + var page = rows.Take(limit).ToList(); + var next = rows.Count > limit + ? new RankCursor(order, unvoted, page[^1].Value, page[^1].Key).Encode() + : null; + return new RankedPage(page.Select(r => new RankedSubject(r.Key, r.Title, r.Tally)).ToList(), next); + } + public async Task GetSubjectAsync( string collection, string key, diff --git a/src/Bifrost.Web/Features/ConceptVotes/ConceptTallyExport.cs b/src/Bifrost.Web/Features/ConceptVotes/ConceptTallyExport.cs index fb1f87f..5943a09 100644 --- a/src/Bifrost.Web/Features/ConceptVotes/ConceptTallyExport.cs +++ b/src/Bifrost.Web/Features/ConceptVotes/ConceptTallyExport.cs @@ -65,4 +65,89 @@ public static class ConceptTallyExport response.Headers.CacheControl = "public, max-age=60"; return Results.Json(tallies); }).AllowAnonymous().RequireCors(CorsPolicy); + + public const int DefaultPageSize = 50; + public const int MaxPageSize = 100; + + // The ranked, paged read beside the map: GET /public/concepts/{collection}/ranked + // ?sort=net|total|newest (default net) &limit=1..100 (default 50) &cursor= &unvoted=network + // returns { items: [{ key, title, up, down, members: { up, down } }], next_cursor }. The query is a closed + // shape: an unknown or repeated key, an out-of-range value or a cursor that is not exactly one this route issued + // for the same sort and filter is a 400. unvoted=network leaves out the concepts the caller's own network tag + // has already voted on; the tag is derived from the connection and compared inside the query, never accepted + // from the request and never returned. That answer depends on the caller, so it is never cached. + public static IEndpointConventionBuilder MapConceptRankedExport(this IEndpointRouteBuilder app) => + app.MapGet(RoutePrefix + "/{collection}/ranked", async ( + string collection, + HttpContext http, + ConceptAppraisalService appraisals, + CancellationToken cancellationToken) => + { + if (!ConceptKey.IsValid(collection)) + { + return Results.NotFound(); + } + + if (!TryReadRankedQuery(http.Request.Query, out var order, out var limit, out var cursor, out var unvoted)) + { + return JsonText(new { error = "request_invalid" }, StatusCodes.Status400BadRequest); + } + + var page = await appraisals.GetRankedAsync( + collection, order, limit, cursor, unvoted, http.Connection.RemoteIpAddress, cancellationToken); + if (page is null) + { + return Results.NotFound(); + } + + http.Response.Headers.CacheControl = unvoted ? "private, no-store" : "public, max-age=60"; + return JsonText(new + { + items = page.Items.Select(i => new + { + key = i.Key, + title = i.Title, + up = i.Tally.Up, + down = i.Tally.Down, + members = new { up = i.Tally.Members.Up, down = i.Tally.Members.Down } + }), + next_cursor = page.NextCursor + }, StatusCodes.Status200OK); + }).AllowAnonymous().RequireCors(CorsPolicy); + + private static IResult JsonText(object body, int status) => + Results.Text(System.Text.Json.JsonSerializer.Serialize(body), "application/json", statusCode: status); + + internal static bool TryReadRankedQuery( + IQueryCollection query, out RankOrder order, out int limit, out RankCursor? cursor, out bool unvoted) + { + order = RankOrder.Net; + limit = DefaultPageSize; + cursor = null; + unvoted = false; + foreach (var (key, values) in query) + { + if (values.Count != 1 || values[0] is not { } value) + { + return false; + } + + switch (key) + { + case "sort" when value == "net": order = RankOrder.Net; break; + case "sort" when value == "total": order = RankOrder.Total; break; + case "sort" when value == "newest": order = RankOrder.Newest; break; + case "limit" when int.TryParse(value, System.Globalization.NumberStyles.None, + System.Globalization.CultureInfo.InvariantCulture, out var n) && + n is >= 1 and <= MaxPageSize && value == n.ToString(System.Globalization.CultureInfo.InvariantCulture): + limit = n; + break; + case "cursor" when RankCursor.TryDecode(value, out var decoded): cursor = decoded; break; + case "unvoted" when value == "network": unvoted = true; break; + default: return false; + } + } + + return cursor is null || (cursor.Order == order && cursor.Unvoted == unvoted); + } } diff --git a/src/Bifrost.Web/Features/ConceptVotes/RankCursor.cs b/src/Bifrost.Web/Features/ConceptVotes/RankCursor.cs new file mode 100644 index 0000000..c8375ba --- /dev/null +++ b/src/Bifrost.Web/Features/ConceptVotes/RankCursor.cs @@ -0,0 +1,59 @@ +using System.Globalization; +using System.Text; +using Microsoft.AspNetCore.WebUtilities; + +namespace Bifrost.Web.Features.ConceptVotes; + +// The opaque cursor of the ranked read: the last row's sort value and key, plus the order and filter it was issued +// for, so a cursor cannot be replayed under another order or filter. Text "1|order|u|value|key", base64url without +// padding; the length cap only bounds decoding work. Not signed: it names a position in public data and grants nothing. Decoding is strict and canonical (a +// cursor that does not re-encode to the same text is refused), and every field is re-validated, so no cursor text +// reaches a query unparsed. +public sealed record RankCursor(RankOrder Order, bool Unvoted, long Value, string Key) +{ + public const int MaxLength = 200; + + public string Encode() => + WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes( + string.Create(CultureInfo.InvariantCulture, $"1|{(int)Order}|{(Unvoted ? 1 : 0)}|{Value}|{Key}"))); + + public static bool TryDecode(string? text, out RankCursor cursor) + { + cursor = null!; + if (string.IsNullOrEmpty(text) || text.Length > MaxLength) + { + return false; + } + + string plain; + try + { + var bytes = WebEncoders.Base64UrlDecode(text); + plain = new UTF8Encoding(false, true).GetString(bytes); + } + catch (Exception ex) when (ex is FormatException or ArgumentException) + { + return false; + } + + var parts = plain.Split('|'); + if (parts.Length != 5 || parts[0] != "1" || + !int.TryParse(parts[1], NumberStyles.None, CultureInfo.InvariantCulture, out var order) || + !Enum.IsDefined((RankOrder)order) || + parts[2] is not ("0" or "1") || + !long.TryParse(parts[3], NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out var value) || + !ConceptKey.IsValid(parts[4])) + { + return false; + } + + var candidate = new RankCursor((RankOrder)order, parts[2] == "1", value, parts[4]); + if (candidate.Encode() != text) + { + return false; + } + + cursor = candidate; + return true; + } +} diff --git a/src/Bifrost.Web/Program.cs b/src/Bifrost.Web/Program.cs index bf6103a..923ed4e 100644 --- a/src/Bifrost.Web/Program.cs +++ b/src/Bifrost.Web/Program.cs @@ -293,6 +293,7 @@ app.MapGet("/readyz", async (ReadinessService readinessService, CancellationToke }).AllowAnonymous(); app.MapConceptTallyExport(); +app.MapConceptRankedExport(); if (oidcOptions is not null) { diff --git a/tests/Bifrost.Web.Tests/ConceptRankedExportTests.cs b/tests/Bifrost.Web.Tests/ConceptRankedExportTests.cs new file mode 100644 index 0000000..c6ebd74 --- /dev/null +++ b/tests/Bifrost.Web.Tests/ConceptRankedExportTests.cs @@ -0,0 +1,561 @@ +using System.Net; +using System.Text; +using System.Text.Json; +using Bifrost.Web.Data; +using Bifrost.Web.Domain; +using Bifrost.Web.Features.ConceptVotes; +using Bifrost.Web.Features.Shared; +using Bifrost.Web.Tests.Support; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Bifrost.Web.Tests; + +public sealed class ConceptRankedExportTests : IClassFixture +{ + private const string AllowedOrigin = "https://aetheria.gamecult.org"; + private static readonly DateTimeOffset Epoch = new(2026, 11, 1, 0, 0, 0, TimeSpan.Zero); + + private readonly RankedFactory _factory; + + public ConceptRankedExportTests(RankedFactory factory) + { + _factory = factory; + } + + [Fact] + public async Task Net_ranks_up_minus_down_and_ties_break_by_key() + { + var c = await Seed(); + + var keys = Keys(await GetOk(c, "?sort=net")); + + Assert.Equal(["bravo", "alpha", "charlie", "delta", "echo"], keys); + } + + [Fact] + public async Task Default_order_is_net_and_the_default_page_is_everything_under_the_cap() + { + var c = await Seed(); + + var body = await GetOk(c, ""); + + Assert.Equal(Keys(await GetOk(c, "?sort=net")), Keys(body)); + Assert.Equal(JsonValueKind.Null, body.GetProperty("next_cursor").ValueKind); + } + + [Fact] + public async Task Total_ranks_up_plus_down() + { + var c = await Seed(); + + Assert.Equal(["alpha", "bravo", "charlie", "delta", "echo"], Keys(await GetOk(c, "?sort=total"))); + } + + [Fact] + public async Task Newest_ranks_by_registration_time() + { + var c = await Seed(); + + Assert.Equal(["echo", "delta", "charlie", "bravo", "alpha"], Keys(await GetOk(c, "?sort=newest"))); + } + + [Fact] + public async Task Items_carry_counts_and_title_only() + { + var c = await Seed(); + + var first = (await GetOk(c, "?limit=1")).GetProperty("items")[0]; + + Assert.Equal(["down", "key", "members", "title", "up"], first.EnumerateObject().Select(p => p.Name).Order().ToArray()); + Assert.Equal(["down", "up"], first.GetProperty("members").EnumerateObject().Select(p => p.Name).Order().ToArray()); + } + + [Theory] + [InlineData("net")] + [InlineData("total")] + [InlineData("newest")] + public async Task Paging_walks_every_subject_once_in_the_same_order_as_one_big_page(string sort) + { + var c = await Seed(); + var whole = Keys(await GetOk(c, $"?sort={sort}")); + + var walked = new List(); + string? cursor = null; + var pages = 0; + do + { + var body = await GetOk(c, $"?sort={sort}&limit=2" + (cursor is null ? "" : "&cursor=" + cursor)); + walked.AddRange(Keys(body)); + cursor = body.GetProperty("next_cursor").ValueKind == JsonValueKind.Null ? null : body.GetProperty("next_cursor").GetString(); + pages++; + } + while (cursor is not null && pages < 10); + + Assert.Equal(whole, walked); + Assert.Equal(3, pages); + } + + [Fact] + public async Task Ties_break_by_ordinal_key_even_when_keys_hold_hyphens_and_were_registered_out_of_order() + { + var c = "c" + Guid.NewGuid().ToString("N"); + string[] ordinal = ["a-b", "a-c", "ab", "b", "b-a"]; + using (var scope = _factory.Services.CreateScope()) + { + var db = scope.ServiceProvider.GetRequiredService(); + foreach (var key in ordinal.Reverse()) + { + db.ConceptSubjects.Add(new ConceptSubject { Collection = c, Key = key, Title = key, RegisteredAtUtc = Epoch }); + } + + await db.SaveChangesAsync(); + } + + var walked = new List(); + string? cursor = null; + do + { + var body = await GetOk(c, "?limit=1" + (cursor is null ? "" : "&cursor=" + cursor)); + walked.AddRange(Keys(body)); + cursor = body.GetProperty("next_cursor").ValueKind == JsonValueKind.Null ? null : body.GetProperty("next_cursor").GetString(); + } + while (cursor is not null && walked.Count < 10); + + Assert.Equal(ordinal, walked); + Assert.Equal(ordinal, Keys(await GetOk(c, "?sort=newest"))); + } + + [Fact] + public async Task Unvoted_without_a_client_address_still_pages() + { + var c = await Seed(); + + var first = await GetOk(c, "?unvoted=network&limit=2"); + var second = await GetOk(c, "?unvoted=network&limit=2&cursor=" + first.GetProperty("next_cursor").GetString()); + + Assert.Equal(["bravo", "alpha"], Keys(first)); + Assert.Equal(["charlie", "delta"], Keys(second)); + } + + [Fact] + public async Task A_vote_between_pages_never_repeats_the_cursors_own_row() + { + var c = await Seed(); + var first = await GetOk(c, "?limit=2"); + var seen = Keys(first); + + await Cast(c, "echo", "mover-1", ConceptStance.Up); + await Cast(c, "echo", "mover-2", ConceptStance.Up); + await Cast(c, "echo", "mover-3", ConceptStance.Up); + await Cast(c, "echo", "mover-4", ConceptStance.Up); + var second = Keys(await GetOk(c, "?limit=5&cursor=" + first.GetProperty("next_cursor").GetString())); + + Assert.Empty(seen.Intersect(second)); + } + + [Theory] + [InlineData("?limit=0")] + [InlineData("?limit=101")] + [InlineData("?limit=-1")] + [InlineData("?limit=+5")] + [InlineData("?limit=05")] + [InlineData("?limit=5.0")] + [InlineData("?limit=%205")] + [InlineData("?limit=")] + [InlineData("?limit=abc")] + [InlineData("?limit=99999999999999999999")] + [InlineData("?sort=")] + [InlineData("?sort=oldest")] + [InlineData("?sort=Net")] + [InlineData("?sort=net&sort=total")] + [InlineData("?limit=1&limit=2")] + [InlineData("?unvoted=")] + [InlineData("?unvoted=all")] + [InlineData("?unvoted=network&unvoted=network")] + [InlineData("?Sort=net")] + [InlineData("?tag=abc")] + [InlineData("?networkTag=abc")] + [InlineData("?page=2")] + public async Task A_query_outside_the_closed_shape_is_a_400(string query) + { + var c = await Seed(); + + using var response = await Get(c, query); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + Assert.Equal("request_invalid", JsonDocument.Parse(await response.Content.ReadAsStringAsync()).RootElement.GetProperty("error").GetString()); + } + + [Fact] + public async Task The_page_cap_is_one_hundred_and_one_hundred_is_allowed() + { + var c = await Seed(); + + using var ok = await Get(c, "?limit=100"); + using var over = await Get(c, "?limit=101"); + + Assert.Equal(HttpStatusCode.OK, ok.StatusCode); + Assert.Equal(HttpStatusCode.BadRequest, over.StatusCode); + } + + public static IEnumerable HostileCursors() + { + static string B64(string text) => Convert.ToBase64String(Encoding.UTF8.GetBytes(text)).TrimEnd('=').Replace('+', '-').Replace('/', '_'); + yield return ["x"]; + yield return ["%00"]; + yield return ["not base64!"]; + yield return ["AAAA"]; + yield return [new string('A', 201)]; + yield return [new string('A', 100_000)]; + yield return [B64("")]; + yield return [B64("1|0|0|5")]; + yield return [B64("1|0|0|5|alpha|extra")]; + yield return [B64("2|0|0|5|alpha")]; + yield return [B64("1|3|0|5|alpha")]; + yield return [B64("1|-1|0|5|alpha")]; + yield return [B64("1|0|2|5|alpha")]; + yield return [B64("1|0|0|abc|alpha")]; + yield return [B64("1|0|0|+5|alpha")]; + yield return [B64("1|0|0|-0|alpha")]; + yield return [B64("1|0|0|05|alpha")]; + yield return [B64("1|0|0|99999999999999999999|alpha")]; + yield return [B64("1|0|0|5|Alpha")]; + yield return [B64("1|0|0|5|al pha")]; + yield return [B64("1|0|0|5|alpha\n")]; + yield return [B64("1|0|0|5|'; drop table")]; + yield return [B64("1|0|0|5|../x")]; + yield return [B64("1|0|0|5|" + new string('a', 81))]; + yield return [B64("1|0|0|5|")]; + yield return [Convert.ToBase64String(Encoding.UTF8.GetBytes("1|0|0|5|alpha"))]; + yield return [B64("1|0|0|5|alpha") + "="]; + yield return [Convert.ToBase64String([0xFF, 0xFE, 0xFD, 0xFC]).Replace('+', '-').Replace('/', '_').TrimEnd('=')]; + } + + [Theory] + [MemberData(nameof(HostileCursors))] + public async Task A_hostile_cursor_is_a_400_and_never_a_query(string cursor) + { + var c = await Seed(); + + using var response = await Get(c, "?cursor=" + Uri.EscapeDataString(cursor)); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + } + + [Fact] + public async Task A_cursor_is_bound_to_the_sort_and_the_filter_it_was_issued_for() + { + var c = await Seed(); + var net = (await GetOk(c, "?limit=1")).GetProperty("next_cursor").GetString()!; + var unvoted = (await GetOk(c, "?limit=1&unvoted=network", client: "198.51.100.9")).GetProperty("next_cursor").GetString()!; + + using var otherSort = await Get(c, "?sort=total&cursor=" + net); + using var dropsFilter = await Get(c, "?cursor=" + unvoted); + using var addsFilter = await Get(c, "?unvoted=network&cursor=" + net, client: "198.51.100.9"); + using var same = await Get(c, "?limit=1&cursor=" + net); + + Assert.Equal(HttpStatusCode.BadRequest, otherSort.StatusCode); + Assert.Equal(HttpStatusCode.BadRequest, dropsFilter.StatusCode); + Assert.Equal(HttpStatusCode.BadRequest, addsFilter.StatusCode); + Assert.Equal(HttpStatusCode.OK, same.StatusCode); + } + + [Fact] + public async Task A_well_formed_cursor_past_every_row_is_an_empty_last_page() + { + var c = await Seed(); + var past = new RankCursor(RankOrder.Net, false, long.MinValue, "a").Encode(); + + var body = await GetOk(c, "?cursor=" + past); + + Assert.Empty(Keys(body)); + Assert.Equal(JsonValueKind.Null, body.GetProperty("next_cursor").ValueKind); + } + + [Theory] + [InlineData("Alpha")] + [InlineData("a_b")] + [InlineData("..")] + [InlineData("a%2Fb")] + [InlineData("a%0A")] + [InlineData("%27%3B--")] + [InlineData("nope")] + public async Task A_hostile_or_unknown_collection_is_a_404(string collection) + { + using var response = await Get(collection, ""); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task A_collection_over_the_key_grammar_is_a_404() + { + using var response = await Get(new string('a', 81), ""); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task Retired_subjects_are_left_out() + { + var c = await Seed(); + using (var scope = _factory.Services.CreateScope()) + { + var db = scope.ServiceProvider.GetRequiredService(); + (await db.ConceptSubjects.SingleAsync(x => x.Collection == c && x.Key == "bravo")).RetiredAtUtc = Epoch; + await db.SaveChangesAsync(); + } + + Assert.DoesNotContain("bravo", Keys(await GetOk(c, ""))); + } + + [Fact] + public async Task Unvoted_leaves_out_what_the_callers_network_voted_and_not_what_others_did() + { + var c = await Seed(); + + var fromVoter = Keys(await GetOk(c, "?unvoted=network", client: Voter)); + var fromStranger = Keys(await GetOk(c, "?unvoted=network", client: "198.51.100.77")); + + Assert.Equal(["charlie", "delta", "echo"], fromVoter.Order().ToArray()); + Assert.Equal(["alpha", "bravo", "charlie", "delta", "echo"], fromStranger.Order().ToArray()); + } + + [Fact] + public async Task Unvoted_pages_and_ranks_the_same_way() + { + var c = await Seed(); + + var walked = new List(); + string? cursor = null; + do + { + var body = await GetOk(c, "?unvoted=network&sort=newest&limit=1" + (cursor is null ? "" : "&cursor=" + cursor), client: "198.51.100.77"); + walked.AddRange(Keys(body)); + cursor = body.GetProperty("next_cursor").ValueKind == JsonValueKind.Null ? null : body.GetProperty("next_cursor").GetString(); + } + while (cursor is not null && walked.Count < 10); + + Assert.Equal(["echo", "delta", "charlie", "bravo", "alpha"], walked); + } + + [Fact] + public async Task Unvoted_without_a_client_address_filters_nothing_and_matches_no_untagged_row() + { + var c = await Seed(); + await CastUntagged(c, "echo", "ghost-device"); + + var keys = Keys(await GetOk(c, "?unvoted=network", client: null)); + + Assert.Equal(5, keys.Length); + } + + [Fact] + public async Task Unvoted_and_plain_responses_expose_no_tag_or_voter_identity() + { + var c = await Seed(); + string[] tags; + using (var scope = _factory.Services.CreateScope()) + { + tags = await scope.ServiceProvider.GetRequiredService() + .ConceptAppraisals.Where(x => x.NetworkTag != "").Select(x => x.NetworkTag).Distinct().ToArrayAsync(); + } + + Assert.NotEmpty(tags); + foreach (var query in new[] { "", "?unvoted=network", "?limit=2&unvoted=network" }) + { + using var response = await Get(c, query, client: Voter); + var text = await response.Content.ReadAsStringAsync(); + foreach (var forbidden in tags.Concat(["voter-", "device:", "account:", "networktag", "voterkey", Voter])) + { + Assert.DoesNotContain(forbidden, text, StringComparison.OrdinalIgnoreCase); + } + } + + // The cursor is position only: it carries no tag either. + var cursor = (await GetOk(c, "?limit=1&unvoted=network", client: Voter)).GetProperty("next_cursor").GetString()!; + var decoded = Encoding.UTF8.GetString(Microsoft.AspNetCore.WebUtilities.WebEncoders.Base64UrlDecode(cursor)); + foreach (var tag in tags) + { + Assert.DoesNotContain(tag, decoded); + } + } + + [Fact] + public async Task Nothing_in_the_request_can_name_a_tag_or_a_client() + { + var c = await Seed(); + using var client = _factory.CreateClient(); + using var request = new HttpRequestMessage(HttpMethod.Get, $"/public/concepts/{c}/ranked?unvoted=network"); + request.Headers.Add("X-Test-Anonymous", "true"); + request.Headers.Add("X-Test-Client", "198.51.100.77"); + request.Headers.Add("X-Network-Tag", "anything"); + request.Headers.Add("X-Forwarded-For", Voter); + + using var response = await client.SendAsync(request); + + // The test connection address wins; the forwarded header is the proxy's to set, not the request's. + Assert.Equal(5, Keys(JsonDocument.Parse(await response.Content.ReadAsStringAsync()).RootElement).Length); + } + + [Fact] + public async Task Cache_control_is_public_for_the_plain_read_and_never_cached_for_the_unvoted_one() + { + var c = await Seed(); + + using var plain = await Get(c, ""); + using var unvoted = await Get(c, "?unvoted=network", client: Voter); + + Assert.True(plain.Headers.CacheControl!.Public); + Assert.Equal(TimeSpan.FromSeconds(60), plain.Headers.CacheControl.MaxAge); + Assert.True(unvoted.Headers.CacheControl!.Private); + Assert.True(unvoted.Headers.CacheControl.NoStore); + Assert.False(unvoted.Headers.CacheControl.Public); + } + + [Fact] + public async Task Cors_and_vary_follow_the_tally_export_policy_and_the_read_sets_no_cookie_and_writes_nothing() + { + var c = await Seed(); + int rows, audits; + using (var scope = _factory.Services.CreateScope()) + { + var db = scope.ServiceProvider.GetRequiredService(); + rows = db.ConceptAppraisals.Count(); + audits = db.AuditEvents.Count(); + } + + using var allowed = await Get(c, "", origin: AllowedOrigin); + using var other = await Get(c, "", origin: "https://evil.example"); + using var bad = await Get(c, "?limit=0", origin: AllowedOrigin); + + Assert.Equal([AllowedOrigin], allowed.Headers.GetValues("Access-Control-Allow-Origin").ToArray()); + Assert.False(allowed.Headers.Contains("Access-Control-Allow-Credentials")); + Assert.False(other.Headers.Contains("Access-Control-Allow-Origin")); + foreach (var response in new[] { allowed, other, bad }) + { + Assert.Contains("Origin", response.Headers.Vary.ToArray()); + Assert.False(response.Headers.Contains("Set-Cookie")); + } + + using var after = _factory.Services.CreateScope(); + var dbAfter = after.ServiceProvider.GetRequiredService(); + Assert.Equal(rows, dbAfter.ConceptAppraisals.Count()); + Assert.Equal(audits, dbAfter.AuditEvents.Count()); + } + + // The network that cast the votes in Seed. + private const string Voter = "203.0.113.7"; + + // alpha 3 up 2 down (net 1, total 5), bravo 2 up (net 2, total 2), charlie 1 up 1 down (net 0, total 2), + // delta and echo unvoted (net 0, total 0). Registered alpha first, echo last. Voter's network cast alpha and + // bravo; charlie's votes came from a second network. + private async Task Seed() + { + var c = "c" + Guid.NewGuid().ToString("N"); + using (var scope = _factory.Services.CreateScope()) + { + var db = scope.ServiceProvider.GetRequiredService(); + var i = 0; + foreach (var key in new[] { "alpha", "bravo", "charlie", "delta", "echo" }) + { + db.ConceptSubjects.Add(new ConceptSubject { Collection = c, Key = key, Title = key.ToUpperInvariant(), RegisteredAtUtc = Epoch.AddDays(i++) }); + } + + await db.SaveChangesAsync(); + } + + await Cast(c, "alpha", "voter-1", ConceptStance.Up); + await Cast(c, "alpha", "voter-2", ConceptStance.Up); + await Cast(c, "alpha", "voter-3", ConceptStance.Up); + await Cast(c, "alpha", "voter-4", ConceptStance.Down); + await Cast(c, "alpha", "voter-5", ConceptStance.Down); + await Cast(c, "bravo", "voter-1", ConceptStance.Up); + await Cast(c, "bravo", "voter-2", ConceptStance.Up); + await Cast(c, "charlie", "other-1", ConceptStance.Up, "192.0.2.50"); + await Cast(c, "charlie", "other-2", ConceptStance.Down, "192.0.2.50"); + return c; + } + + private async Task Cast(string collection, string key, string device, ConceptStance stance, string client = Voter) + { + using var scope = _factory.Services.CreateScope(); + var voter = VoterIdentity.For(CurrentBifrostActor.Anonymous, device)!; + var result = await scope.ServiceProvider.GetRequiredService() + .CastAsync(collection, key, voter, stance, IPAddress.Parse(client), default); + Assert.Equal(CastResult.Recorded, result); + } + + private async Task CastUntagged(string collection, string key, string device) + { + using var scope = _factory.Services.CreateScope(); + var voter = VoterIdentity.For(CurrentBifrostActor.Anonymous, device)!; + await scope.ServiceProvider.GetRequiredService() + .CastAsync(collection, key, voter, ConceptStance.Up, null, default); + } + + private async Task Get(string collection, string query, string? client = null, string? origin = null) + { + using var http = _factory.CreateClient(); + using var request = new HttpRequestMessage(HttpMethod.Get, $"/public/concepts/{collection}/ranked{query}"); + request.Headers.Add("X-Test-Anonymous", "true"); + if (client is not null) + { + request.Headers.Add("X-Test-Client", client); + } + + if (origin is not null) + { + request.Headers.TryAddWithoutValidation("Origin", origin); + } + + return await http.SendAsync(request); + } + + private async Task GetOk(string collection, string query, string? client = null) + { + using var response = await Get(collection, query, client); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + return JsonDocument.Parse(await response.Content.ReadAsStringAsync()).RootElement.Clone(); + } + + private static string[] Keys(JsonElement body) => + body.GetProperty("items").EnumerateArray().Select(i => i.GetProperty("key").GetString()!).ToArray(); + + // The test host has no connection address; a header stands in for the one the proxy path would have resolved. + private sealed class ClientAddressFilter : IStartupFilter + { + public Action Configure(Action next) => app => + { + app.Use((context, nextMiddleware) => + { + if (context.Request.Headers.TryGetValue("X-Test-Client", out var address)) + { + context.Connection.RemoteIpAddress = IPAddress.Parse(address.ToString()); + } + + return nextMiddleware(context); + }); + next(app); + }; + } + + public sealed class RankedFactory : TestWebApplicationFactory + { + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + base.ConfigureWebHost(builder); + builder.ConfigureServices(services => services.AddTransient()); + builder.ConfigureAppConfiguration((_, configuration) => + configuration.AddInMemoryCollection(new Dictionary + { + ["ConceptVotes:AllowedOrigins:0"] = AllowedOrigin, + ["ConceptVotes:NetworkTagSecret"] = "ranked-test-tag-secret-0123456789-abcdef" + })); + } + } +} From 7e9ba5796ba0fe200edbe9459e8e557131fcf1df Mon Sep 17 00:00:00 2001 From: Heimdall Date: Wed, 11 Nov 2026 01:02:36 +0000 Subject: [PATCH 05/26] Desk: the fork desk's Bifrost half (#40) --- src/Bifrost.Web/Configuration/DeskOptions.cs | 68 + .../Features/Desk/DeskBodyShape.cs | 270 + .../Features/Desk/DeskDocuments.cs | 194 + .../Features/Desk/DeskEndpoints.cs | 445 + .../Features/Desk/DeskMindRoutes.cs | 84 + src/Bifrost.Web/Features/Desk/DeskPorts.cs | 67 + .../Features/Oidc/OidcEndpoints.cs | 12 +- .../Shared/StartupConfigurationValidator.cs | 105 +- src/Bifrost.Web/Program.cs | 17 + tests/Bifrost.Web.Tests/DeskAnswerTests.cs | 203 + tests/Bifrost.Web.Tests/DeskAuthCaseTests.cs | 49 + tests/Bifrost.Web.Tests/DeskBodyShapeTests.cs | 267 + tests/Bifrost.Web.Tests/DeskDocumentTests.cs | 67 + tests/Bifrost.Web.Tests/DeskHardeningTests.cs | 327 + tests/Bifrost.Web.Tests/DeskMindRouteTests.cs | 91 + .../Bifrost.Web.Tests/DeskMindRoutesTests.cs | 125 + tests/Bifrost.Web.Tests/DeskOptionsTests.cs | 234 + tests/Bifrost.Web.Tests/DeskReadTests.cs | 354 + tests/Bifrost.Web.Tests/DeskScenarioTests.cs | 188 + tests/Bifrost.Web.Tests/DeskSurfaceTests.cs | 149 + tests/Bifrost.Web.Tests/DeskVectorPinTests.cs | 18 + .../Fixtures/fork-card-vectors.json | 16970 ++++++++++++++++ tests/Bifrost.Web.Tests/HostTrustTests.cs | 3 +- tests/Bifrost.Web.Tests/Support/DeskReplay.cs | 75 + .../Bifrost.Web.Tests/Support/DeskVectors.cs | 50 + .../Support/DeskWebApplicationFactory.cs | 182 + .../Support/FakeDeskCredential.cs | 42 + .../Bifrost.Web.Tests/Support/FakeDeskDoor.cs | 125 + 28 files changed, 20775 insertions(+), 6 deletions(-) create mode 100644 src/Bifrost.Web/Configuration/DeskOptions.cs create mode 100644 src/Bifrost.Web/Features/Desk/DeskBodyShape.cs create mode 100644 src/Bifrost.Web/Features/Desk/DeskDocuments.cs create mode 100644 src/Bifrost.Web/Features/Desk/DeskEndpoints.cs create mode 100644 src/Bifrost.Web/Features/Desk/DeskMindRoutes.cs create mode 100644 src/Bifrost.Web/Features/Desk/DeskPorts.cs create mode 100644 tests/Bifrost.Web.Tests/DeskAnswerTests.cs create mode 100644 tests/Bifrost.Web.Tests/DeskAuthCaseTests.cs create mode 100644 tests/Bifrost.Web.Tests/DeskBodyShapeTests.cs create mode 100644 tests/Bifrost.Web.Tests/DeskDocumentTests.cs create mode 100644 tests/Bifrost.Web.Tests/DeskHardeningTests.cs create mode 100644 tests/Bifrost.Web.Tests/DeskMindRouteTests.cs create mode 100644 tests/Bifrost.Web.Tests/DeskMindRoutesTests.cs create mode 100644 tests/Bifrost.Web.Tests/DeskOptionsTests.cs create mode 100644 tests/Bifrost.Web.Tests/DeskReadTests.cs create mode 100644 tests/Bifrost.Web.Tests/DeskScenarioTests.cs create mode 100644 tests/Bifrost.Web.Tests/DeskSurfaceTests.cs create mode 100644 tests/Bifrost.Web.Tests/DeskVectorPinTests.cs create mode 100644 tests/Bifrost.Web.Tests/Fixtures/fork-card-vectors.json create mode 100644 tests/Bifrost.Web.Tests/Support/DeskReplay.cs create mode 100644 tests/Bifrost.Web.Tests/Support/DeskVectors.cs create mode 100644 tests/Bifrost.Web.Tests/Support/DeskWebApplicationFactory.cs create mode 100644 tests/Bifrost.Web.Tests/Support/FakeDeskCredential.cs create mode 100644 tests/Bifrost.Web.Tests/Support/FakeDeskDoor.cs diff --git a/src/Bifrost.Web/Configuration/DeskOptions.cs b/src/Bifrost.Web/Configuration/DeskOptions.cs new file mode 100644 index 0000000..688d098 --- /dev/null +++ b/src/Bifrost.Web/Configuration/DeskOptions.cs @@ -0,0 +1,68 @@ +namespace Bifrost.Web.Configuration; + +// The fork desk's configuration (CultLib fork-card.md, Transport; M1 and M5). Minds is an ARRAY, never a map keyed +// by Mind id: configuration keys are case-insensitive, so a keyed section would let GAMECULT-CHAT reach +// gamecult-chat. Account ids are lowercase D-format Guids. An empty Minds list means the desk is not configured. +public sealed class DeskOptions +{ + public const string SectionName = "Desk"; + + public List Minds { get; init; } = []; + + public DeskAttributionOptions Attribution { get; init; } = new(); + + // The one origin allowed to call /api/desk from a browser (exactly this, no credentials). + public string AllowedOrigin { get; init; } = string.Empty; + + // The cap on an answer body, in bytes, judged before it is parsed. + public int MaxBodyBytes { get; init; } = 16 * 1024; + + // What an `unavailable` document tells the client to wait. + public int RetryAfterSeconds { get; init; } = 30; + + public bool IsConfigured => Minds.Count > 0; +} + +public sealed class DeskMindOptions +{ + public string Id { get; init; } = string.Empty; + + // The full path of this Mind's socket on the peer-checked door. Bifrost only connects to it. + public string Socket { get; init; } = string.Empty; + + // Accounts that may answer this Mind's forks. Sent to Huginn with every call; no request sets or extends it. + public List Deciders { get; init; } = []; + + // Huginn's bound on the list a call carries (fork-card.md: one to 64 ids). + public const int MaxDeciders = 64; + + // One to 64 ids, none repeated. The startup validator and the request path both ask this one question. + public static bool DecidersWithinBounds(IReadOnlyCollection deciders) => + deciders.Count is >= 1 and <= MaxDeciders && deciders.Distinct(StringComparer.Ordinal).Count() == deciders.Count; +} + +// The authority a stamped answer carries (fork-card.md: Operator, Steward or Member), from configuration because +// Bifrost has no such roles: Operator is one account, Stewards a list, everyone else a Member. +public sealed class DeskAttributionOptions +{ + public string? Operator { get; init; } + + public List Stewards { get; init; } = []; + + public string AuthorityOf(string accountId) + { + if (!string.IsNullOrEmpty(Operator) && string.Equals(accountId, Operator, StringComparison.Ordinal)) + { + return DeskAuthority.Operator; + } + + return Stewards.Contains(accountId, StringComparer.Ordinal) ? DeskAuthority.Steward : DeskAuthority.Member; + } +} + +public static class DeskAuthority +{ + public const string Operator = "Operator"; + public const string Steward = "Steward"; + public const string Member = "Member"; +} diff --git a/src/Bifrost.Web/Features/Desk/DeskBodyShape.cs b/src/Bifrost.Web/Features/Desk/DeskBodyShape.cs new file mode 100644 index 0000000..b5ee4f9 --- /dev/null +++ b/src/Bifrost.Web/Features/Desk/DeskBodyShape.cs @@ -0,0 +1,270 @@ +using System.Text; +using System.Text.Json; +using System.Text.RegularExpressions; + +namespace Bifrost.Web.Features.Desk; + +// The edge's judgment of an answer body (fork-card.md, answer_request, steps 1 and 2). It checks the shape only: +// whether the answer is legal (a retraction's target, the choice, the fork) is Huginn's. A body that passes is +// forwarded as the bytes received; the parsed request is returned for the door's context and for tests. +public sealed record DeskBodyVerdict(AnswerRequest? Request, string? RefusalCode) +{ + public static DeskBodyVerdict Pass(AnswerRequest request) => new(request, null); + + public static DeskBodyVerdict Refuse(string code) => new(null, code); +} + +public static partial class DeskBodyShape +{ + public const int MaxNoteBytes = 4000; + + // Two keys the receiver alone stamps. Refused whole at any depth, never stripped. + private static readonly string[] ForbiddenKeys = ["answerer", "authority"]; + + private static readonly string[] TopKeys = + ["schemaVersion", "fork_id", "options_hash", "answer", "retracted", "retracts", "answered_at", "request_id"]; + + private static readonly string[] AnswerKeys = ["choice", "note"]; + + private static readonly UTF8Encoding StrictUtf8 = new(false, true); + + private static readonly JsonDocumentOptions ParseOptions = new() { MaxDepth = 32 }; + + public static DeskBodyVerdict Judge(ReadOnlySpan body, int maxBodyBytes) + { + if (body.Length > maxBodyBytes) + { + return DeskBodyVerdict.Refuse(DeskRefusals.BodyInvalid); + } + + JsonDocument document; + try + { + document = JsonDocument.Parse(body.ToArray(), ParseOptions); + } + catch (JsonException) + { + return DeskBodyVerdict.Refuse(DeskRefusals.BodyInvalid); + } + + using (document) + { + try + { + return JudgeParsed(document.RootElement); + } + catch (InvalidOperationException) + { + // A string the parser accepted but cannot decode: invalid UTF-8 or a lone surrogate. + return DeskBodyVerdict.Refuse(DeskRefusals.BodyInvalid); + } + } + } + + private static DeskBodyVerdict JudgeParsed(JsonElement root) + { + if (HasForbiddenKey(root)) + { + return DeskBodyVerdict.Refuse(DeskRefusals.BodyForbiddenField); + } + + if (root.ValueKind != JsonValueKind.Object) + { + return DeskBodyVerdict.Refuse(DeskRefusals.BodyInvalid); + } + + var top = Members(root); + if (top is null) + { + return DeskBodyVerdict.Refuse(DeskRefusals.BodyInvalid); + } + + // Unknown keys, including inside the answer object, before any value is judged. + if (top.Keys.Any(key => !TopKeys.Contains(key, StringComparer.Ordinal))) + { + return DeskBodyVerdict.Refuse(DeskRefusals.BodyUnknownField); + } + + Dictionary? answerMembers = null; + if (top.TryGetValue("answer", out var answerElement) && answerElement.ValueKind == JsonValueKind.Object) + { + answerMembers = Members(answerElement); + if (answerMembers is not null && + answerMembers.Keys.Any(key => !AnswerKeys.Contains(key, StringComparer.Ordinal))) + { + return DeskBodyVerdict.Refuse(DeskRefusals.BodyUnknownField); + } + } + + return Shape(top, answerMembers); + } + + private static DeskBodyVerdict Shape(Dictionary top, Dictionary? answerMembers) + { + var invalid = DeskBodyVerdict.Refuse(DeskRefusals.BodyInvalid); + + // Map-mode: every key is written. + if (TopKeys.Any(key => !top.ContainsKey(key))) + { + return invalid; + } + + if (!IsString(top["schemaVersion"], out var schemaVersion) || schemaVersion != DeskSchemas.AnswerRequest || + !IsString(top["fork_id"], out var forkId) || forkId.Length == 0 || + !IsString(top["options_hash"], out var optionsHash) || !Hex64().IsMatch(optionsHash) || + !IsString(top["answered_at"], out var answeredAt) || !Instant().IsMatch(answeredAt) || + !IsString(top["request_id"], out var requestId) || !RequestId().IsMatch(requestId)) + { + return invalid; + } + + var retractedElement = top["retracted"]; + if (retractedElement.ValueKind is not (JsonValueKind.True or JsonValueKind.False)) + { + return invalid; + } + + var retracted = retractedElement.ValueKind == JsonValueKind.True; + + string? retracts = null; + if (top["retracts"].ValueKind != JsonValueKind.Null) + { + if (!IsString(top["retracts"], out var target) || !AnswerId().IsMatch(target)) + { + return invalid; + } + + retracts = target; + } + + AnswerObject? answer = null; + if (top["answer"].ValueKind != JsonValueKind.Null) + { + if (answerMembers is null || AnswerKeys.Any(key => !answerMembers.ContainsKey(key))) + { + return invalid; + } + + string? choice = null; + if (answerMembers["choice"].ValueKind != JsonValueKind.Null) + { + if (!IsString(answerMembers["choice"], out var picked) || picked.Length == 0) + { + return invalid; + } + + choice = picked; + } + + string? note = null; + if (answerMembers["note"].ValueKind != JsonValueKind.Null) + { + if (!IsString(answerMembers["note"], out var words) || !IsWords(words)) + { + return invalid; + } + + note = words; + } + + if (choice is null && note is null) + { + return invalid; + } + + answer = new AnswerObject(choice, note); + } + + // No answer and no retraction is no request. The illegal retraction forms (a target-less retraction, a + // retraction carrying an answer, a target on a plain answer) pass here: Huginn stores them as bad_retraction. + if (answer is null && !retracted) + { + return invalid; + } + + return DeskBodyVerdict.Pass(new AnswerRequest( + schemaVersion, forkId, optionsHash, answer, retracted, retracts, answeredAt, requestId)); + } + + // A note is 1 to 4000 UTF-8 bytes and not whitespace only. Bytes, not characters or UTF-16 units. + private static bool IsWords(string value) + { + int bytes; + try + { + bytes = StrictUtf8.GetByteCount(value); + } + catch (EncoderFallbackException) + { + return false; + } + + return bytes is >= 1 and <= MaxNoteBytes && !value.All(IsSchemaSpace); + } + + // The whitespace set of the schema's `words` pattern. + private static bool IsSchemaSpace(char c) => + c is (>= (char)0x09 and <= (char)0x0d) or (char)0x20 or (char)0x85 or (char)0xa0 or (char)0x1680 + or (>= (char)0x2000 and <= (char)0x200a) or (char)0x2028 or (char)0x2029 or (char)0x202f or (char)0x205f or (char)0x3000; + + private static bool IsString(JsonElement element, out string value) + { + value = string.Empty; + if (element.ValueKind != JsonValueKind.String) + { + return false; + } + + value = element.GetString() ?? string.Empty; + return true; + } + + // Object members by unescaped name; null when a name repeats, because the forwarded bytes would then mean + // different things to different parsers. + private static Dictionary? Members(JsonElement element) + { + var members = new Dictionary(StringComparer.Ordinal); + foreach (var property in element.EnumerateObject()) + { + if (!members.TryAdd(property.Name, property.Value)) + { + return null; + } + } + + return members; + } + + private static bool HasForbiddenKey(JsonElement element) + { + switch (element.ValueKind) + { + case JsonValueKind.Object: + foreach (var property in element.EnumerateObject()) + { + if (ForbiddenKeys.Contains(property.Name, StringComparer.Ordinal) || HasForbiddenKey(property.Value)) + { + return true; + } + } + + return false; + case JsonValueKind.Array: + return element.EnumerateArray().Any(HasForbiddenKey); + default: + return false; + } + } + + [GeneratedRegex(@"^[0-9a-f]{64}\z")] + private static partial Regex Hex64(); + + [GeneratedRegex(@"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,7})?Z\z")] + private static partial Regex Instant(); + + [GeneratedRegex(@"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z")] + private static partial Regex RequestId(); + + [GeneratedRegex(@"^n[1-9][0-9]*\z")] + private static partial Regex AnswerId(); +} diff --git a/src/Bifrost.Web/Features/Desk/DeskDocuments.cs b/src/Bifrost.Web/Features/Desk/DeskDocuments.cs new file mode 100644 index 0000000..aaa652f --- /dev/null +++ b/src/Bifrost.Web/Features/Desk/DeskDocuments.cs @@ -0,0 +1,194 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Bifrost.Web.Features.Desk; + +// The fork-desk documents Bifrost reads or writes, spelled as CultLib's contracts/cultnet/governance/fork-card.md +// (commit 89755b08) spells them: snake_case keys, every key written (nil where absent), declared in the +// contract's key order. Bifrost authors only DeskRefusal and DeskUnavailable. AnswerRequest is the client's body; +// ForkCard, DeskAnswerDocument and AnswerResult are Huginn's documents, typed here so they pass through unchanged. + +public static class DeskSchemas +{ + public const string AnswerRequest = "gamecult.fork.answer_request.v0"; + public const string AnswerResult = "gamecult.fork.answer_result.v0"; + public const string Card = "gamecult.fork.card.v0"; + public const string DeskAnswer = "gamecult.desk_answer.v0"; + public const string Refusal = "gamecult.fork.refusal.v0"; + public const string Unavailable = "gamecult.fork.unavailable.v0"; +} + +public static class DeskJson +{ + // Defaults write nulls, which map-mode needs ("every key written"). + public static readonly JsonSerializerOptions Options = new() + { + // The documents are not HTML: keep apostrophes and non-ASCII text as written. + Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + }; +} + +public sealed record AnswerObject( + [property: JsonPropertyName("choice")] string? Choice, + [property: JsonPropertyName("note")] string? Note); + +public sealed record AnswerRequest( + [property: JsonPropertyName("schemaVersion")] string SchemaVersion, + [property: JsonPropertyName("fork_id")] string ForkId, + [property: JsonPropertyName("options_hash")] string OptionsHash, + [property: JsonPropertyName("answer")] AnswerObject? Answer, + [property: JsonPropertyName("retracted")] bool Retracted, + [property: JsonPropertyName("retracts")] string? Retracts, + [property: JsonPropertyName("answered_at")] string AnsweredAt, + [property: JsonPropertyName("request_id")] string RequestId); + +public sealed record DeskRefusal( + [property: JsonPropertyName("schemaVersion")] string SchemaVersion, + [property: JsonPropertyName("code")] string Code, + [property: JsonPropertyName("error")] string Error); + +public sealed record DeskUnavailable( + [property: JsonPropertyName("schemaVersion")] string SchemaVersion, + [property: JsonPropertyName("code")] string Code, + [property: JsonPropertyName("error")] string Error, + [property: JsonPropertyName("retryAfterSeconds")] int? RetryAfterSeconds); + +public sealed record ForkOption( + [property: JsonPropertyName("id")] string Id, + [property: JsonPropertyName("text")] string Text); + +public sealed record ForkRef( + [property: JsonPropertyName("kind")] string Kind, + [property: JsonPropertyName("id")] string Id); + +public sealed record ForkCard( + [property: JsonPropertyName("schemaVersion")] string SchemaVersion, + [property: JsonPropertyName("fork_id")] string ForkId, + [property: JsonPropertyName("title")] string Title, + [property: JsonPropertyName("context")] string Context, + [property: JsonPropertyName("options")] ForkOption[] Options, + [property: JsonPropertyName("recommendation")] string Recommendation, + [property: JsonPropertyName("depends")] string[] Depends, + [property: JsonPropertyName("leans")] string? Leans, + [property: JsonPropertyName("flag")] string? Flag, + [property: JsonPropertyName("raised_in")] ForkRef? RaisedIn, + [property: JsonPropertyName("deciders")] string[] Deciders, + [property: JsonPropertyName("status")] string Status, + [property: JsonPropertyName("ruling")] string? Ruling, + [property: JsonPropertyName("ruled_from")] string[] RuledFrom, + [property: JsonPropertyName("order")] long Order, + [property: JsonPropertyName("options_hash")] string OptionsHash); + +// One stored answer: Counted, Revision, Retraction or Refused, told apart by Outcome and Code. +public sealed record DeskAnswerDocument( + [property: JsonPropertyName("schemaVersion")] string SchemaVersion, + [property: JsonPropertyName("answer_id")] string AnswerId, + [property: JsonPropertyName("fork_id")] string ForkId, + [property: JsonPropertyName("outcome")] string Outcome, + [property: JsonPropertyName("of")] string? Of, + [property: JsonPropertyName("retracts")] string? Retracts, + [property: JsonPropertyName("code")] string? Code, + [property: JsonPropertyName("options_hash")] string OptionsHash, + [property: JsonPropertyName("choice")] string? Choice, + [property: JsonPropertyName("note")] string? Note, + [property: JsonPropertyName("answered_at")] string AnsweredAt, + [property: JsonPropertyName("received_at")] string ReceivedAt, + [property: JsonPropertyName("claim_in_future")] bool ClaimInFuture, + [property: JsonPropertyName("answerer")] string Answerer, + [property: JsonPropertyName("authority")] string Authority, + [property: JsonPropertyName("request_id")] string RequestId); + +public sealed record ForkRecord( + [property: JsonPropertyName("schemaVersion")] string SchemaVersion, + [property: JsonPropertyName("fork_id")] string ForkId, + [property: JsonPropertyName("options_hash")] string OptionsHash, + [property: JsonPropertyName("counted_answer_id")] string? CountedAnswerId, + [property: JsonPropertyName("answers")] DeskAnswerDocument[] Answers); + +public sealed record ForkEntry( + [property: JsonPropertyName("schemaVersion")] string SchemaVersion, + [property: JsonPropertyName("card")] ForkCard Card, + [property: JsonPropertyName("record")] ForkRecord Record); + +public sealed record ForkList( + [property: JsonPropertyName("schemaVersion")] string SchemaVersion, + [property: JsonPropertyName("mind")] string Mind, + [property: JsonPropertyName("stamp")] string Stamp, + [property: JsonPropertyName("open_count")] int OpenCount, + [property: JsonPropertyName("recorded_count")] int RecordedCount, + [property: JsonPropertyName("forks")] ForkEntry[] Forks, + [property: JsonPropertyName("next_cursor")] string? NextCursor); + +public sealed record AnswerResult( + [property: JsonPropertyName("schemaVersion")] string SchemaVersion, + [property: JsonPropertyName("answer")] DeskAnswerDocument Answer, + [property: JsonPropertyName("card")] ForkCard? Card, + [property: JsonPropertyName("counted_answer_id")] string? CountedAnswerId); + +// The refusal codes Bifrost itself writes, each with its one status and message (vectors doorRefusals). +public static class DeskRefusals +{ + public const string Unauthenticated = "unauthenticated"; + public const string BodyForbiddenField = "body_forbidden_field"; + public const string BodyUnknownField = "body_unknown_field"; + public const string BodyInvalid = "body_invalid"; + public const string ForkUnknown = "fork_unknown"; + public const string InsufficientScope = "insufficient_scope"; + public const string Forbidden = "forbidden"; + public const string RequestInvalid = "request_invalid"; + public const string RequestIdConflict = "request_id_conflict"; + + private static readonly Dictionary Table = new(StringComparer.Ordinal) + { + [Unauthenticated] = (401, "unauthenticated: a verified session is required"), + [BodyForbiddenField] = (400, "body_forbidden_field: answerer and authority are set by the receiver"), + [BodyUnknownField] = (400, "body_unknown_field: the body is a closed shape"), + [BodyInvalid] = (400, "body_invalid: the body does not satisfy gamecult.fork.answer_request.v0"), + [ForkUnknown] = (404, "fork_unknown: no such fork"), + [InsufficientScope] = (403, "insufficient_scope: the token lacks the scope this route needs"), + [Forbidden] = (403, "forbidden: only Active members may use the desk"), + [RequestInvalid] = (400, "request_invalid: the request does not satisfy its document"), + [RequestIdConflict] = (409, "request_id_conflict: this request_id was used for a different body"), + }; + + // fork_too_large (Libby's ruling) is list-only and names its fork, so it is built here, not read from the table. + // Not a literal const: the pinned vectors' doorRefusals do not name it yet, and the schema test pins literals to them. + public static string ForkTooLarge => "fork_too_large"; + + public static int ForkTooLargeStatus => 422; + + public static DeskRefusal ForkTooLargeDocument(string forkId, int order) => new( + DeskSchemas.Refusal, + ForkTooLarge, + order > 0 + ? $"fork_too_large: fork {forkId} (order {order}) is too large for a page; continue with cursor c{order}" + : $"fork_too_large: fork {forkId} (order {order}) is too large for a page; the list has reached its end"); + + public static bool IsKnown(string code) => Table.ContainsKey(code); + + public static int StatusOf(string code) => Table[code].Status; + + public static DeskRefusal Document(string code) => new(DeskSchemas.Refusal, code, Table[code].Error); +} + +public static class DeskUnavailables +{ + public const int Status = 503; + + public static DeskUnavailable Document(int? retryAfterSeconds) => new( + DeskSchemas.Unavailable, + "unavailable", + "unavailable: the outcome is not confirmed, try again with the same request_id", + retryAfterSeconds); +} + +// Notes are returned only to deciders. For everyone else Bifrost nils every note at the door: the key stays written +// (every key written, nil where absent), so the stripped body is still the contract's shape. Cards, outcomes, codes +// and counts are untouched; Huginn returns the full record. +public static class DeskNotes +{ + public static ForkEntry Strip(ForkEntry entry) => + entry with { Record = entry.Record with { Answers = [.. entry.Record.Answers.Select(a => a with { Note = null })] } }; + + public static ForkList Strip(ForkList list) => list with { Forks = [.. list.Forks.Select(Strip)] }; +} diff --git a/src/Bifrost.Web/Features/Desk/DeskEndpoints.cs b/src/Bifrost.Web/Features/Desk/DeskEndpoints.cs new file mode 100644 index 0000000..7f5cfff --- /dev/null +++ b/src/Bifrost.Web/Features/Desk/DeskEndpoints.cs @@ -0,0 +1,445 @@ +using System.Text.Json; +using Bifrost.Web.Configuration; +using Bifrost.Web.Data; +using Bifrost.Web.Features.Oidc; +using Microsoft.AspNetCore.Cors.Infrastructure; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Options; +using Microsoft.Net.Http.Headers; + +namespace Bifrost.Web.Features.Desk; + +// The fork desk's HTTP edge (fork-card.md, Transport). One catch-all under /api/desk judges, in this order and no +// other: the raw target, the bearer, its audience, its scope, Active membership (the body is unread), the body's +// shape, the {mind} lookup, the door. Every refusal is a contract document. Bifrost holds no fork state: it passes +// Huginn's outcome through, attaches the current card to a stale_fork answer, and says `unavailable` when the door +// cannot confirm an outcome. +public static class DeskEndpoints +{ + public const string RoutePrefix = "/api/desk"; + public const string Audience = "bifrost-desk"; + public const string ReadScope = "desk.read"; + public const string AnswerScope = "desk.answer"; + public const string CorsPolicy = "DeskCors"; + + public static IServiceCollection AddBifrostDesk(this IServiceCollection services) + { + services.AddSingleton(provider => new DeskMindRoutes(provider.GetRequiredService>().Value.Minds)); + services.AddCors(); + // Exactly the configured origin, no credentials, and only on the routes that require this policy. + services + .AddOptions() + .Configure>((cors, desk) => + cors.AddPolicy( + CorsPolicy, + policy => policy + .WithOrigins(desk.Value.AllowedOrigin) + .WithMethods("GET", "POST") + .WithHeaders(HeaderNames.Authorization, HeaderNames.ContentType))); + return services; + } + + public static IEndpointConventionBuilder MapDesk(this IEndpointRouteBuilder app) + { + // A configured desk with no credential or door port would serve nothing true: stop startup instead. + app.ServiceProvider.GetRequiredService(); + app.ServiceProvider.GetRequiredService(); + + return app.MapMethods(RoutePrefix + "/{**rest}", ["GET", "POST"], HandleAsync) + .RequireCors(CorsPolicy) + .AllowAnonymous(); + } + + private static async Task HandleAsync( + HttpContext http, + IDeskCredential credential, + IDeskDoor door, + DeskMindRoutes minds, + IOptions options, + BifrostDbContext db) + { + var cancellationToken = http.RequestAborted; + var desk = options.Value; + http.Response.Headers.Append(HeaderNames.Vary, "Origin"); + http.Response.Headers.CacheControl = "no-store"; + + // 1. The raw target, never the decoded Request.Path. + var rawTarget = http.Features.Get()?.RawTarget ?? string.Empty; + var route = DeskRoute.Parse(http.Request.Method, rawTarget); + if (route is null) + { + await WriteRefusalAsync(http, DeskRefusals.ForkUnknown); + return; + } + + // 2-4. Bearer, audience, scope. + DeskToken? token; + try + { + token = await credential.AuthenticateAsync(http, cancellationToken); + } + catch (Exception) when (!cancellationToken.IsCancellationRequested) + { + // The bearer could not be judged: neither a pass nor a refusal of the caller. + await WriteUnavailableAsync(http, desk); + return; + } + + if (token is null || !string.Equals(token.Audience, Audience, StringComparison.Ordinal)) + { + await WriteRefusalAsync(http, DeskRefusals.Unauthenticated); + return; + } + + var scope = route.Kind == DeskRouteKind.Answer ? AnswerScope : ReadScope; + if (!token.Scopes.Contains(scope, StringComparer.Ordinal)) + { + await WriteRefusalAsync(http, DeskRefusals.InsufficientScope); + return; + } + + // 5. Active membership, re-read now; the body has not been touched. + Guid? accountId; + try + { + accountId = await OidcEndpoints.ActiveMemberIdAsync(db, token.Subject, cancellationToken); + } + catch (Exception) when (!cancellationToken.IsCancellationRequested) + { + await WriteUnavailableAsync(http, desk); + return; + } + + if (accountId is null) + { + await WriteRefusalAsync(http, DeskRefusals.Forbidden); + return; + } + + // 6. The request's shape. An answer's bytes are the bytes forwarded; list and read judge their query. + byte[] body = []; + DeskListRequest? listRequest = null; + var forkId = string.Empty; + if (route.Kind == DeskRouteKind.Answer) + { + var read = await ReadBodyAsync(http.Request, desk.MaxBodyBytes, cancellationToken); + var verdict = read is null + ? DeskBodyVerdict.Refuse(DeskRefusals.BodyInvalid) + : DeskBodyShape.Judge(read, desk.MaxBodyBytes); + if (verdict.RefusalCode is not null) + { + await WriteRefusalAsync(http, verdict.RefusalCode); + return; + } + + body = read!; + } + else if (route.Kind == DeskRouteKind.Read + ? !DeskQuery.TryRead(route.RawQuery, route.RawFork!, out forkId) + : !DeskQuery.TryList(route.RawQuery, string.Empty, out listRequest)) + { + await WriteRefusalAsync(http, DeskRefusals.RequestInvalid); + return; + } + + // 7. The {mind} lookup: a miss is the same document as an unknown fork. + var mind = minds.Resolve(route.RawMind); + if (mind is null) + { + await WriteRefusalAsync(http, DeskRefusals.ForkUnknown); + return; + } + + // 8-10. The door. A decider list outside 1..64 unique ids is a configuration fault: nothing is sent. + if (!DeskMindOptions.DecidersWithinBounds(mind.Deciders)) + { + await WriteUnavailableAsync(http, desk); + return; + } + + var subject = accountId.Value.ToString("D"); + var context = new DeskDoorContext(mind.Id, desk.Attribution.AuthorityOf(subject), subject, mind.Deciders.ToArray()); + try + { + switch (route.Kind) + { + case DeskRouteKind.List: + var listed = await door.ListAsync(mind.Socket, context, listRequest! with { Mind = mind.Id }, cancellationToken); + if (listed is DeskListOutcome.Listed(var list)) + { + await WriteJsonAsync(http, 200, IsDecider(context) ? list : DeskNotes.Strip(list)); + } + else if (listed is DeskListOutcome.ForkTooLarge(var largeId, var largeOrder)) + { + await WriteJsonAsync(http, DeskRefusals.ForkTooLargeStatus, DeskRefusals.ForkTooLargeDocument(largeId, largeOrder)); + } + else if (listed is DeskListOutcome.NotListed(var listCode) && DoorMayRefuse(listCode) && listCode != DeskRefusals.RequestIdConflict) + { + await WriteRefusalAsync(http, ReadListCode(listCode)); + } + else + { + await WriteUnavailableAsync(http, desk); + } + + return; + case DeskRouteKind.Read: + var found = await door.ReadAsync(mind.Socket, context, forkId, cancellationToken); + if (found is DeskReadOutcome.Found(var entry)) + { + await WriteJsonAsync(http, 200, IsDecider(context) ? entry : DeskNotes.Strip(entry)); + } + else if (found is DeskReadOutcome.NotFound(var readCode) && DoorMayRefuse(readCode) && readCode != DeskRefusals.RequestIdConflict) + { + await WriteRefusalAsync(http, ReadListCode(readCode)); + } + else + { + await WriteUnavailableAsync(http, desk); + } + + return; + default: + await AnswerAsync(http, door, mind, context, body, desk); + return; + } + } + catch (Exception) when (!cancellationToken.IsCancellationRequested) + { + await WriteUnavailableAsync(http, desk); + } + } + + private static async Task AnswerAsync( + HttpContext http, IDeskDoor door, DeskMindOptions mind, DeskDoorContext context, byte[] body, DeskOptions desk) + { + var cancellationToken = http.RequestAborted; + var outcome = await door.AnswerAsync(mind.Socket, context, body, cancellationToken); + switch (outcome) + { + case DeskAnswerOutcome.NotStored(var code) when DoorMayRefuse(code): + await WriteRefusalAsync(http, code); + return; + case DeskAnswerOutcome.Answered(var result) when DeskStatus.Of(result) is { } status: + // A stale_fork answer, fresh or replayed, carries the CURRENT card: nothing is stored for it. + if (string.Equals(result.Answer.Code, DeskStatus.StaleFork, StringComparison.Ordinal)) + { + var current = await door.ReadAsync(mind.Socket, context, result.Answer.ForkId, cancellationToken); + if (current is not DeskReadOutcome.Found(var entry)) + { + await WriteUnavailableAsync(http, desk); + return; + } + + result = result with { Card = entry.Card }; + } + + await WriteJsonAsync(http, status, result); + return; + default: + await WriteUnavailableAsync(http, desk); + return; + } + } + + // A note is a decider's own words: only a caller on the admin-set list reads them (visibility ruling c). + private static bool IsDecider(DeskDoorContext context) => + context.Deciders.Contains(context.Subject, StringComparer.Ordinal); + + // Codes the door may hand back for nothing stored. The three the edge alone judges are never the door's. + private static bool DoorMayRefuse(string code) => + code is not (DeskRefusals.Unauthenticated or DeskRefusals.InsufficientScope or DeskRefusals.Forbidden) && + DeskRefusals.IsKnown(code); + + // request_id_conflict is the answer's alone (Libby): a read or list carries no request_id, so the door saying it + // there is a door fault and the edge says `unavailable`. + // A read or list sends no body. A door that refuses one for its body (an `authority` field among them) is + // refusing the request, which is request_invalid; it is not an outage. + private static string ReadListCode(string code) => + code is DeskRefusals.BodyForbiddenField or DeskRefusals.BodyUnknownField or DeskRefusals.BodyInvalid + ? DeskRefusals.RequestInvalid + : code; + + // At most `cap` bytes. Null when the body is longer, judged by Content-Length first and then while reading. + private static async Task ReadBodyAsync(HttpRequest request, int cap, CancellationToken cancellationToken) + { + if (request.ContentLength > cap) + { + return null; + } + + using var buffer = new MemoryStream(); + var chunk = new byte[4096]; + int count; + while ((count = await request.Body.ReadAsync(chunk, cancellationToken)) > 0) + { + if (buffer.Length + count > cap) + { + return null; + } + + buffer.Write(chunk, 0, count); + } + + return buffer.ToArray(); + } + + private static Task WriteRefusalAsync(HttpContext http, string code) => + WriteJsonAsync(http, DeskRefusals.StatusOf(code), DeskRefusals.Document(code)); + + private static async Task WriteUnavailableAsync(HttpContext http, DeskOptions desk) + { + http.Response.Headers.RetryAfter = desk.RetryAfterSeconds.ToString(System.Globalization.CultureInfo.InvariantCulture); + await WriteJsonAsync(http, DeskUnavailables.Status, DeskUnavailables.Document(desk.RetryAfterSeconds)); + } + + private static async Task WriteJsonAsync(HttpContext http, int status, T document) + { + http.Response.StatusCode = status; + http.Response.ContentType = "application/json; charset=utf-8"; + await JsonSerializer.SerializeAsync(http.Response.Body, document, DeskJson.Options, http.RequestAborted); + } +} + +// The one place a stored outcome becomes an HTTP status. fork-card.md prose says 200 for any stored outcome; the +// vectors (results[].httpStatus) say a Refused answer carries its refusal's status, and the vectors are the check. +public static class DeskStatus +{ + public const string StaleFork = "stale_fork"; + + private static readonly Dictionary RefusedStatus = new(StringComparer.Ordinal) + { + [StaleFork] = 409, + ["not_a_decider"] = 403, + ["fork_closed"] = 409, + ["choice_invalid"] = 400, + ["bad_retraction"] = 400, + ["already_retracted"] = 409, + }; + + // Null when the door sent an outcome or a refusal code this contract does not name: Bifrost then cannot vouch + // for it and says `unavailable`. + public static int? Of(AnswerResult result) => result.Answer.Outcome switch + { + "Counted" or "Revision" or "Retraction" => 200, + "Refused" when result.Answer.Code is { } code && RefusedStatus.TryGetValue(code, out var status) => status, + _ => null, + }; +} + +public enum DeskRouteKind +{ + List, + Read, + Answer, +} + +public sealed record DeskRoute(DeskRouteKind Kind, string RawMind, string? RawFork, string RawQuery) +{ + // The raw request target against the three routes. Segments stay percent-encoded: {mind} is decoded once, by + // the resolver, and nothing here normalises a dot segment. Any other shape is not a desk route. + public static DeskRoute? Parse(string method, string rawTarget) + { + var path = rawTarget; + var query = string.Empty; + var split = path.IndexOf('?'); + if (split >= 0) + { + query = path[(split + 1)..]; + path = path[..split]; + } + + if (!path.StartsWith(DeskEndpoints.RoutePrefix + "/", StringComparison.Ordinal)) + { + return null; + } + + var segments = path[(DeskEndpoints.RoutePrefix.Length + 1)..].Split('/'); + return (method, segments) switch + { + ("GET", [var mind, "forks"]) => new DeskRoute(DeskRouteKind.List, mind, null, query), + ("GET", [var mind, "forks", var fork]) => new DeskRoute(DeskRouteKind.Read, mind, fork, query), + ("POST", [var mind, "answers"]) => new DeskRoute(DeskRouteKind.Answer, mind, null, query), + _ => null, + }; + } +} + +// The list and read requests the query string and path stand for (fork-card.md, List and Read). Nothing in a +// query sets a decider, an answerer or a Mind; an unknown key, a repeated key or a bad value is request_invalid. +public static class DeskQuery +{ + public const int MaxLimit = 50; + + public static bool TryList(string rawQuery, string mind, out DeskListRequest request) + { + request = new DeskListRequest(mind, null, null, MaxLimit); + if (!TryPairs(rawQuery, out var pairs)) + { + return false; + } + + string? status = null, cursor = null; + var limit = MaxLimit; + foreach (var (key, value) in pairs) + { + switch (key) + { + case "status" when value is "open" or "recorded": + status = value; + break; + case "status" when value.Length == 0: + break; + case "cursor" when IsCursor(value): + cursor = value; + break; + case "cursor" when value.Length == 0: + break; + case "limit" when int.TryParse(value, System.Globalization.NumberStyles.None, System.Globalization.CultureInfo.InvariantCulture, out var n) && n <= MaxLimit: + limit = n; + break; + default: + return false; + } + } + + request = new DeskListRequest(mind, status, cursor, limit); + return true; + } + + // The read request has no query at all. + public static bool TryRead(string rawQuery, string rawFork, out string forkId) + { + forkId = string.Empty; + return rawQuery.Length == 0 && DeskMindRoutes.TryDecode(rawFork, out forkId) && forkId.Length > 0; + } + + // c, N a positive integer without a leading zero. + private static bool IsCursor(string value) => + value.Length > 1 && value[0] == 'c' && value[1] is >= '1' and <= '9' && value.Skip(2).All(char.IsAsciiDigit); + + private static bool TryPairs(string rawQuery, out List<(string Key, string Value)> pairs) + { + pairs = []; + var seen = new HashSet(StringComparer.Ordinal); + if (rawQuery.Length == 0) + { + return true; + } + + foreach (var part in rawQuery.Split('&')) + { + var eq = part.IndexOf('='); + var rawKey = eq < 0 ? part : part[..eq]; + var rawValue = eq < 0 ? string.Empty : part[(eq + 1)..]; + if (!DeskMindRoutes.TryDecode(rawKey, out var key) || !DeskMindRoutes.TryDecode(rawValue, out var value) || !seen.Add(key)) + { + return false; + } + + pairs.Add((key, value)); + } + + return true; + } +} diff --git a/src/Bifrost.Web/Features/Desk/DeskMindRoutes.cs b/src/Bifrost.Web/Features/Desk/DeskMindRoutes.cs new file mode 100644 index 0000000..c4f82d3 --- /dev/null +++ b/src/Bifrost.Web/Features/Desk/DeskMindRoutes.cs @@ -0,0 +1,84 @@ +using System.Text; +using Bifrost.Web.Configuration; + +namespace Bifrost.Web.Features.Desk; + +// The {mind} segment of a desk route (fork-card.md M1). The raw segment is percent-decoded once and compared, +// ordinally, with the configured ids: no case folding, trimming or normalisation, and a decoding failure is a miss. +// The segment is never joined into a path, a socket name or a query, so a value off the list opens nothing. +public sealed class DeskMindRoutes +{ + private static readonly UTF8Encoding StrictUtf8 = new(false, true); + + private readonly Dictionary byId; + + public DeskMindRoutes(IEnumerable minds) + { + byId = new Dictionary(StringComparer.Ordinal); + foreach (var mind in minds) + { + byId.Add(mind.Id, mind); + } + } + + public DeskMindOptions? Resolve(string rawSegment) => + TryDecode(rawSegment, out var id) && byId.TryGetValue(id, out var mind) ? mind : null; + + // One pass of percent-decoding. Uri.UnescapeDataString is not used: it leaves a bad escape in place and turns + // bytes that are not UTF-8 into U+FFFD instead of failing. + internal static bool TryDecode(string raw, out string decoded) + { + decoded = string.Empty; + var bytes = new List(raw.Length); + for (var i = 0; i < raw.Length; i++) + { + var c = raw[i]; + if (c == '%') + { + if (i + 2 >= raw.Length) + { + return false; + } + + if (!TryHex(raw[i + 1], out var high) || !TryHex(raw[i + 2], out var low)) + { + return false; + } + + bytes.Add((byte)((high << 4) | low)); + i += 2; + } + else if (c > 0x7f) + { + // A raw request target is ASCII; anything else is not a segment this desk issues. + return false; + } + else + { + bytes.Add((byte)c); + } + } + + try + { + decoded = StrictUtf8.GetString(bytes.ToArray()); + return true; + } + catch (DecoderFallbackException) + { + return false; + } + } + + private static bool TryHex(char c, out int value) + { + value = c switch + { + >= '0' and <= '9' => c - '0', + >= 'a' and <= 'f' => c - 'a' + 10, + >= 'A' and <= 'F' => c - 'A' + 10, + _ => -1, + }; + return value >= 0; + } +} diff --git a/src/Bifrost.Web/Features/Desk/DeskPorts.cs b/src/Bifrost.Web/Features/Desk/DeskPorts.cs new file mode 100644 index 0000000..84ddba4 --- /dev/null +++ b/src/Bifrost.Web/Features/Desk/DeskPorts.cs @@ -0,0 +1,67 @@ +namespace Bifrost.Web.Features.Desk; + +// The two ports the desk stands on. Bifrost owns the edge's order and judgments; these two own what is not +// Bifrost's yet: validating a bearer token (the OIDC validation scheme) and speaking to a Mind's Huginn on the +// peer-checked door. No production implementation is registered here: with the Desk section configured and a port +// missing, startup fails (DeskEndpoints.MapDesk) instead of serving. + +// What a validated bearer token claims. The credential port verifies the token; the desk judges audience, scope and +// membership. A null Subject or Audience means the token did not carry one. +public sealed record DeskToken(string? Subject, string? Audience, IReadOnlyCollection Scopes); + +public interface IDeskCredential +{ + // Null when the request carries no valid bearer token. + ValueTask AuthenticateAsync(HttpContext context, CancellationToken cancellationToken); +} + +// The envelope sent with every answer, read and list call (fork-card.md M5), under the contract's names, built by +// Bifrost alone: the route's configured Mind id, the verified account's authority (Operator, Steward or Member), +// the account id, and the admin-set decider list of that Mind (1 to 64 unique ids). Nothing in the request sets any +// of it, and Huginn keeps no copy of the list. +public sealed record DeskDoorContext(string Mind, string Authority, string Subject, IReadOnlyList Deciders); + +public abstract record DeskAnswerOutcome +{ + // Huginn stored an outcome (Counted, Revision, Retraction or Refused) and returned its document. + public sealed record Answered(AnswerResult Result) : DeskAnswerOutcome; + + // Nothing stored: Huginn refused with one of the contract's refusal codes. + public sealed record NotStored(string Code) : DeskAnswerOutcome; +} + +public abstract record DeskReadOutcome +{ + public sealed record Found(ForkEntry Entry) : DeskReadOutcome; + + public sealed record NotFound(string Code) : DeskReadOutcome; +} + +// The list request the desk's query string stands for. Mind is the route's, resolved against the configured list. +public sealed record DeskListRequest(string Mind, string? Status, string? Cursor, int Limit); + +public abstract record DeskListOutcome +{ + public sealed record Listed(ForkList List) : DeskListOutcome; + + public sealed record NotListed(string Code) : DeskListOutcome; + + // The next fork is too large for any page: the page holds nothing past the cursor. The client continues with + // cursor c; at Order 0 it has reached the end. List only: no other call can be refused this way. + public sealed record ForkTooLarge(string ForkId, int Order) : DeskListOutcome; +} + +public interface IDeskDoor +{ + // The body is exactly the bytes the client sent. Any exception means the outcome is not confirmed. + Task AnswerAsync( + string socket, DeskDoorContext context, ReadOnlyMemory body, CancellationToken cancellationToken); + + // The entry for one fork, its card built and hashed by Huginn. + Task ReadAsync( + string socket, DeskDoorContext context, string forkId, CancellationToken cancellationToken); + + // A page of entries, the counts and the stamp, as Huginn projects them for this viewer. + Task ListAsync( + string socket, DeskDoorContext context, DeskListRequest request, CancellationToken cancellationToken); +} diff --git a/src/Bifrost.Web/Features/Oidc/OidcEndpoints.cs b/src/Bifrost.Web/Features/Oidc/OidcEndpoints.cs index b435347..35f0297 100644 --- a/src/Bifrost.Web/Features/Oidc/OidcEndpoints.cs +++ b/src/Bifrost.Web/Features/Oidc/OidcEndpoints.cs @@ -117,18 +117,22 @@ public static class OidcEndpoints return Results.Json(claims); } - /// The one reading of "this subject is still an Active member", shared by the token and userinfo endpoints and the logout reconciler. - internal static async Task IsStillActiveMemberAsync(BifrostDbContext db, string? subject, CancellationToken cancellationToken) + /// The one reading of "this subject is still an Active member", shared by the token and userinfo endpoints, the logout reconciler and the fork desk. + internal static async Task IsStillActiveMemberAsync(BifrostDbContext db, string? subject, CancellationToken cancellationToken) => + await ActiveMemberIdAsync(db, subject, cancellationToken) is not null; + + /// The account id of the subject when it is an Active member now, else null. Re-read on every call. + internal static async Task ActiveMemberIdAsync(BifrostDbContext db, string? subject, CancellationToken cancellationToken) { if (!Guid.TryParse(subject, out var accountId)) { - return false; + return null; } var account = await db.UserAccounts.AsNoTracking() .Include(x => x.Membership) .SingleOrDefaultAsync(x => x.Id == accountId, cancellationToken); - return new CurrentBifrostActor(true, account).IsActiveMember; + return new CurrentBifrostActor(true, account).IsActiveMember ? accountId : null; } private static IResult Deny(string error, string description) => diff --git a/src/Bifrost.Web/Features/Shared/StartupConfigurationValidator.cs b/src/Bifrost.Web/Features/Shared/StartupConfigurationValidator.cs index 98b8dc7..6be28cb 100644 --- a/src/Bifrost.Web/Features/Shared/StartupConfigurationValidator.cs +++ b/src/Bifrost.Web/Features/Shared/StartupConfigurationValidator.cs @@ -12,7 +12,8 @@ public sealed class StartupConfigurationValidator( IOptions heimdallOptions, IOptions stripeOptions, IOptions hostOptions, - IOptions conceptVoteOptions) + IOptions conceptVoteOptions, + IOptions deskOptions) { public void Validate() { @@ -45,6 +46,8 @@ public sealed class StartupConfigurationValidator( failures.Add("ConceptVotes:MaxTrackedKeys must be at least 1."); } + ValidateDesk(deskOptions.Value, failures); + if (hostEnvironment.IsDevelopment() || hostEnvironment.IsEnvironment("Testing")) { Refuse(failures); @@ -116,6 +119,106 @@ public sealed class StartupConfigurationValidator( Refuse(failures); } + // Checked in every environment: a bad Mind list or decider id would otherwise pass startup and misroute or + // mis-attribute answers. Account existence is a request-time question, not a startup one. + private void ValidateDesk(DeskOptions desk, List failures) + { + if (!desk.IsConfigured) + { + if (configuration.GetSection(DeskOptions.SectionName).Exists()) + { + failures.Add("Desk:Minds must list at least one Mind when the Desk section exists."); + } + + return; + } + + if (!Uri.TryCreate(desk.AllowedOrigin, UriKind.Absolute, out var origin) || + origin.Scheme is not ("http" or "https") || + !string.Equals(desk.AllowedOrigin, origin.GetLeftPart(UriPartial.Authority), StringComparison.Ordinal)) + { + failures.Add("Desk:AllowedOrigin must be one http(s) origin: scheme, host and optional port, no path."); + } + + if (desk.MaxBodyBytes < 1) + { + failures.Add("Desk:MaxBodyBytes must be at least 1."); + } + + if (desk.RetryAfterSeconds < 1) + { + failures.Add("Desk:RetryAfterSeconds must be at least 1."); + } + + var ids = new HashSet(StringComparer.Ordinal); + var sockets = new HashSet(StringComparer.Ordinal); + for (var i = 0; i < desk.Minds.Count; i++) + { + var mind = desk.Minds[i]; + if (string.IsNullOrWhiteSpace(mind.Id)) + { + failures.Add($"Desk:Minds:{i}:Id must not be blank."); + } + else if (!ids.Add(mind.Id)) + { + failures.Add($"Desk:Minds:{i}:Id repeats an earlier Mind id."); + } + + if (!Path.IsPathFullyQualified(mind.Socket)) + { + failures.Add($"Desk:Minds:{i}:Socket must be an absolute path."); + } + else if (!sockets.Add(mind.Socket)) + { + failures.Add($"Desk:Minds:{i}:Socket repeats an earlier Mind's socket."); + } + + if (mind.Deciders.Count is < 1 or > DeskMindOptions.MaxDeciders) + { + failures.Add($"Desk:Minds:{i}:Deciders must hold 1 to {DeskMindOptions.MaxDeciders} account ids."); + } + + var deciders = new HashSet(StringComparer.Ordinal); + foreach (var decider in mind.Deciders) + { + if (!IsLowercaseGuid(decider)) + { + failures.Add($"Desk:Minds:{i}:Deciders entries must be lowercase D-format account ids."); + } + else if (!deciders.Add(decider)) + { + failures.Add($"Desk:Minds:{i}:Deciders repeats an account id."); + } + } + } + + var attribution = desk.Attribution; + if (!string.IsNullOrEmpty(attribution.Operator) && !IsLowercaseGuid(attribution.Operator)) + { + failures.Add("Desk:Attribution:Operator must be a lowercase D-format account id."); + } + + var stewards = new HashSet(StringComparer.Ordinal); + foreach (var steward in attribution.Stewards) + { + if (!IsLowercaseGuid(steward)) + { + failures.Add("Desk:Attribution:Stewards entries must be lowercase D-format account ids."); + } + else if (!stewards.Add(steward)) + { + failures.Add("Desk:Attribution:Stewards repeats an account id."); + } + else if (string.Equals(steward, attribution.Operator, StringComparison.Ordinal)) + { + failures.Add("Desk:Attribution:Operator must not also be a steward."); + } + } + } + + private static bool IsLowercaseGuid(string? value) => + Guid.TryParseExact(value, "D", out var guid) && string.Equals(value, guid.ToString("D"), StringComparison.Ordinal); + private static void Refuse(List failures) { if (failures.Count > 0) diff --git a/src/Bifrost.Web/Program.cs b/src/Bifrost.Web/Program.cs index 923ed4e..87d8088 100644 --- a/src/Bifrost.Web/Program.cs +++ b/src/Bifrost.Web/Program.cs @@ -7,6 +7,7 @@ using Bifrost.Web.Data; using Bifrost.Web.Domain; using Bifrost.Web.Features.Bridge; using Bifrost.Web.Features.ConceptVotes; +using Bifrost.Web.Features.Desk; using Bifrost.Web.Features.GitHub; using Bifrost.Web.Features.Heimdall; using Bifrost.Web.Features.Membership; @@ -76,6 +77,10 @@ builder.Services .AddOptions() .BindConfiguration(ConceptVoteOptions.SectionName); +builder.Services + .AddOptions() + .BindConfiguration(DeskOptions.SectionName); + builder.Services.AddAntiforgery(options => { options.Cookie.Name = BifrostCookies.Antiforgery; @@ -252,6 +257,13 @@ if (oidcOptions is not null) builder.Services.AddBifrostOidc(oidcOptions); } +// The desk is mapped only when the Desk section exists, and a configured desk needs its credential and door ports. +var deskConfigured = builder.Configuration.GetSection(DeskOptions.SectionName).Exists(); +if (deskConfigured) +{ + builder.Services.AddBifrostDesk(); +} + var app = builder.Build(); var appJsonSerializerOptions = CreateAppJsonSerializerOptions(); @@ -300,6 +312,11 @@ if (oidcOptions is not null) app.MapBifrostOidc(); } +if (deskConfigured) +{ + app.MapDesk(); +} + app.MapPost("/github/webhooks", async ( HttpRequest request, GitHubWebhookService gitHubWebhookService, diff --git a/tests/Bifrost.Web.Tests/DeskAnswerTests.cs b/tests/Bifrost.Web.Tests/DeskAnswerTests.cs new file mode 100644 index 0000000..9cf0048 --- /dev/null +++ b/tests/Bifrost.Web.Tests/DeskAnswerTests.cs @@ -0,0 +1,203 @@ +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; +using Bifrost.Web.Features.Desk; +using Bifrost.Web.Tests.Support; + +namespace Bifrost.Web.Tests; + +public sealed class DeskAnswerTests +{ + private static readonly string Socket = DeskWebApplicationFactory.SocketOf("gamecult-chat"); + + private static async Task<(DeskWebApplicationFactory Factory, string Token)> ArrangeAsync() + { + var factory = new DeskWebApplicationFactory(); + await factory.EnsureAccountAsync(DeskWebApplicationFactory.Alice, true); + return (factory, factory.Token(DeskWebApplicationFactory.Alice, "bifrost-desk", "desk.answer")); + } + + private static AnswerResult Stale(string forkId) => new( + DeskSchemas.AnswerResult, + new DeskAnswerDocument( + DeskSchemas.DeskAnswer, "n1", forkId, "Refused", null, null, "stale_fork", "oldhash", "no", null, + "2026-11-12T19:02:00Z", "2026-11-12T19:02:05Z", false, DeskWebApplicationFactory.Alice, "Operator", + "00000001-0000-4000-8000-000000000001"), + null, + null); + + [Fact] + public async Task Stale_fork_attaches_current_card_fresh_and_replayed() + { + var (factory, token) = await ArrangeAsync(); + using (factory) + { + factory.Door.AnswerScript = (_, _, _) => new DeskAnswerOutcome.Answered(Stale(DeskReplay.ChatFork)); + var target = DeskReplay.Target("answer", "gamecult-chat", null); + + factory.Door.Forks[(Socket, DeskReplay.ChatFork)] = DeskReplay.Entry(DeskReplay.Card(DeskReplay.ChatFork, "first card")); + var fresh = await factory.SendAsync("POST", target, DeskReplay.AnswerBody(DeskReplay.ChatFork), token); + + // The same stored answer is replayed after the card changed: nothing is stored for the card, so the reply carries the new one. + factory.Door.Forks[(Socket, DeskReplay.ChatFork)] = DeskReplay.Entry(DeskReplay.Card(DeskReplay.ChatFork, "second card")); + var replayed = await factory.SendAsync("POST", target, DeskReplay.AnswerBody(DeskReplay.ChatFork), token); + + Assert.Equal(409, fresh.Status); + Assert.Equal("first card", (string?)JsonNode.Parse(fresh.Body)!["card"]!["title"]); + Assert.Equal(409, replayed.Status); + Assert.Equal("second card", (string?)JsonNode.Parse(replayed.Body)!["card"]!["title"]); + } + } + + [Fact] + public async Task A_refusal_that_is_not_stale_carries_no_card_and_asks_no_read() + { + var (factory, token) = await ArrangeAsync(); + using (factory) + { + var closed = Stale(DeskReplay.ChatFork); + factory.Door.AnswerScript = (_, _, _) => new DeskAnswerOutcome.Answered(closed with { Answer = closed.Answer with { Code = "fork_closed" } }); + var reply = await factory.SendAsync("POST", DeskReplay.Target("answer", "gamecult-chat", null), DeskReplay.AnswerBody(DeskReplay.ChatFork), token); + + Assert.Equal(409, reply.Status); + Assert.Null(JsonNode.Parse(reply.Body)!["card"]); + Assert.DoesNotContain(factory.Door.Calls, c => c.Kind == "read"); + } + } + + [Fact] + public async Task Card_read_failure_is_unavailable() + { + foreach (var failure in new[] { "throws", "refuses" }) + { + var (factory, token) = await ArrangeAsync(); + using (factory) + { + factory.Door.AnswerScript = (_, _, _) => new DeskAnswerOutcome.Answered(Stale(DeskReplay.ChatFork)); + factory.Door.ThrowOnRead = failure == "throws"; + // "refuses": no fork held, so the read answers fork_unknown + + var reply = await factory.SendAsync("POST", DeskReplay.Target("answer", "gamecult-chat", null), DeskReplay.AnswerBody(DeskReplay.ChatFork), token); + + Assert.True(503 == reply.Status, failure); + Assert.Equal("unavailable", (string?)JsonNode.Parse(reply.Body)!["code"]); + } + } + } + + [Fact] + public async Task The_body_reaches_the_door_byte_for_byte() + { + var (factory, token) = await ArrangeAsync(); + using (factory) + { + DeskReplay.HoldVectorForks(factory.Door); + // Whitespace, escapes and key spacing a re-serialiser would normalise. + var compact = Encoding.UTF8.GetString(DeskReplay.AnswerBody(DeskReplay.ChatFork)); + var odd = compact.Replace("{", "{ \n\t", StringComparison.Ordinal).Replace(",\"", " ,\r\n \"", StringComparison.Ordinal) + .Replace("Keep", "\\u004beep", StringComparison.Ordinal) + " \n"; + var bytes = Encoding.UTF8.GetBytes(odd); + + var reply = await factory.SendAsync("POST", DeskReplay.Target("answer", "gamecult-chat", null), bytes, token); + + Assert.Equal(200, reply.Status); + Assert.Equal(bytes, Assert.Single(factory.Door.Calls).Body); + } + } + + [Fact] + public async Task The_door_context_comes_from_the_account_and_the_configuration_never_the_request() + { + var (factory, _) = await ArrangeAsync(); + using (factory) + { + DeskReplay.HoldVectorForks(factory.Door); + await factory.EnsureAccountAsync(DeskWebApplicationFactory.Steward, true); + var token = factory.Token(DeskWebApplicationFactory.Steward.ToUpperInvariant(), "bifrost-desk", "desk.answer"); + + // Query strings and headers that name a decider or an answerer change nothing. + var reply = await factory.SendAsync( + "POST", DeskReplay.Target("answer", "nibu", null) + "?deciders=" + DeskWebApplicationFactory.Member + "&answerer=x", + DeskReplay.AnswerBody("nibu:question:glow-order"), token, + configure: http => http.Request.Headers["X-Deciders"] = DeskWebApplicationFactory.Member); + + Assert.Equal(200, reply.Status); + var call = Assert.Single(factory.Door.Calls); + Assert.Equal(DeskWebApplicationFactory.Steward, call.Context.Subject); + Assert.Equal("Steward", call.Context.Authority); + Assert.Equal("nibu", call.Context.Mind); + Assert.Equal(DeskWebApplicationFactory.DecidersOf("nibu"), call.Context.Deciders); + } + } + + [Fact] + public async Task Body_is_judged_after_membership_and_before_the_mind() + { + var (factory, token) = await ArrangeAsync(); + using (factory) + { + // A bad body for an unknown Mind is the body's refusal; a non-Active member's bad body is forbidden. + var bad = Encoding.UTF8.GetBytes("{\"answerer\":\"x\"}"); + var reply = await factory.SendAsync("POST", DeskReplay.Target("answer", "ghost", null), bad, token); + Assert.Equal("body_forbidden_field", (string?)JsonNode.Parse(reply.Body)!["code"]); + + await factory.EnsureAccountAsync(DeskWebApplicationFactory.Alice, false); + var forbidden = await factory.SendAsync("POST", DeskReplay.Target("answer", "ghost", null), bad, token); + Assert.Equal("forbidden", (string?)JsonNode.Parse(forbidden.Body)!["code"]); + Assert.Empty(factory.Door.Calls); + } + } + + [Fact] + public async Task Membership_is_re_read_on_every_request() + { + var (factory, token) = await ArrangeAsync(); + using (factory) + { + DeskReplay.HoldVectorForks(factory.Door); + var target = DeskReplay.Target("answer", "gamecult-chat", null); + Assert.Equal(200, (await factory.SendAsync("POST", target, DeskReplay.AnswerBody(DeskReplay.ChatFork), token)).Status); + await factory.EnsureAccountAsync(DeskWebApplicationFactory.Alice, false); + Assert.Equal(403, (await factory.SendAsync("POST", target, DeskReplay.AnswerBody(DeskReplay.ChatFork), token)).Status); + } + } + + [Fact] + public async Task An_oversized_body_is_refused_before_it_is_read() + { + var (factory, token) = await ArrangeAsync(); + using (factory) + { + var big = Encoding.UTF8.GetBytes("{\"x\":\"" + new string('a', 17 * 1024) + "\"}"); + var reply = await factory.SendAsync("POST", DeskReplay.Target("answer", "gamecult-chat", null), big, token); + Assert.Equal("body_invalid", (string?)JsonNode.Parse(reply.Body)!["code"]); + + // Chunked: no Content-Length to judge, so the cap applies while reading. + var chunked = await factory.SendAsync( + "POST", DeskReplay.Target("answer", "gamecult-chat", null), big, token, + configure: http => http.Request.ContentLength = null); + Assert.Equal("body_invalid", (string?)JsonNode.Parse(chunked.Body)!["code"]); + Assert.Empty(factory.Door.Calls); + } + } + + [Fact] + public async Task A_door_that_sends_a_code_the_contract_does_not_name_is_unavailable() + { + var (factory, token) = await ArrangeAsync(); + using (factory) + { + foreach (var script in new Func[] + { + () => new DeskAnswerOutcome.NotStored("something_internal"), + () => new DeskAnswerOutcome.NotStored("forbidden"), + () => new DeskAnswerOutcome.Answered(Stale("x") with { Answer = Stale("x").Answer with { Code = "made_up" } }), + }) + { + factory.Door.AnswerScript = (_, _, _) => script(); + var reply = await factory.SendAsync("POST", DeskReplay.Target("answer", "gamecult-chat", null), DeskReplay.AnswerBody(DeskReplay.ChatFork), token); + Assert.Equal(503, reply.Status); + } + } + } +} diff --git a/tests/Bifrost.Web.Tests/DeskAuthCaseTests.cs b/tests/Bifrost.Web.Tests/DeskAuthCaseTests.cs new file mode 100644 index 0000000..bedbbfc --- /dev/null +++ b/tests/Bifrost.Web.Tests/DeskAuthCaseTests.cs @@ -0,0 +1,49 @@ +using System.Text.Json.Nodes; +using Bifrost.Web.Features.Desk; +using Bifrost.Web.Tests.Support; + +namespace Bifrost.Web.Tests; + +public sealed class DeskAuthCaseTests +{ + [Fact] + public async Task Vector_auth_cases() + { + var cases = DeskVectors.Rows("transport", "authCases").ToList(); + Assert.Equal(22, cases.Count); + + foreach (var row in cases) + { + using var factory = new DeskWebApplicationFactory(); + DeskReplay.HoldVectorForks(factory.Door); + factory.Door.Down = (bool)row["huginn_down"]!; + var token = await DeskReplay.TokenAsync(factory, row["token"]); + + var route = (string)row["route"]!; + var reply = await factory.SendAsync( + route == "answer" ? "POST" : "GET", DeskReplay.Target(route, "gamecult-chat", DeskReplay.ChatFork), + route == "answer" ? DeskReplay.AnswerBody(DeskReplay.ChatFork) : null, token); + + var name = (string)row["name"]!; + var expect = (string)row["expect"]!; + var body = JsonNode.Parse(reply.Body); + switch (expect) + { + case "ok": + Assert.True(200 == reply.Status, $"{name}: {reply.Status} {reply.Text}"); + Assert.Single(factory.Door.Calls); + break; + case "unavailable": + Assert.True(503 == reply.Status, $"{name}: {reply.Status}"); + Assert.True(DeskVectors.SameShape(body, JsonNode.Parse(System.Text.Json.JsonSerializer.Serialize(DeskUnavailables.Document(30), DeskJson.Options))), name); + break; + default: + Assert.True(DeskRefusals.StatusOf(expect) == reply.Status, $"{name}: {reply.Status}"); + Assert.True(DeskVectors.SameShape(body, JsonNode.Parse(System.Text.Json.JsonSerializer.Serialize(DeskRefusals.Document(expect), DeskJson.Options))), name); + // Auth, scope and membership are judged before Huginn is asked. + Assert.True(factory.Door.Calls.Count == 0, $"{name}: the door was called"); + break; + } + } + } +} diff --git a/tests/Bifrost.Web.Tests/DeskBodyShapeTests.cs b/tests/Bifrost.Web.Tests/DeskBodyShapeTests.cs new file mode 100644 index 0000000..43dc1f5 --- /dev/null +++ b/tests/Bifrost.Web.Tests/DeskBodyShapeTests.cs @@ -0,0 +1,267 @@ +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; +using Bifrost.Web.Features.Desk; +using Bifrost.Web.Tests.Support; + +namespace Bifrost.Web.Tests; + +public sealed class DeskBodyShapeTests +{ + private const int Cap = 16 * 1024; + + private static DeskBodyVerdict Judge(JsonNode body, int cap = Cap) => DeskBodyShape.Judge(DeskVectors.Bytes(body), cap); + + private static DeskBodyVerdict Judge(string json, int cap = Cap) => DeskBodyShape.Judge(Encoding.UTF8.GetBytes(json), cap); + + private static JsonObject ValidBody() => + DeskVectors.Rows("request", "valid").First(row => (string)row["name"]! == "choice_and_note")["message"]!.DeepClone().AsObject(); + + private static string ExpectedCode(string name) => name switch + { + "answerer_key" or "authority_key" => DeskRefusals.BodyForbiddenField, + "answer_object_with_a_key" => DeskRefusals.BodyUnknownField, + _ => DeskRefusals.BodyInvalid, + }; + + public static IEnumerable ValidRows() => + DeskVectors.Rows("request", "valid").Select(row => new object[] { (string)row["name"]! }); + + public static IEnumerable InvalidRows() => + DeskVectors.Rows("request", "invalid").Select(row => new object[] { (string)row["name"]! }); + + [Theory] + [MemberData(nameof(ValidRows))] + public void Valid_request_vectors_pass(string name) + { + var message = DeskVectors.Rows("request", "valid").First(row => (string)row["name"]! == name)["message"]!; + + var verdict = Judge(message); + + Assert.Null(verdict.RefusalCode); + Assert.NotNull(verdict.Request); + // The parsed request is the document: same keys, same order, same values. + var echoed = JsonSerializer.SerializeToNode(verdict.Request, DeskJson.Options); + Assert.True(DeskVectors.SameShape(message, echoed), $"{name} did not round-trip: {echoed}"); + } + + [Theory] + [MemberData(nameof(InvalidRows))] + public void Invalid_request_vectors_refuse_with_their_code(string name) + { + var message = DeskVectors.Rows("request", "invalid").First(row => (string)row["name"]! == name)["message"]!; + + var verdict = Judge(message); + + Assert.Null(verdict.Request); + Assert.Equal(ExpectedCode(name), verdict.RefusalCode); + } + + [Fact] + public void Forbidden_key_at_any_depth_beats_unknown_key() + { + foreach (var key in new[] { "answerer", "authority" }) + { + var nested = ValidBody(); + nested["answer"]!["detail"] = new JsonObject { ["deep"] = new JsonArray(new JsonObject { [key] = "x" }) }; + nested["extra"] = 1; + Assert.Equal(DeskRefusals.BodyForbiddenField, Judge(nested).RefusalCode); + + var top = ValidBody(); + top["unknown"] = 1; + top[key] = "someone"; + Assert.Equal(DeskRefusals.BodyForbiddenField, Judge(top).RefusalCode); + + // Written with an escape: the name is the same once decoded, and still refused whole. + var escaped = DeskVectors.Bytes(ValidBody()).ToArray(); + var text = Encoding.UTF8.GetString(escaped).TrimEnd('}') + $",\"{key[..3]}\\u{(int)key[3]:x4}{key[4..]}\":\"x\"}}"; + Assert.Equal(DeskRefusals.BodyForbiddenField, Judge(text).RefusalCode); + } + + // The key name is matched exactly: a near miss is only an unknown key. + var near = ValidBody(); + near["Answerer"] = "x"; + Assert.Equal(DeskRefusals.BodyUnknownField, Judge(near).RefusalCode); + } + + [Fact] + public void Mind_and_deciders_in_the_body_are_unknown_fields() + { + foreach (var key in new[] { "mind", "deciders" }) + { + var body = ValidBody(); + body[key] = "x"; + Assert.Equal(DeskRefusals.BodyUnknownField, Judge(body).RefusalCode); + } + } + + [Fact] + public void Illegal_retraction_forms_reach_the_door() + { + var forms = new (string Answer, string Retracted, string Retracts)[] + { + ("null", "true", "null"), // a retraction with no target + ("{\"choice\":\"no\",\"note\":null}", "true", "\"n1\""), // a retraction carrying an answer + ("{\"choice\":\"no\",\"note\":null}", "false", "\"n1\""), // a plain answer with a target + ("null", "true", "\"n1\""), // the legal retraction + }; + + foreach (var (answer, retracted, retracts) in forms) + { + var body = ValidBody(); + body["answer"] = JsonNode.Parse(answer); + body["retracted"] = JsonNode.Parse(retracted); + body["retracts"] = JsonNode.Parse(retracts); + + var verdict = Judge(body); + + Assert.True(verdict.RefusalCode is null, $"answer={answer} retracted={retracted} retracts={retracts}: {verdict.RefusalCode}"); + Assert.NotNull(verdict.Request); + } + } + + [Fact] + public void Neither_answer_nor_retraction_is_body_invalid() + { + foreach (var retracts in new[] { "null", "\"n1\"" }) + { + var body = ValidBody(); + body["answer"] = null; + body["retracted"] = false; + body["retracts"] = JsonNode.Parse(retracts); + + Assert.Equal(DeskRefusals.BodyInvalid, Judge(body).RefusalCode); + } + } + + [Fact] + public void Note_limits_are_utf8_bytes() + { + string Note(string text) => Judge(WithNote(text)).RefusalCode ?? "pass"; + + string Repeat(string unit, int count) => string.Concat(Enumerable.Repeat(unit, count)); + var astral = char.ConvertFromUtf32(0x1F600); // 4 bytes + var combining = "e" + (char)0x0301; // 3 bytes + var twoByte = ((char)0xe9).ToString(); // 2 bytes + var spaces = new string(new[] { (char)9, (char)0xa0, (char)0x3000, (char)0x2028 }); + + Assert.Equal("pass", Note(new string('x', 4000))); + Assert.Equal(DeskRefusals.BodyInvalid, Note(new string('x', 4001))); + Assert.Equal("pass", Note(Repeat(astral, 1000))); + Assert.Equal(DeskRefusals.BodyInvalid, Note(Repeat(astral, 1001))); + Assert.Equal("pass", Note(Repeat(combining, 1333) + "x")); + Assert.Equal(DeskRefusals.BodyInvalid, Note(Repeat(combining, 1334))); + Assert.Equal(DeskRefusals.BodyInvalid, Note(Repeat(twoByte, 1999) + "xxx")); + Assert.Equal("pass", Note("x")); + Assert.Equal(DeskRefusals.BodyInvalid, Note("")); + Assert.Equal(DeskRefusals.BodyInvalid, Note(spaces)); + Assert.Equal("pass", Note(" a ")); + + static JsonNode WithNote(string text) + { + var body = ValidBody(); + body["answer"] = new JsonObject { ["choice"] = null, ["note"] = text }; + return body; + } + } + + [Fact] + public void Lone_surrogate_in_a_note_is_body_invalid() + { + var json = DeskVectors.Bytes(ValidBody()).ToArray(); + var text = Encoding.UTF8.GetString(json).Replace("\"note\":\"", "\"note\":\"\\ud800"); + Assert.Equal(DeskRefusals.BodyInvalid, Judge(text).RefusalCode); + } + + [Fact] + public void Over_cap_refused_before_parse() + { + var body = ValidBody(); + var bytes = DeskVectors.Bytes(body); + + Assert.Null(DeskBodyShape.Judge(bytes, bytes.Length).RefusalCode); + Assert.Equal(DeskRefusals.BodyInvalid, DeskBodyShape.Judge(bytes, bytes.Length - 1).RefusalCode); + + // Over the cap the forbidden key is never found, because the body is never parsed. + body["answerer"] = "x"; + var forbidden = DeskVectors.Bytes(body); + Assert.Equal(DeskRefusals.BodyForbiddenField, DeskBodyShape.Judge(forbidden, forbidden.Length).RefusalCode); + Assert.Equal(DeskRefusals.BodyInvalid, DeskBodyShape.Judge(forbidden, forbidden.Length - 1).RefusalCode); + } + + [Theory] + [InlineData("")] + [InlineData("not json")] + [InlineData("[]")] + [InlineData("null")] + [InlineData("\"text\"")] + [InlineData("{")] + public void Non_object_or_broken_json_is_body_invalid(string json) => + Assert.Equal(DeskRefusals.BodyInvalid, Judge(json).RefusalCode); + + [Fact] + public void Invalid_utf8_is_body_invalid() + { + var bytes = DeskVectors.Bytes(ValidBody()); + var bad = bytes.Take(bytes.Length - 2).Concat(new byte[] { 0xff }).Concat(bytes.Skip(bytes.Length - 2)).ToArray(); + Assert.Equal(DeskRefusals.BodyInvalid, DeskBodyShape.Judge(bad, Cap).RefusalCode); + } + + [Fact] + public void Repeated_key_is_body_invalid() + { + var text = Encoding.UTF8.GetString(DeskVectors.Bytes(ValidBody())).TrimEnd('}') + ",\"fork_id\":\"other\"}"; + Assert.Equal(DeskRefusals.BodyInvalid, Judge(text).RefusalCode); + + var answer = Encoding.UTF8.GetString(DeskVectors.Bytes(ValidBody())).Replace("\"choice\":\"no\"", "\"choice\":\"no\",\"choice\":\"yes\""); + Assert.Equal(DeskRefusals.BodyInvalid, Judge(answer).RefusalCode); + } + + [Fact] + public void Trailing_newline_does_not_rescue_a_bad_pattern() + { + var body = ValidBody(); + body["request_id"] = ((string)body["request_id"]!) + "\n"; + Assert.Equal(DeskRefusals.BodyInvalid, Judge(body).RefusalCode); + + body = ValidBody(); + body["options_hash"] = ((string)body["options_hash"]!) + "\n"; + Assert.Equal(DeskRefusals.BodyInvalid, Judge(body).RefusalCode); + } + + [Fact] + public void Wrong_schema_version_and_bad_retracts_are_body_invalid() + { + var body = ValidBody(); + body["schemaVersion"] = "gamecult.fork.answer_request.v1"; + Assert.Equal(DeskRefusals.BodyInvalid, Judge(body).RefusalCode); + + foreach (var retracts in new[] { "n0", "n01", "x1", "n" }) + { + body = ValidBody(); + body["answer"] = null; + body["retracted"] = true; + body["retracts"] = retracts; + Assert.Equal(DeskRefusals.BodyInvalid, Judge(body).RefusalCode); + } + } + + [Fact] + public void Missing_key_is_body_invalid_for_each_key() + { + foreach (var key in ValidBody().Select(p => p.Key).ToList()) + { + var body = ValidBody(); + body.Remove(key); + Assert.Equal(DeskRefusals.BodyInvalid, Judge(body).RefusalCode); + } + } + + [Fact] + public void Answer_object_must_hold_both_keys() + { + var body = ValidBody(); + body["answer"] = new JsonObject { ["choice"] = "no" }; + Assert.Equal(DeskRefusals.BodyInvalid, Judge(body).RefusalCode); + } +} diff --git a/tests/Bifrost.Web.Tests/DeskDocumentTests.cs b/tests/Bifrost.Web.Tests/DeskDocumentTests.cs new file mode 100644 index 0000000..bce61c6 --- /dev/null +++ b/tests/Bifrost.Web.Tests/DeskDocumentTests.cs @@ -0,0 +1,67 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using Bifrost.Web.Features.Desk; +using Bifrost.Web.Tests.Support; + +namespace Bifrost.Web.Tests; + +public sealed class DeskDocumentTests +{ + private static JsonNode? Write(T document) => JsonSerializer.SerializeToNode(document, DeskJson.Options); + + [Fact] + public void Refusal_and_unavailable_documents_match_vectors() + { + var rows = DeskVectors.Rows("doorRefusals").ToList(); + Assert.Equal(9, rows.Count); + foreach (var row in rows) + { + var code = (string)row["message"]!["code"]!; + Assert.True(DeskVectors.SameShape(row["message"], Write(DeskRefusals.Document(code))), $"refusal {code}"); + Assert.Equal((int)row["httpStatus"]!, DeskRefusals.StatusOf(code)); + } + + var unavailable = DeskVectors.Rows("unavailable").ToList(); + Assert.Equal(2, unavailable.Count); + foreach (var row in unavailable) + { + var retry = (int?)row["message"]!["retryAfterSeconds"]; + Assert.True(DeskVectors.SameShape(row["message"], Write(DeskUnavailables.Document(retry))), (string)row["name"]!); + Assert.Equal(DeskUnavailables.Status, (int)row["httpStatus"]!); + } + } + + [Fact] + public void Every_refusal_code_in_the_schema_has_a_document() + { + var codes = DeskVectors.Rows("doorRefusals").Select(row => (string)row["message"]!["code"]!).ToHashSet(); + var written = typeof(DeskRefusals).GetFields().Where(f => f.IsLiteral).Select(f => (string)f.GetRawConstantValue()!).ToHashSet(); + Assert.Equal(codes, written); + } + + [Fact] + public void Answer_results_round_trip_with_their_spelling() + { + var rows = DeskVectors.Rows("results").ToList(); + Assert.Equal(6, rows.Count); + foreach (var row in rows) + { + var message = row["message"]!; + var result = message.Deserialize(DeskJson.Options)!; + Assert.True(DeskVectors.SameShape(message, Write(result)), (string)row["name"]!); + } + } + + [Fact] + public void Cards_round_trip_with_their_spelling() + { + var rows = DeskVectors.Rows("card", "valid").ToList(); + Assert.Equal(5, rows.Count); + foreach (var row in rows) + { + var message = row["message"]!; + var card = message.Deserialize(DeskJson.Options)!; + Assert.True(DeskVectors.SameShape(message, Write(card)), (string)row["name"]!); + } + } +} diff --git a/tests/Bifrost.Web.Tests/DeskHardeningTests.cs b/tests/Bifrost.Web.Tests/DeskHardeningTests.cs new file mode 100644 index 0000000..c7b0055 --- /dev/null +++ b/tests/Bifrost.Web.Tests/DeskHardeningTests.cs @@ -0,0 +1,327 @@ +using System.Text; +using System.Text.Json.Nodes; +using Bifrost.Web.Configuration; +using Bifrost.Web.Features.Desk; +using Bifrost.Web.Tests.Support; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace Bifrost.Web.Tests; + +// The rules the desk's edge keeps that a happy path never shows: whose decider list rides each call, the body cap +// to the byte, what the Retry-After comes from, the audience's case, no-store, and what a failing port becomes. +public sealed class DeskHardeningTests +{ + private const string Nibu = "nibu"; + private const string Chat = "gamecult-chat"; + private const string NibuFork = "nibu:question:glow-order"; + + private static async Task ArrangeAsync(Dictionary? configuration = null) + { + var factory = new DeskWebApplicationFactory(configuration); + DeskReplay.HoldVectorForks(factory.Door); + await factory.EnsureAccountAsync(DeskWebApplicationFactory.Alice, true); + return factory; + } + + private static string Token(DeskWebApplicationFactory factory, string audience = "bifrost-desk") => + factory.Token(DeskWebApplicationFactory.Alice, audience, "desk.read", "desk.answer"); + + private static Task SendRouteAsync(DeskWebApplicationFactory factory, string route, string mind, string token) + { + var fork = mind == Nibu ? NibuFork : DeskReplay.ChatFork; + return route switch + { + "answer" => factory.SendAsync("POST", DeskReplay.Target("answer", mind, null), DeskReplay.AnswerBody(fork), token), + "read" => factory.SendAsync("GET", DeskReplay.Target("read", mind, fork), null, token), + _ => factory.SendAsync("GET", DeskReplay.Target("list", mind, null), null, token), + }; + } + + private static readonly string[] Routes = ["answer", "read", "list"]; + + private static string Code(DeskReply reply) => (string)JsonNode.Parse(reply.Body)!["code"]!; + + // (a) Each Mind's list is its own: a context built from another Mind's list changes the call. + [Fact] + public async Task Each_call_carries_the_route_minds_own_decider_list_and_envelope() + { + using var factory = await ArrangeAsync(); + Assert.NotEqual(DeskWebApplicationFactory.DecidersOf(Chat), DeskWebApplicationFactory.DecidersOf(Nibu)); + var token = Token(factory); + + foreach (var mind in new[] { Chat, Nibu }) + { + foreach (var route in Routes) + { + factory.Door.Calls.Clear(); + var reply = await SendRouteAsync(factory, route, mind, token); + Assert.True(reply.Status == 200, $"{mind} {route}: {reply.Status}"); + var call = Assert.Single(factory.Door.Calls); + Assert.True(DeskWebApplicationFactory.DecidersOf(mind).SequenceEqual(call.Context.Deciders), $"{mind} {route}: deciders"); + Assert.Equal(mind, call.Context.Mind); + Assert.Equal(DeskWebApplicationFactory.SocketOf(mind), call.Socket); + Assert.Equal(DeskWebApplicationFactory.Alice, call.Context.Subject); + Assert.Equal("Operator", call.Context.Authority); + } + } + } + + // (b) The cap to the byte, at the HTTP edge, however the length is announced. + [Theory] + [InlineData(0)] + [InlineData(4096)] + [InlineData(4097)] + [InlineData(8192)] + public async Task A_body_of_exactly_the_cap_reaches_the_shape_judge_and_one_more_byte_does_not(int cap) + { + var bare = DeskReplay.AnswerBody(DeskReplay.ChatFork); + cap = Math.Max(cap, bare.Length); + var config = DeskWebApplicationFactory.DefaultConfiguration(); + config["Desk:MaxBodyBytes"] = cap.ToString(System.Globalization.CultureInfo.InvariantCulture); + using var factory = await ArrangeAsync(config); + var token = Token(factory); + var target = DeskReplay.Target("answer", Chat, null); + byte[] Padded(int length) => [.. bare, .. Enumerable.Repeat((byte)' ', length - bare.Length)]; + + var modes = new (string Name, Action Configure)[] + { + ("content-length", _ => { }), + ("no content-length", http => http.Request.ContentLength = null), + ("chunked", http => + { + http.Request.ContentLength = null; + http.Request.Headers.TransferEncoding = "chunked"; + }), + }; + foreach (var (name, configure) in modes) + { + factory.Door.Calls.Clear(); + var exact = await factory.SendAsync("POST", target, Padded(cap), token, configure: configure); + Assert.True(exact.Status == 200, $"{name} exact: {exact.Status} {exact.Text}"); + Assert.Equal(cap, Assert.Single(factory.Door.Calls).Body!.Length); + + factory.Door.Calls.Clear(); + var over = await factory.SendAsync("POST", target, Padded(cap + 1), token, configure: configure); + Assert.True(over.Status == 400 && Code(over) == "body_invalid", $"{name} over: {over.Status} {over.Text}"); + Assert.Empty(factory.Door.Calls); + } + } + + // A declared length over the cap is refused on the header alone, before any byte is read. + [Fact] + public async Task A_declared_length_over_the_cap_is_refused_before_reading() + { + var bare = DeskReplay.AnswerBody(DeskReplay.ChatFork); + var config = DeskWebApplicationFactory.DefaultConfiguration(); + config["Desk:MaxBodyBytes"] = bare.Length.ToString(System.Globalization.CultureInfo.InvariantCulture); + using var factory = await ArrangeAsync(config); + + var reply = await factory.SendAsync( + "POST", DeskReplay.Target("answer", Chat, null), bare, Token(factory), + configure: http => http.Request.ContentLength = bare.Length + 1); + + Assert.Equal("body_invalid", Code(reply)); + Assert.Empty(factory.Door.Calls); + } + + // (c) The wait an `unavailable` names is the configured one, in the document and in the header. + [Fact] + public async Task Retry_after_comes_from_configuration_in_the_document_and_the_header() + { + var config = DeskWebApplicationFactory.DefaultConfiguration(); + config["Desk:RetryAfterSeconds"] = "7"; + using var factory = await ArrangeAsync(config); + factory.Door.Down = true; + var token = Token(factory); + + foreach (var route in Routes) + { + var reply = await SendRouteAsync(factory, route, Chat, token); + Assert.True(reply.Status == 503, route); + Assert.Equal(7, (int)JsonNode.Parse(reply.Body)!["retryAfterSeconds"]!); + Assert.Equal("7", reply.Headers.RetryAfter.ToString()); + } + } + + // (d) The audience is compared ordinally. + [Theory] + [InlineData("BIFROST-DESK")] + [InlineData("Bifrost-Desk")] + [InlineData("bifrost-desk ")] + [InlineData("bifrost-votes")] + public async Task The_audience_is_case_sensitive(string audience) + { + using var factory = await ArrangeAsync(); + + foreach (var route in Routes) + { + var reply = await SendRouteAsync(factory, route, Chat, Token(factory, audience)); + Assert.True(reply.Status == 401 && Code(reply) == "unauthenticated", $"{route}: {reply.Status}"); + } + + Assert.Empty(factory.Door.Calls); + } + + // (e) No desk response is cacheable: answers, refusals, unavailable and an unrecognised target alike. + [Fact] + public async Task Every_desk_response_is_no_store() + { + using var factory = await ArrangeAsync(); + var token = Token(factory); + var replies = new List<(string, DeskReply)>(); + foreach (var route in Routes) + { + replies.Add((route + " 200", await SendRouteAsync(factory, route, Chat, token))); + replies.Add((route + " 401", await SendRouteAsync(factory, route, Chat, ""))); + replies.Add((route + " 404", await SendRouteAsync(factory, route, "ghost", token))); + } + + factory.Door.Down = true; + replies.Add(("503", await SendRouteAsync(factory, "list", Chat, token))); + replies.Add(("unrecognised", await factory.SendAsync("GET", "/api/desk/x", null, token))); + + foreach (var (label, reply) in replies) + { + Assert.True(reply.Headers.CacheControl.ToString() == "no-store", $"{label}: '{reply.Headers.CacheControl}'"); + } + + Assert.Equal(200, replies[0].Item2.Status); + Assert.Equal(503, replies[^2].Item2.Status); + } + + // (f) A cancelled request is not `unavailable`: the exception goes on to the host. + [Fact] + public async Task A_cancelled_request_is_not_called_unavailable() + { + using var factory = await ArrangeAsync(); + using var cts = new CancellationTokenSource(); + factory.Door.AnswerScript = (_, _, _) => + { + cts.Cancel(); + throw new IOException("the client went away"); + }; + + var reply = await factory.SendAsync( + "POST", DeskReplay.Target("answer", Chat, null), DeskReplay.AnswerBody(DeskReplay.ChatFork), Token(factory), + configure: http => http.RequestAborted = cts.Token); + + // The host's exception handling answers; the desk does not claim the door was merely unavailable. + Assert.NotEqual(503, reply.Status); + Assert.DoesNotContain("unavailable", reply.Text); + } + + // (f) A stale_fork needs the current card; a card read that finds nothing, for any code, is `unavailable`. + [Theory] + [InlineData("fork_unknown")] + [InlineData("request_invalid")] + [InlineData("made_up")] + public async Task A_stale_fork_whose_card_read_finds_nothing_is_unavailable_never_a_stale_fork_without_a_card(string code) + { + using var factory = await ArrangeAsync(); + var stale = new AnswerResult( + DeskSchemas.AnswerResult, + new DeskAnswerDocument( + DeskSchemas.DeskAnswer, "n1", DeskReplay.ChatFork, "Refused", null, null, "stale_fork", "oldhash", "no", null, + "2026-11-12T19:02:00Z", "2026-11-12T19:02:05Z", false, DeskWebApplicationFactory.Alice, "Operator", + "00000001-0000-4000-8000-000000000001"), + null, + null); + factory.Door.AnswerScript = (_, _, _) => new DeskAnswerOutcome.Answered(stale); + factory.Door.ReadScript = (_, _) => new DeskReadOutcome.NotFound(code); + + var reply = await SendRouteAsync(factory, "answer", Chat, Token(factory)); + + Assert.Equal(503, reply.Status); + Assert.Equal("unavailable", Code(reply)); + Assert.Null(JsonNode.Parse(reply.Body)!["card"]); + Assert.Contains(factory.Door.Calls, c => c.Kind == "read"); + } + + // (h) A port that fails is `unavailable`, never a 500 and never a pass. + [Fact] + public async Task A_throwing_credential_port_is_unavailable_and_opens_nothing() + { + using var factory = await ArrangeAsync(); + factory.Credential.Throws = true; + + foreach (var route in Routes) + { + var reply = await SendRouteAsync(factory, route, Chat, Token(factory)); + Assert.True(reply.Status == 503 && Code(reply) == "unavailable", $"{route}: {reply.Status}"); + } + + Assert.Empty(factory.Door.Calls); + } + + [Fact] + public async Task A_throwing_account_lookup_is_unavailable_and_opens_nothing() + { + using var factory = await ArrangeAsync(); + // The request's database context is gone by the time the desk looks the account up (the database failed). + factory.Credential.OnAuthenticate = http => http.RequestServices.GetRequiredService().Dispose(); + + foreach (var route in Routes) + { + var reply = await SendRouteAsync(factory, route, Chat, Token(factory)); + Assert.True(reply.Status == 503 && Code(reply) == "unavailable", $"{route}: {reply.Status}"); + } + + Assert.Empty(factory.Door.Calls); + } + + // The runtime guard on the decider list (the startup validator is the first layer; this is the second). + private static string Id(int n) => new Guid(n, 0, 0, new byte[8]).ToString("D"); + + private static void SetDeciders(DeskWebApplicationFactory factory, string mind, IEnumerable ids) + { + var list = factory.Services.GetRequiredService>().Value.Minds.Single(m => m.Id == mind).Deciders; + list.Clear(); + list.AddRange(ids); + } + + [Fact] + public async Task The_door_is_called_with_64_deciders() + { + using var factory = await ArrangeAsync(); + var ids = Enumerable.Range(1, 64).Select(Id).ToList(); + SetDeciders(factory, Chat, ids); + + foreach (var route in Routes) + { + factory.Door.Calls.Clear(); + var reply = await SendRouteAsync(factory, route, Chat, Token(factory)); + Assert.True(reply.Status == 200, route); + Assert.Equal(ids, Assert.Single(factory.Door.Calls).Context.Deciders); + } + } + + [Theory] + [InlineData("65")] + [InlineData("0")] + [InlineData("duplicate")] + public async Task A_decider_list_out_of_bounds_is_unavailable_without_a_door_call(string fault) + { + using var factory = await ArrangeAsync(); + var ids = fault switch + { + "65" => Enumerable.Range(1, 65).Select(Id).ToList(), + "0" => [], + _ => [Id(1), Id(2), Id(1)], + }; + SetDeciders(factory, Chat, ids); + + foreach (var route in Routes) + { + var reply = await SendRouteAsync(factory, route, Chat, Token(factory)); + Assert.True(reply.Status == 503 && Code(reply) == "unavailable", $"{route}: {reply.Status}"); + } + + Assert.Empty(factory.Door.Calls); + + // Only that Mind's list is at fault: the other Mind still answers. + var other = await SendRouteAsync(factory, "list", Nibu, Token(factory)); + Assert.Equal(200, other.Status); + } +} diff --git a/tests/Bifrost.Web.Tests/DeskMindRouteTests.cs b/tests/Bifrost.Web.Tests/DeskMindRouteTests.cs new file mode 100644 index 0000000..86b1835 --- /dev/null +++ b/tests/Bifrost.Web.Tests/DeskMindRouteTests.cs @@ -0,0 +1,91 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using Bifrost.Web.Features.Desk; +using Bifrost.Web.Tests.Support; + +namespace Bifrost.Web.Tests; + +public sealed class DeskMindRouteTests +{ + private static async Task<(DeskReply Reply, DeskWebApplicationFactory Factory)> RunAsync(JsonObject row) + { + var factory = new DeskWebApplicationFactory(); + DeskReplay.HoldVectorForks(factory.Door); + var token = await DeskReplay.TokenAsync(factory, row["token"]); + var route = (string)row["route"]!; + var forkId = (string?)row["fork_id"]; + var reply = await factory.SendAsync( + route == "answer" ? "POST" : "GET", + DeskReplay.Target(route, (string)row["mind"]!, forkId), + route == "answer" ? DeskReplay.AnswerBody(forkId!) : null, + token); + return (reply, factory); + } + + [Fact] + public async Task Vector_route_cases() + { + var cases = DeskVectors.Rows("transport", "mindRoutes", "cases").ToList(); + Assert.Equal(57, cases.Count); + + foreach (var row in cases) + { + var name = (string)row["name"]!; + var (reply, factory) = await RunAsync(row); + using (factory) + { + Assert.True((int)row["httpStatus"]! == reply.Status, $"{name}: {reply.Status} {reply.Text}"); + var expect = (string)row["expect"]!; + if (expect != "ok") + { + Assert.True( + DeskVectors.SameShape(JsonNode.Parse(reply.Body), JsonNode.Parse(JsonSerializer.Serialize(DeskRefusals.Document(expect), DeskJson.Options))), + name); + } + + var opened = (string?)row["opened"]; + if (opened is null) + { + Assert.True(factory.Door.Calls.Count == 0, $"{name}: opened {factory.Door.Calls.FirstOrDefault()?.Socket}"); + } + else + { + var call = Assert.Single(factory.Door.Calls); + Assert.True(call.Socket.EndsWith("/" + opened, StringComparison.Ordinal), $"{name}: {call.Socket}"); + } + + var storedIn = (string?)row["stored_in"]; + if (storedIn is null) + { + Assert.Empty(factory.Door.StoredIn); + } + else + { + Assert.Equal([DeskWebApplicationFactory.SocketOf(storedIn)], factory.Door.StoredIn); + } + } + } + } + + [Fact] + public async Task Same_response_pairs_are_byte_equal() + { + var cases = DeskVectors.Rows("transport", "mindRoutes", "cases").ToDictionary(r => (string)r["name"]!); + var pairs = DeskVectors.Array("transport", "mindRoutes", "sameResponse"); + Assert.Equal(4, pairs.Count); + + foreach (var pair in pairs) + { + var first = cases[(string)pair![0]!]; + var second = cases[(string)pair[1]!]; + var (a, fa) = await RunAsync(first); + var (b, fb) = await RunAsync(second); + using (fa) + using (fb) + { + Assert.Equal(a.Status, b.Status); + Assert.Equal(a.Body, b.Body); + } + } + } +} diff --git a/tests/Bifrost.Web.Tests/DeskMindRoutesTests.cs b/tests/Bifrost.Web.Tests/DeskMindRoutesTests.cs new file mode 100644 index 0000000..f8d9604 --- /dev/null +++ b/tests/Bifrost.Web.Tests/DeskMindRoutesTests.cs @@ -0,0 +1,125 @@ +using Bifrost.Web.Configuration; +using Bifrost.Web.Features.Desk; +using Bifrost.Web.Tests.Support; + +namespace Bifrost.Web.Tests; + +public sealed class DeskMindRoutesTests +{ + private static DeskMindRoutes Routes() + { + var minds = DeskVectors.Root["transport"]!["mindRoutes"]!["minds"]!.AsObject(); + return new DeskMindRoutes(minds.Select(m => new DeskMindOptions { Id = m.Key, Socket = "/run/huginn/" + (string)m.Value! })); + } + + [Fact] + public void Every_vector_segment_resolves_or_misses() + { + var routes = Routes(); + var cases = DeskVectors.Rows("transport", "mindRoutes", "cases").ToList(); + Assert.Equal(57, cases.Count); + + foreach (var row in cases) + { + var mind = (string)row["mind"]!; + var opened = (string?)row["opened"]; + var resolved = routes.Resolve(mind); + + // A route that opens a socket resolves to the Mind whose socket it is; every other route is a miss. + // Auth-refused cases never reach the lookup, so only the cases that reach it are judged here. + var expect = (string)row["expect"]!; + if (expect is not ("ok" or "fork_unknown" or "unavailable")) + { + continue; + } + + if (opened is null) + { + Assert.True(resolved is null, $"{row["name"]}: '{Short(mind)}' resolved to '{resolved?.Id}'"); + } + else + { + Assert.True(resolved is not null, $"{row["name"]}: '{Short(mind)}' missed"); + Assert.EndsWith(opened, resolved!.Socket, StringComparison.Ordinal); + } + } + } + + [Fact] + public void Segments_that_do_not_name_a_configured_mind_miss() + { + var routes = Routes(); + foreach (var mind in new[] + { + "ghost", ".", "..", "../x", "a/b", "a%2fb", "%2e%2e%2fx", "eureka%00", "nibu%20", "nibu ", " nibu", + "%ff", "%c3%28", "%", "%6", "%zz", "nibu%", "%256eibu", "", new string('a', 4096), "nïbu", + }) + { + Assert.Null(routes.Resolve(mind)); + } + } + + [Fact] + public void Percent_encoded_configured_ids_are_decoded_once() + { + var routes = Routes(); + + Assert.Equal("nibu", routes.Resolve("%6eibu")!.Id); + Assert.Equal("nibu", routes.Resolve("%6Eibu")!.Id); + Assert.Equal("gamecult-chat", routes.Resolve("gamecult%2Dchat")!.Id); + } + + [Fact] + public void Other_case_misses_despite_config_case_insensitivity() + { + var routes = Routes(); + + Assert.Null(routes.Resolve("GAMECULT-CHAT")); + Assert.Null(routes.Resolve("Nibu")); + Assert.Null(routes.Resolve("gamecult-chat%20")); + Assert.NotNull(routes.Resolve("gamecult-chat")); + } + + [Fact] + public void Double_encoded_misses() + { + var routes = Routes(); + + Assert.Null(routes.Resolve("%256eibu")); + Assert.Null(routes.Resolve("gamecult%252Dchat")); + } + + [Fact] + public void Configured_ids_that_differ_only_by_case_stay_distinct() + { + var routes = new DeskMindRoutes([ + new DeskMindOptions { Id = "a", Socket = "/s/a" }, + new DeskMindOptions { Id = "A", Socket = "/s/A" }, + ]); + + Assert.Equal("/s/a", routes.Resolve("a")!.Socket); + Assert.Equal("/s/A", routes.Resolve("A")!.Socket); + } + + [Fact] + public void A_decoding_failure_is_a_miss_even_when_the_failure_spells_a_configured_id() + { + var replacement = char.ConvertFromUtf32(0xFFFD); + var e = char.ConvertFromUtf32(0xE9); + var routes = new DeskMindRoutes([ + new DeskMindOptions { Id = replacement, Socket = "/s/replacement" }, + new DeskMindOptions { Id = e, Socket = "/s/e-acute" }, + new DeskMindOptions { Id = "a%zz", Socket = "/s/bad-escape" }, + new DeskMindOptions { Id = "100%", Socket = "/s/trailing" }, + ]); + + Assert.Null(routes.Resolve("%ff")); // not UTF-8, not U+FFFD + Assert.Null(routes.Resolve(char.ConvertFromUtf32(0xC3) + char.ConvertFromUtf32(0xA9))); // UTF-8 bytes as raw chars + Assert.Null(routes.Resolve("a%zz")); + Assert.Null(routes.Resolve("100%")); + Assert.Equal("/s/replacement", routes.Resolve("%ef%bf%bd")!.Socket); // the real encoding still resolves + Assert.Equal("/s/e-acute", routes.Resolve("%c3%a9")!.Socket); + } + + private static string Short(string value) => value.Length > 40 ? value[..40] + "..." : value; +} diff --git a/tests/Bifrost.Web.Tests/DeskOptionsTests.cs b/tests/Bifrost.Web.Tests/DeskOptionsTests.cs new file mode 100644 index 0000000..8b09244 --- /dev/null +++ b/tests/Bifrost.Web.Tests/DeskOptionsTests.cs @@ -0,0 +1,234 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Bifrost.Web.Configuration; +using Bifrost.Web.Features.Desk; +using Bifrost.Web.Features.Shared; +using Microsoft.Extensions.Options; + +namespace Bifrost.Web.Tests; + +public sealed class DeskOptionsTests +{ + private const string Alice = "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f"; + private const string Bob = "00000000-0000-4000-8000-000000000002"; + + private static Dictionary Good() => new() + { + ["Desk:AllowedOrigin"] = "https://chat.gamecult.org", + ["Desk:Minds:0:Id"] = "gamecult-chat", + ["Desk:Minds:0:Socket"] = "/run/huginn/huginn-gamecult-chat.sock", + ["Desk:Minds:0:Deciders:0"] = Alice, + ["Desk:Minds:1:Id"] = "nibu", + ["Desk:Minds:1:Socket"] = "/run/huginn/huginn-nibu.sock", + ["Desk:Minds:1:Deciders:0"] = Bob, + ["Desk:Attribution:Operator"] = Alice, + ["Desk:Attribution:Stewards:0"] = Bob, + }; + + private static DeskOptions Bind(Dictionary values) + { + var options = new DeskOptions(); + new ConfigurationBuilder().AddInMemoryCollection(values).Build().GetSection(DeskOptions.SectionName).Bind(options); + return options; + } + + private static void Validate(Dictionary values, string environment = "Testing") + { + var configuration = new ConfigurationBuilder().AddInMemoryCollection(values).Build(); + new StartupConfigurationValidator( + configuration, + new StubEnvironment(environment), + Options.Create(new GitHubOAuthOptions()), + Options.Create(new GitHubAppOptions()), + Options.Create(new HeimdallOptions()), + Options.Create(new StripeOptions()), + Options.Create(new BifrostHostOptions()), + Options.Create(new ConceptVoteOptions()), + Options.Create(Bind(values))).Validate(); + } + + private static void Refused(Dictionary values, string expectedFragment) + { + var failure = Assert.Throws(() => Validate(values)); + Assert.Contains(expectedFragment, failure.Message); + } + + [Fact] + public void A_good_desk_configuration_binds_by_position_and_passes() + { + var options = Bind(Good()); + + Assert.Equal(["gamecult-chat", "nibu"], options.Minds.Select(m => m.Id)); + Assert.Equal([Alice], options.Minds[0].Deciders); + Assert.Equal([Bob], options.Minds[1].Deciders); + Validate(Good()); + } + + [Fact] + public void No_desk_section_is_not_configured_and_passes() + { + Assert.False(Bind([]).IsConfigured); + Validate([]); + } + + [Fact] + public void Mind_ids_keep_the_case_the_configuration_gave_them() + { + var values = Good(); + values["Desk:Minds:0:Id"] = "GameCult-Chat"; + + Assert.Equal("GameCult-Chat", Bind(values).Minds[0].Id); + } + + [Fact] + public void Startup_refuses_duplicate_ids() => Refused(Dup(), "Desk:Minds:1:Id repeats"); + + private static Dictionary Dup() + { + var values = Good(); + values["Desk:Minds:1:Id"] = "gamecult-chat"; + return values; + } + + [Fact] + public void Startup_refuses_a_blank_id() + { + var values = Good(); + values["Desk:Minds:1:Id"] = " "; + + Refused(values, "Desk:Minds:1:Id must not be blank"); + } + + [Theory] + [InlineData("huginn.sock")] + [InlineData("run/huginn.sock")] + [InlineData("")] + public void Startup_refuses_relative_socket(string socket) + { + var values = Good(); + values["Desk:Minds:1:Socket"] = socket; + + Refused(values, "Desk:Minds:1:Socket must be an absolute path"); + } + + [Theory] + [InlineData("E0D1C2B3-A495-4687-8879-6A5B4C3D2E1F")] + [InlineData("e0d1c2b3a495468788796a5b4c3d2e1f")] + [InlineData("{e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f}")] + [InlineData("not-a-guid")] + [InlineData("")] + public void Startup_refuses_bad_decider_id(string decider) + { + var values = Good(); + values["Desk:Minds:0:Deciders:1"] = decider; + + Refused(values, "Desk:Minds:0:Deciders entries must be lowercase D-format"); + } + + [Fact] + public void Startup_refuses_a_repeated_decider() + { + var values = Good(); + values["Desk:Minds:0:Deciders:1"] = Alice; + + Refused(values, "Desk:Minds:0:Deciders repeats"); + } + + [Fact] + public void Startup_refuses_operator_in_stewards() + { + var values = Good(); + values["Desk:Attribution:Stewards:1"] = Alice; + + Refused(values, "must not also be a steward"); + } + + [Fact] + public void Startup_refuses_bad_or_repeated_attribution_ids() + { + var bad = Good(); + bad["Desk:Attribution:Operator"] = "ALICE"; + Refused(bad, "Desk:Attribution:Operator must be a lowercase"); + + var badSteward = Good(); + badSteward["Desk:Attribution:Stewards:1"] = "E0D1C2B3-A495-4687-8879-6A5B4C3D2E1F"; + Refused(badSteward, "Desk:Attribution:Stewards entries must be lowercase"); + + var repeated = Good(); + repeated["Desk:Attribution:Stewards:1"] = Bob; + Refused(repeated, "Desk:Attribution:Stewards repeats"); + } + + private static string Id(int n) => new Guid(n, 0, 0, new byte[8]).ToString("D"); + + private static Dictionary WithDeciders(int count) + { + var values = Good(); + for (var i = 0; i < count; i++) + { + values[$"Desk:Minds:0:Deciders:{i}"] = Id(i + 1); + } + + if (count == 0) + { + values.Remove("Desk:Minds:0:Deciders:0"); + } + + return values; + } + + [Fact] + public void Startup_takes_one_to_64_deciders_per_mind() + { + Validate(WithDeciders(1)); + Validate(WithDeciders(64)); + Refused(WithDeciders(65), "Desk:Minds:0:Deciders must hold 1 to 64"); + Refused(WithDeciders(0), "Desk:Minds:0:Deciders must hold 1 to 64"); + + // The bound is per Mind: the second Mind is judged on its own list. + var second = Good(); + second.Remove("Desk:Minds:1:Deciders:0"); + Refused(second, "Desk:Minds:1:Deciders must hold 1 to 64"); + } + + [Fact] + public void Startup_refuses_two_minds_sharing_one_socket() + { + var values = Good(); + values["Desk:Minds:1:Socket"] = values["Desk:Minds:0:Socket"]; + + Refused(values, "Desk:Minds:1:Socket repeats an earlier Mind's socket"); + } + + [Fact] + public void Desk_failures_are_reported_in_production_too_and_alongside_each_other() + { + var values = Good(); + values["Desk:Minds:1:Socket"] = "relative"; + values["Desk:Attribution:Stewards:1"] = Alice; + + var failure = Assert.Throws(() => Validate(values, "Production")); + Assert.Contains("Desk:Minds:1:Socket", failure.Message); + Assert.Contains("must not also be a steward", failure.Message); + } + + [Fact] + public void Attribution_is_operator_steward_member() + { + var attribution = Bind(Good()).Attribution; + + Assert.Equal("Operator", attribution.AuthorityOf(Alice)); + Assert.Equal("Steward", attribution.AuthorityOf(Bob)); + Assert.Equal("Member", attribution.AuthorityOf("00000000-0000-4000-8000-000000000003")); + Assert.Equal("Member", attribution.AuthorityOf(Alice.ToUpperInvariant())); + Assert.Equal("Member", new DeskAttributionOptions().AuthorityOf(string.Empty)); + } + + private sealed class StubEnvironment(string name) : IHostEnvironment + { + public string EnvironmentName { get; set; } = name; + public string ApplicationName { get; set; } = "Bifrost.Web"; + public string ContentRootPath { get; set; } = string.Empty; + public Microsoft.Extensions.FileProviders.IFileProvider ContentRootFileProvider { get; set; } = null!; + } +} diff --git a/tests/Bifrost.Web.Tests/DeskReadTests.cs b/tests/Bifrost.Web.Tests/DeskReadTests.cs new file mode 100644 index 0000000..a713433 --- /dev/null +++ b/tests/Bifrost.Web.Tests/DeskReadTests.cs @@ -0,0 +1,354 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using Bifrost.Web.Features.Desk; +using Bifrost.Web.Tests.Support; + +namespace Bifrost.Web.Tests; + +public sealed class DeskReadTests +{ + private static string ReadToken(DeskWebApplicationFactory f) => + f.Token(DeskWebApplicationFactory.Alice, "bifrost-desk", "desk.read"); + + private static Uri Unused => new("http://unused"); + + // A list_request invalid row names the query that cannot express it; the others are the shapes a query cannot hold. + private static readonly Dictionary InvalidListQueries = new() + { + ["limit_51"] = "?limit=51", + ["limit_negative"] = "?limit=-1", + ["status_all"] = "?status=all", + ["cursor_malformed"] = "?cursor=next", + ["cursor_zero"] = "?cursor=c0", + ["unknown_key"] = "?sort=oldest", + }; + + [Fact] + public async Task List_and_read_request_invalid_vectors() + { + using var factory = new DeskWebApplicationFactory(); + DeskReplay.HoldVectorForks(factory.Door); + await factory.EnsureAccountAsync(DeskWebApplicationFactory.Alice, true); + var token = ReadToken(factory); + + var invalid = DeskVectors.Rows("transport", "listRequest", "invalid").Select(r => (string)r["name"]!).ToList(); + foreach (var name in invalid.Where(n => n != "mind_empty")) + { + var reply = await factory.SendAsync("GET", "/api/desk/gamecult-chat/forks" + InvalidListQueries[name], null, token); + Assert.True(reply.Status == 400, $"{name}: {reply.Status}"); + Assert.Equal("request_invalid", (string?)JsonNode.Parse(reply.Body)!["code"]); + } + + // Also: repeated key, bad escape, query on a read, empty fork id. + foreach (var target in new[] + { + "/api/desk/gamecult-chat/forks?limit=1&limit=2", "/api/desk/gamecult-chat/forks?limit=%zz", "/api/desk/gamecult-chat/forks?limit=", + "/api/desk/gamecult-chat/forks?limit=+1", "/api/desk/gamecult-chat/forks?limit=1.0", + "/api/desk/gamecult-chat/forks/gamecult-chat%3Aquestion%3Aq1?x=1", "/api/desk/gamecult-chat/forks/", "/api/desk/gamecult-chat/forks/%ff", + }) + { + var reply = await factory.SendAsync("GET", target, null, token); + Assert.True(reply.Status == 400, $"{target}: {reply.Status}"); + } + + // The invalid request is judged before the Mind: an unknown Mind with a bad query is the query's refusal. + var order = await factory.SendAsync("GET", "/api/desk/ghost/forks?limit=99", null, token); + Assert.Equal("request_invalid", (string?)JsonNode.Parse(order.Body)!["code"]); + Assert.Empty(factory.Door.Calls); + } + + [Fact] + public async Task Valid_list_queries_reach_the_door_as_typed_requests() + { + using var factory = new DeskWebApplicationFactory(); + await factory.EnsureAccountAsync(DeskWebApplicationFactory.Alice, true); + + foreach (var row in DeskVectors.Rows("transport", "listRequest", "valid")) + { + var m = row["message"]!; + var query = $"?status={(string?)m["status"]}&cursor={(string?)m["cursor"]}&limit={(int)m["limit"]!}"; + var reply = await factory.SendAsync("GET", "/api/desk/gamecult-chat/forks" + query, null, ReadToken(factory)); + Assert.Equal(200, reply.Status); + var call = factory.Door.Calls[^1]; + Assert.Equal(new DeskListRequest("gamecult-chat", (string?)m["status"], (string?)m["cursor"], (int)m["limit"]!), call.List); + } + + var bare = await factory.SendAsync("GET", "/api/desk/nibu/forks", null, ReadToken(factory)); + Assert.Equal(new DeskListRequest("nibu", null, null, 50), factory.Door.Calls[^1].List); + Assert.Equal(200, bare.Status); + } + + [Fact] + public async Task List_scenarios_pass_through() + { + var steps = 0; + foreach (var scenario in DeskVectors.Rows("transport", "listScenarios")) + { + using var factory = new DeskWebApplicationFactory(); + await factory.EnsureAccountAsync(DeskWebApplicationFactory.Alice, true); + foreach (var step in scenario["steps"]!.AsArray().Select(s => s!.AsObject())) + { + var request = step["request"]!; + var response = step["response"]!.AsObject(); + var list = response.Deserialize(DeskJson.Options)!; + factory.Door.ListScript = (_, _) => new DeskListOutcome.Listed(list); + var query = $"?status={(string?)request["status"]}&cursor={(string?)request["cursor"]}&limit={(int)request["limit"]!}"; + + var reply = await factory.SendAsync("GET", $"/api/desk/{(string)request["mind"]!}/forks" + query, null, ReadToken(factory)); + + Assert.Equal(200, reply.Status); + Assert.True(DeskVectors.SameShape(JsonNode.Parse(reply.Body), response), (string)scenario["name"]!); + steps++; + } + } + + Assert.Equal(7, steps); + } + + [Fact] + public async Task Read_returns_the_entry_unchanged_and_an_unknown_fork_is_refused() + { + using var factory = new DeskWebApplicationFactory(); + DeskReplay.HoldVectorForks(factory.Door); + await factory.EnsureAccountAsync(DeskWebApplicationFactory.Alice, true); + + var ok = await factory.SendAsync("GET", DeskReplay.Target("read", "gamecult-chat", DeskReplay.ChatFork), null, ReadToken(factory)); + var entry = JsonSerializer.SerializeToNode(DeskReplay.Entry(DeskReplay.Card(DeskReplay.ChatFork)), DeskJson.Options); + var missing = await factory.SendAsync("GET", DeskReplay.Target("read", "gamecult-chat", "gamecult-chat:question:ghost"), null, ReadToken(factory)); + + Assert.Equal(200, ok.Status); + Assert.True(DeskVectors.SameShape(JsonNode.Parse(ok.Body), entry)); + Assert.Equal(DeskReplay.ChatFork, Assert.IsType(factory.Door.Calls[0].ForkId)); + Assert.Equal(404, missing.Status); + } + + [Fact] + public async Task A_fork_too_large_for_a_page_is_a_422_that_names_the_fork_and_the_cursor_to_continue_with() + { + using var factory = new DeskWebApplicationFactory(); + await factory.EnsureAccountAsync(DeskWebApplicationFactory.Alice, true); + + factory.Door.ListScript = (_, _) => new DeskListOutcome.ForkTooLarge("nibu:question:huge", 7); + var reply = await factory.SendAsync("GET", "/api/desk/nibu/forks", null, ReadToken(factory)); + var doc = JsonNode.Parse(reply.Body)!; + Assert.Equal(422, reply.Status); + Assert.Equal("fork_too_large", (string?)doc["code"]); + var error = (string)doc["error"]!; + Assert.Contains("nibu:question:huge", error); + Assert.Contains("order 7", error); + Assert.Contains("c7", error); + + // At order 0 there is no cursor to continue with: the list has reached its end. + factory.Door.ListScript = (_, _) => new DeskListOutcome.ForkTooLarge("nibu:question:first", 0); + var end = await factory.SendAsync("GET", "/api/desk/nibu/forks", null, ReadToken(factory)); + var endError = (string)JsonNode.Parse(end.Body)!["error"]!; + Assert.Equal(422, end.Status); + Assert.Contains("nibu:question:first", endError); + Assert.Contains("order 0", endError); + Assert.DoesNotContain("cursor c0", endError); + Assert.Contains("end", endError); + } + + [Fact] + public async Task fork_too_large_is_list_only_elsewhere_the_door_saying_it_is_unavailable() + { + using var factory = new DeskWebApplicationFactory(); + await factory.EnsureAccountAsync(DeskWebApplicationFactory.Alice, true); + + factory.Door.ReadScript = (_, _) => new DeskReadOutcome.NotFound("fork_too_large"); + factory.Door.ListScript = (_, _) => new DeskListOutcome.NotListed("fork_too_large"); + foreach (var target in new[] { "/api/desk/nibu/forks", DeskReplay.Target("read", "nibu", "nibu:question:glow-order") }) + { + var reply = await factory.SendAsync("GET", target, null, ReadToken(factory)); + Assert.Equal(503, reply.Status); + } + } + + [Fact] + public async Task A_short_page_with_a_cursor_below_its_last_entry_passes_through_unchanged() + { + using var factory = new DeskWebApplicationFactory(); + await factory.EnsureAccountAsync(DeskWebApplicationFactory.Alice, true); + factory.Door.ListScript = (_, r) => new DeskListOutcome.Listed(new ForkList("gamecult.fork.list.v0", r.Mind, "s1", 3, 0, [], "c2")); + + var reply = await factory.SendAsync("GET", "/api/desk/nibu/forks?limit=10", null, ReadToken(factory)); + + Assert.Equal(200, reply.Status); + Assert.Equal("c2", (string?)JsonNode.Parse(reply.Body)!["next_cursor"]); + } + + [Fact] + public async Task A_door_that_refuses_a_read_or_list_for_a_body_field_such_as_authority_is_request_invalid_not_unavailable() + { + using var factory = new DeskWebApplicationFactory(); + await factory.EnsureAccountAsync(DeskWebApplicationFactory.Alice, true); + + foreach (var code in new[] { "body_forbidden_field", "body_unknown_field", "body_invalid" }) + { + factory.Door.ReadScript = (_, _) => new DeskReadOutcome.NotFound(code); + factory.Door.ListScript = (_, _) => new DeskListOutcome.NotListed(code); + foreach (var target in new[] { "/api/desk/nibu/forks", DeskReplay.Target("read", "nibu", "nibu:question:glow-order") }) + { + var reply = await factory.SendAsync("GET", target, null, ReadToken(factory)); + Assert.Equal(400, reply.Status); + Assert.Equal("request_invalid", (string?)JsonNode.Parse(reply.Body)!["code"]); + } + } + } + + [Fact] + public async Task request_id_conflict_is_the_answers_alone_on_read_and_list_it_is_a_door_fault() + { + using var factory = new DeskWebApplicationFactory(); + await factory.EnsureAccountAsync(DeskWebApplicationFactory.Alice, true); + + factory.Door.ReadScript = (_, _) => new DeskReadOutcome.NotFound(DeskRefusals.RequestIdConflict); + factory.Door.ListScript = (_, _) => new DeskListOutcome.NotListed(DeskRefusals.RequestIdConflict); + foreach (var target in new[] { "/api/desk/nibu/forks", DeskReplay.Target("read", "nibu", "nibu:question:glow-order") }) + { + var reply = await factory.SendAsync("GET", target, null, ReadToken(factory)); + Assert.Equal(503, reply.Status); + Assert.Equal("unavailable", (string?)JsonNode.Parse(reply.Body)!["code"]); + } + } + + [Fact] + public async Task Door_failure_on_list_and_read_is_unavailable() + { + using var factory = new DeskWebApplicationFactory(); + await factory.EnsureAccountAsync(DeskWebApplicationFactory.Alice, true); + factory.Door.Down = true; + + foreach (var target in new[] { "/api/desk/nibu/forks", DeskReplay.Target("read", "nibu", "nibu:question:glow-order") }) + { + var reply = await factory.SendAsync("GET", target, null, ReadToken(factory)); + Assert.Equal(503, reply.Status); + Assert.Equal(30, (int?)JsonNode.Parse(reply.Body)!["retryAfterSeconds"]); + } + } + + [Fact] + public async Task Every_door_call_carries_the_configured_deciders_and_nothing_in_the_request_can_change_them() + { + using var factory = new DeskWebApplicationFactory(); + DeskReplay.HoldVectorForks(factory.Door); + await factory.EnsureAccountAsync(DeskWebApplicationFactory.Alice, true); + var hostile = "deciders=" + DeskWebApplicationFactory.Member; + Action headers = http => + { + http.Request.Headers["X-Deciders"] = DeskWebApplicationFactory.Member; + http.Request.Headers["Deciders"] = DeskWebApplicationFactory.Member; + }; + var read = ReadToken(factory); + var answer = factory.Token(DeskWebApplicationFactory.Alice, "bifrost-desk", "desk.answer"); + + await factory.SendAsync("GET", "/api/desk/nibu/forks", null, read, configure: headers); + var hostileList = await factory.SendAsync("GET", "/api/desk/nibu/forks?" + hostile, null, read, configure: headers); + Assert.Equal(400, hostileList.Status); + await factory.SendAsync("GET", DeskReplay.Target("read", "nibu", "nibu:question:glow-order"), null, read, configure: headers); + await factory.SendAsync("POST", DeskReplay.Target("answer", "nibu", null) + "?" + hostile, DeskReplay.AnswerBody("nibu:question:glow-order"), answer, configure: headers); + // A body naming deciders never reaches the door. + var body = JsonNode.Parse(DeskReplay.AnswerBody("nibu:question:glow-order"))!.AsObject(); + body["deciders"] = new JsonArray(DeskWebApplicationFactory.Member); + var named = await factory.SendAsync("POST", DeskReplay.Target("answer", "nibu", null), DeskVectors.Bytes(body), answer); + + Assert.Equal("body_unknown_field", (string?)JsonNode.Parse(named.Body)!["code"]); + Assert.Equal(["list", "read", "answer"], factory.Door.Calls.Select(c => c.Kind).ToArray()); + Assert.All(factory.Door.Calls, c => Assert.Equal(DeskWebApplicationFactory.DecidersOf("nibu"), c.Context.Deciders)); + } + private const string Secret = "the decider's own words"; + + private static DeskAnswerDocument Doc(string id, string outcome, string? of, string? retracts, string? note) => new( + DeskSchemas.DeskAnswer, id, DeskReplay.ChatFork, outcome, of, retracts, null, "h", note == null ? null : "a", note, + "2026-11-12T19:00:00Z", "2026-11-12T19:00:01Z", false, DeskWebApplicationFactory.Alice, "Steward", "r-" + id); + + // Counted, Revision, Retraction and Refused documents, every one carrying the decider's note. + private static ForkEntry NotedEntry() + { + var entry = DeskReplay.Entry(DeskReplay.Card(DeskReplay.ChatFork)); + DeskAnswerDocument[] answers = + [ + Doc("n1", "Counted", null, null, Secret), + Doc("n2", "Revision", "n1", null, Secret + " 2"), + Doc("n3", "Retraction", null, "n2", Secret + " 3"), + Doc("n4", "Refused", null, null, Secret + " 4"), + ]; + return entry with { Record = entry.Record with { CountedAnswerId = "n1", Answers = answers } }; + } + + private static bool AnyNote(JsonNode? node) => node switch + { + JsonObject o => o.Any(kv => (kv.Key == "note" && kv.Value is not null) || AnyNote(kv.Value)), + JsonArray a => a.Any(AnyNote), + _ => false, + }; + + private static async Task<(string Read, string List)> BodiesAsync(DeskWebApplicationFactory factory, string account) + { + var entry = NotedEntry(); + factory.Door.ReadScript = (_, _) => new DeskReadOutcome.Found(entry); + factory.Door.ListScript = (_, r) => new DeskListOutcome.Listed( + new ForkList("gamecult.fork.list.v0", r.Mind, "s1", 1, 0, [entry, entry], null)); + var token = factory.Token(account, "bifrost-desk", "desk.read"); + var read = await factory.SendAsync("GET", DeskReplay.Target("read", "gamecult-chat", DeskReplay.ChatFork), null, token); + var list = await factory.SendAsync("GET", DeskReplay.Target("list", "gamecult-chat", null), null, token); + Assert.Equal(200, read.Status); + Assert.Equal(200, list.Status); + return (System.Text.Encoding.UTF8.GetString(read.Body), System.Text.Encoding.UTF8.GetString(list.Body)); + } + + [Fact] + public async Task A_non_decider_reads_and_lists_no_notes_anywhere_and_the_body_keeps_the_contract_shape() + { + using var factory = new DeskWebApplicationFactory(); + await factory.EnsureAccountAsync(DeskWebApplicationFactory.Member, true); + Assert.DoesNotContain(DeskWebApplicationFactory.Member, DeskWebApplicationFactory.DecidersOf("gamecult-chat")); + + var (read, list) = await BodiesAsync(factory, DeskWebApplicationFactory.Member); + + Assert.DoesNotContain("decider's own words", read); + Assert.DoesNotContain("decider's own words", list); + Assert.False(AnyNote(JsonNode.Parse(read))); + Assert.False(AnyNote(JsonNode.Parse(list))); + // Outcomes and cards stay; the key stays written as nil, so the body is still the contract's shape. + var expected = JsonSerializer.SerializeToNode(NotedEntry() with + { + Record = NotedEntry().Record with { Answers = [.. NotedEntry().Record.Answers.Select(a => a with { Note = null })] }, + }, DeskJson.Options); + Assert.True(DeskVectors.SameShape(JsonNode.Parse(read), expected)); + var answers = JsonNode.Parse(read)!["record"]!["answers"]!.AsArray(); + Assert.Equal(new[] { "Counted", "Revision", "Retraction", "Refused" }, answers.Select(a => (string)a!["outcome"]!).ToArray()); + Assert.All(answers, a => Assert.True(a!.AsObject().ContainsKey("note"))); + Assert.Equal(2, JsonNode.Parse(list)!["forks"]!.AsArray().Count); + } + + [Fact] + public async Task A_decider_reads_and_lists_the_notes() + { + using var factory = new DeskWebApplicationFactory(); + await factory.EnsureAccountAsync(DeskWebApplicationFactory.Alice, true); + + var (read, list) = await BodiesAsync(factory, DeskWebApplicationFactory.Alice); + + Assert.Contains(Secret + " 3", read); + Assert.Equal(8, JsonNode.Parse(list)!["forks"]!.AsArray().Sum(f => f!["record"]!["answers"]!.AsArray().Count(a => a!["note"] is not null))); + Assert.True(DeskVectors.SameShape(JsonNode.Parse(read), JsonSerializer.SerializeToNode(NotedEntry(), DeskJson.Options))); + } + + [Fact] + public async Task Decider_status_is_per_mind_the_same_account_is_stripped_where_it_is_not_on_the_list() + { + using var factory = new DeskWebApplicationFactory(); + await factory.EnsureAccountAsync(DeskWebApplicationFactory.Member, true); + Assert.Contains(DeskWebApplicationFactory.Member, DeskWebApplicationFactory.DecidersOf("nibu")); + factory.Door.ListScript = (_, r) => new DeskListOutcome.Listed( + new ForkList("gamecult.fork.list.v0", r.Mind, "s1", 1, 0, [NotedEntry()], null)); + var token = factory.Token(DeskWebApplicationFactory.Member, "bifrost-desk", "desk.read"); + + var onNibu = await factory.SendAsync("GET", "/api/desk/nibu/forks", null, token); + var onChat = await factory.SendAsync("GET", "/api/desk/gamecult-chat/forks", null, token); + + Assert.True(AnyNote(JsonNode.Parse(onNibu.Body))); + Assert.False(AnyNote(JsonNode.Parse(onChat.Body))); + } +} diff --git a/tests/Bifrost.Web.Tests/DeskScenarioTests.cs b/tests/Bifrost.Web.Tests/DeskScenarioTests.cs new file mode 100644 index 0000000..9955bdb --- /dev/null +++ b/tests/Bifrost.Web.Tests/DeskScenarioTests.cs @@ -0,0 +1,188 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using Bifrost.Web.Features.Desk; +using Bifrost.Web.Tests.Support; + +namespace Bifrost.Web.Tests; + +public sealed class DeskScenarioTests +{ + private static readonly string Socket = DeskWebApplicationFactory.SocketOf("gamecult-chat"); + + private static readonly string[] ConfiguredDeciders = DeskWebApplicationFactory.DecidersOf("gamecult-chat"); + + // Replays every answer step of every scenario. The door is scripted from the vector's response; what Bifrost + // must produce by itself (edge refusals, unavailable, the attached card) is compared with the vector. + private sealed record Counts(int Answers, int Edge, int Unavailable, int Skipped); + + private static async Task ReplayAsync(Action? observe = null) + { + int answers = 0, edge = 0, unavailable = 0, skipped = 0; + foreach (var scenario in DeskVectors.Rows("scenarios")) + { + using var factory = new DeskWebApplicationFactory(); + var door = factory.Door; + door.Forks[(Socket, DeskReplay.ChatFork)] = DeskReplay.Entry(scenario["card"]!.Deserialize(DeskJson.Options)!); + var scenarioName = (string)scenario["name"]!; + + foreach (var step in scenario["steps"]!.AsArray().Select(s => s!.AsObject())) + { + var kind = (string)step["kind"]!; + if (kind == "matrix_event") + { + skipped++; // no Matrix ingress exists, so there is no route a Matrix event could take + continue; + } + + if (kind == "card") + { + door.Forks[(Socket, DeskReplay.ChatFork)] = DeskReplay.Entry(step["card"]!.Deserialize(DeskJson.Options)!); + continue; + } + + var server = step["server"]!.AsObject(); + var response = step["response"]!.AsObject(); + var code = (string?)response["code"]; + var answerer = (string?)server["answerer"]; + var active = (bool?)server["active"] ?? true; + + door.Calls.Clear(); + door.StoredIn.Clear(); + door.Down = false; + door.ThrowAfterStore = false; + door.AnswerScript = null; + if (answerer is not null) + { + await factory.EnsureAccountAsync(answerer, active); + } + + var isDown = (bool?)server["door_down"] == true; + var stored = (bool?)server["stored"] == true; + if (isDown) + { + door.Down = !stored; + door.ThrowAfterStore = stored; + } + + if (code is null) + { + var wire = response.Deserialize(DeskJson.Options)!; + // Huginn answers without the card; Bifrost attaches the current one for stale_fork. + door.AnswerScript = (_, _, _) => new DeskAnswerOutcome.Answered(wire with { Card = null }); + } + else if (code is "request_id_conflict" or "fork_unknown") + { + door.AnswerScript = (_, _, _) => new DeskAnswerOutcome.NotStored(code); + } + + var body = DeskVectors.Bytes(step["body"]!); + var token = answerer is null + ? null + : factory.Token(answerer, "bifrost-desk", "desk.answer"); + var reply = await factory.SendAsync("POST", DeskReplay.Target("answer", "gamecult-chat", null), body, token); + + var label = $"{scenarioName}"; + if (code == "unavailable") + { + unavailable++; + Assert.True(503 == reply.Status, $"{label}: {reply.Status}"); + Assert.Equal("30", reply.Headers.RetryAfter.ToString()); + Assert.Equal(stored ? [Socket] : [], door.StoredIn); + } + else if (code is not null && DeskReplay.IsEdgeRefusal(code)) + { + edge++; + Assert.True(DeskRefusals.StatusOf(code) == reply.Status, $"{label}: {reply.Status}"); + Assert.True(door.Calls.Count == 0, $"{label}: the door was called for an edge refusal"); + } + else + { + answers++; + var call = Assert.Single(door.Calls.Where(c => c.Kind == "answer")); + Assert.Equal(body, call.Body); + Assert.Equal(answerer, call.Context.Subject); + Assert.Equal((string)server["authority"]!, call.Context.Authority); + Assert.Equal("gamecult-chat", call.Context.Mind); + Assert.Equal(ConfiguredDeciders, call.Context.Deciders); + Assert.Equal(Socket, call.Socket); + } + + Assert.True( + DeskVectors.SameShape(JsonNode.Parse(reply.Body), response), + $"{label}: response differs\n{reply.Text}\n{response.ToJsonString()}"); + observe?.Invoke(scenarioName, reply, factory, step); + } + } + + return new Counts(answers, edge, unavailable, skipped); + } + + [Fact] + public async Task Every_scenario_answer_step_replays_through_http() + { + var counts = await ReplayAsync(); + + Assert.Equal(3, counts.Skipped); + Assert.Equal(2, counts.Unavailable); + Assert.Equal(15, counts.Edge); + Assert.Equal(100, counts.Answers); + } + + [Fact] + public async Task Edge_refusals_never_call_the_door() + { + var edges = 0; + await ReplayAsync((_, reply, factory, step) => + { + var code = (string?)step["response"]!["code"]; + if (code is not null && DeskReplay.IsEdgeRefusal(code)) + { + edges++; + Assert.Empty(factory.Door.Calls); + } + }); + Assert.Equal(15, edges); + } + + [Fact] + public async Task Answer_results_pass_through_unchanged() + { + var checkedResults = 0; + foreach (var row in DeskVectors.Rows("results")) + { + using var factory = new DeskWebApplicationFactory(); + var message = row["message"]!.AsObject(); + var answer = message["answer"]!.Deserialize(DeskJson.Options)!; + var result = message.Deserialize(DeskJson.Options)!; + factory.Door.Forks[(Socket, answer.ForkId)] = DeskReplay.Entry( + message["card"] is { } card ? card.Deserialize(DeskJson.Options)! : DeskReplay.Card(answer.ForkId)); + factory.Door.AnswerScript = (_, _, _) => new DeskAnswerOutcome.Answered(result with { Card = null }); + await factory.EnsureAccountAsync(DeskWebApplicationFactory.Alice, true); + + var reply = await factory.SendAsync( + "POST", DeskReplay.Target("answer", "gamecult-chat", null), DeskReplay.AnswerBody(answer.ForkId), + factory.Token(DeskWebApplicationFactory.Alice, "bifrost-desk", "desk.answer")); + + Assert.True((int)row["httpStatus"]! == reply.Status, $"{row["name"]}: {reply.Status}"); + Assert.True(DeskVectors.SameShape(JsonNode.Parse(reply.Body), message), $"{row["name"]}"); + checkedResults++; + } + + Assert.Equal(6, checkedResults); + } + + [Fact] + public async Task Unavailable_before_and_after_store() + { + // The scenarios hold both: door_down before the store, and door_down after it (the retry then replays). + var seen = new List<(bool Stored, int Status)>(); + await ReplayAsync((_, reply, factory, step) => + { + if ((string?)step["response"]!["code"] == "unavailable") + { + seen.Add(((bool?)step["server"]!["stored"] == true, reply.Status)); + } + }); + Assert.Equal([(false, 503), (true, 503)], seen.OrderBy(s => s.Stored).ToArray()); + } +} diff --git a/tests/Bifrost.Web.Tests/DeskSurfaceTests.cs b/tests/Bifrost.Web.Tests/DeskSurfaceTests.cs new file mode 100644 index 0000000..e1e5364 --- /dev/null +++ b/tests/Bifrost.Web.Tests/DeskSurfaceTests.cs @@ -0,0 +1,149 @@ +using System.Net.Sockets; +using System.Text; +using System.Text.Json.Nodes; +using Bifrost.Web.Features.Desk; +using Bifrost.Web.Tests.Support; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Hosting.Server.Features; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Bifrost.Web.Tests; + +public sealed class DeskSurfaceTests +{ + private static readonly string AnswerTarget = DeskReplay.Target("answer", "gamecult-chat", null); + + [Fact] + public async Task Not_mapped_without_config() + { + using var factory = new DeskWebApplicationFactory(configured: false); + await factory.EnsureAccountAsync(DeskWebApplicationFactory.Alice, true); + + var reply = await factory.SendAsync( + "POST", AnswerTarget, DeskReplay.AnswerBody(DeskReplay.ChatFork), + factory.Token(DeskWebApplicationFactory.Alice, "bifrost-desk", "desk.answer")); + + Assert.Equal(404, reply.Status); + Assert.Empty(factory.Door.Calls); + var routes = factory.Services.GetRequiredService().Endpoints.OfType(); + Assert.DoesNotContain(routes, e => e.RoutePattern.RawText!.StartsWith("/api/desk", StringComparison.Ordinal)); + } + + [Fact] + public void A_configured_desk_without_its_ports_stops_startup() + { + using var factory = new DeskWebApplicationFactory(registerPorts: false); + + var failure = Assert.ThrowsAny(() => factory.CreateClient()); + + Assert.Contains("IDeskCredential", failure.ToString()); + } + + [Fact] + public async Task Cookie_session_does_not_authenticate() + { + using var factory = new DeskWebApplicationFactory(); + DeskReplay.HoldVectorForks(factory.Door); + await factory.EnsureAccountAsync(DeskWebApplicationFactory.Alice, true); + + // The test host signs every request in as an Active-looking site member through the default scheme. The desk ignores it. + var reply = await factory.SendAsync("POST", AnswerTarget, DeskReplay.AnswerBody(DeskReplay.ChatFork)); + var withCookie = await factory.SendAsync( + "POST", AnswerTarget, DeskReplay.AnswerBody(DeskReplay.ChatFork), + configure: http => http.Request.Headers.Cookie = "bifrost_session=anything"); + + Assert.Equal(401, reply.Status); + Assert.Equal(401, withCookie.Status); + Assert.Empty(factory.Door.Calls); + } + + [Fact] + public async Task Cors_allows_exactly_the_configured_origin_without_credentials() + { + using var factory = new DeskWebApplicationFactory(); + DeskReplay.HoldVectorForks(factory.Door); + await factory.EnsureAccountAsync(DeskWebApplicationFactory.Alice, true); + var token = factory.Token(DeskWebApplicationFactory.Alice, "bifrost-desk", "desk.answer"); + + var allowed = await factory.SendAsync("POST", AnswerTarget, DeskReplay.AnswerBody(DeskReplay.ChatFork), token, DeskWebApplicationFactory.Origin); + var refused = await factory.SendAsync("POST", AnswerTarget, DeskReplay.AnswerBody(DeskReplay.ChatFork), token, "https://evil.example"); + var lookalike = await factory.SendAsync("POST", AnswerTarget, DeskReplay.AnswerBody(DeskReplay.ChatFork), token, DeskWebApplicationFactory.Origin + ".evil.example"); + var preflight = await factory.SendAsync("OPTIONS", AnswerTarget, null, null, DeskWebApplicationFactory.Origin, http => + { + http.Request.Headers["Access-Control-Request-Method"] = "POST"; + http.Request.Headers["Access-Control-Request-Headers"] = "authorization,content-type"; + }); + var elsewhere = await factory.SendAsync("GET", "/healthz", null, null, DeskWebApplicationFactory.Origin); + + Assert.Equal(DeskWebApplicationFactory.Origin, allowed.Headers.AccessControlAllowOrigin.ToString()); + Assert.False(allowed.Headers.ContainsKey("Access-Control-Allow-Credentials")); + Assert.Contains("Origin", allowed.Headers.Vary.ToString()); + Assert.False(refused.Headers.ContainsKey("Access-Control-Allow-Origin")); + Assert.False(lookalike.Headers.ContainsKey("Access-Control-Allow-Origin")); + Assert.Equal(DeskWebApplicationFactory.Origin, preflight.Headers.AccessControlAllowOrigin.ToString()); + Assert.False(preflight.Headers.ContainsKey("Access-Control-Allow-Credentials")); + Assert.Contains("POST", preflight.Headers.AccessControlAllowMethods.ToString()); + Assert.False(elsewhere.Headers.ContainsKey("Access-Control-Allow-Origin")); + } + + [Fact] + public async Task An_unrecognised_target_is_fork_unknown_and_opens_nothing() + { + using var factory = new DeskWebApplicationFactory(); + await factory.EnsureAccountAsync(DeskWebApplicationFactory.Alice, true); + var token = factory.Token(DeskWebApplicationFactory.Alice, "bifrost-desk", "desk.answer"); + + foreach (var target in new[] + { + "/api/desk", "/api/desk/", "/api/desk/gamecult-chat", "/api/desk/gamecult-chat/answers/", "/api/desk/a/b/answers", + "/api/desk/a/../gamecult-chat/answers", "/api/desk/./gamecult-chat/answers", "/api/desk//answers/x", + "/API/DESK/gamecult-chat/answers", "/api/deskx/gamecult-chat/answers", "http://localhost/api/desk/gamecult-chat/answers", + }) + { + var reply = await factory.SendAsync("POST", target, DeskReplay.AnswerBody(DeskReplay.ChatFork), token); + Assert.True(reply.Status == 404, $"{target}: {reply.Status}"); + } + + Assert.Empty(factory.Door.Calls); + } + + // Kestrel normalises Request.Path but leaves RawTarget as received. The desk parses RawTarget, so a dot + // segment can never be folded into a configured Mind's route. + [Fact] + public async Task Dot_segments_never_reach_the_route() + { + var builder = WebApplication.CreateBuilder(); + builder.WebHost.UseUrls("http://127.0.0.1:0"); + await using var app = builder.Build(); + app.Run(async http => + { + var raw = http.Features.Get()!.RawTarget; + var text = $"{http.Request.Path}|{raw}"; + http.Response.ContentLength = text.Length; + await http.Response.WriteAsync(text); + }); + await app.StartAsync(); + var address = app.Services.GetRequiredService().Features.Get()!.Addresses.First(); + var port = new Uri(address).Port; + + using var client = new TcpClient(); + await client.ConnectAsync("127.0.0.1", port); + var stream = client.GetStream(); + await stream.WriteAsync(Encoding.ASCII.GetBytes("GET /api/desk/ghost/../gamecult-chat/answers HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")); + using var reader = new StreamReader(stream, Encoding.ASCII); + var text = await reader.ReadToEndAsync(); + var echoed = text[(text.LastIndexOf("\r\n\r\n", StringComparison.Ordinal) + 4)..].Trim(); + + var parts = echoed.Split('|'); + Assert.Equal("/api/desk/gamecult-chat/answers", parts[0]); + Assert.Equal("/api/desk/ghost/../gamecult-chat/answers", parts[1]); + Assert.Null(DeskRoute.Parse("POST", parts[1])); + Assert.NotNull(DeskRoute.Parse("POST", parts[0])); + } +} diff --git a/tests/Bifrost.Web.Tests/DeskVectorPinTests.cs b/tests/Bifrost.Web.Tests/DeskVectorPinTests.cs new file mode 100644 index 0000000..161f7cb --- /dev/null +++ b/tests/Bifrost.Web.Tests/DeskVectorPinTests.cs @@ -0,0 +1,18 @@ +using System.Security.Cryptography; +using Bifrost.Web.Tests.Support; + +namespace Bifrost.Web.Tests; + +public sealed class DeskVectorPinTests +{ + [Fact] + public void Vectors_are_cultlib_89755b08() + { + var sha = Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(DeskVectors.Path))).ToLowerInvariant(); + + Assert.True( + DeskVectors.FileSha256 == sha, + $"fork-card-vectors.json is not CultLib {DeskVectors.SourceCommit}:contracts/cultnet/governance/fork-card-vectors.json (sha256 {sha}). " + + "Re-vendor from that commit or move the pin on purpose, with the whole contract."); + } +} diff --git a/tests/Bifrost.Web.Tests/Fixtures/fork-card-vectors.json b/tests/Bifrost.Web.Tests/Fixtures/fork-card-vectors.json new file mode 100644 index 0000000..ea738e8 --- /dev/null +++ b/tests/Bifrost.Web.Tests/Fixtures/fork-card-vectors.json @@ -0,0 +1,16970 @@ +{ + "provenance": "Rule source: /opt/commons/handoff/fork-desk-answer-binding-20261110.md (Heimdall), amended by the Persona briefs of 2026-11-10 (Eureka's fit check). Bytes spec-authored with msgpack-python (scripts/gen-governance-vectors.py); every runtime re-encodes `message` and must match messagePackHex. options_hash follows fork-card.md 'Options hash'; the vectors named heimdall_* are that file's, with its digests.", + "optionsHash": [ + { + "name": "heimdall_A_five_members", + "heimdall": "A", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "canonical": "{\"context\":\"Should a slot mark be shared?\",\"depends\":[\"gamecult-chat:question:file-zone-audience\"],\"options\":[{\"id\":\"yes\",\"text\":\"Share it.\"},{\"id\":\"no\",\"text\":\"Keep it local.\"}],\"recommendation\":\"no\",\"title\":\"Slot mark consent\"}", + "canonicalHex": "7B22636F6E74657874223A2253686F756C64206120736C6F74206D61726B206265207368617265643F222C22646570656E6473223A5B2267616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365225D2C226F7074696F6E73223A5B7B226964223A22796573222C2274657874223A2253686172652069742E227D2C7B226964223A226E6F222C2274657874223A224B656570206974206C6F63616C2E227D5D2C227265636F6D6D656E646174696F6E223A226E6F222C227469746C65223A22536C6F74206D61726B20636F6E73656E74227D", + "sha256": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + { + "name": "heimdall_B_recommendation_changed", + "heimdall": "B", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "yes", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "canonical": "{\"context\":\"Should a slot mark be shared?\",\"depends\":[\"gamecult-chat:question:file-zone-audience\"],\"options\":[{\"id\":\"yes\",\"text\":\"Share it.\"},{\"id\":\"no\",\"text\":\"Keep it local.\"}],\"recommendation\":\"yes\",\"title\":\"Slot mark consent\"}", + "canonicalHex": "7B22636F6E74657874223A2253686F756C64206120736C6F74206D61726B206265207368617265643F222C22646570656E6473223A5B2267616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365225D2C226F7074696F6E73223A5B7B226964223A22796573222C2274657874223A2253686172652069742E227D2C7B226964223A226E6F222C2274657874223A224B656570206974206C6F63616C2E227D5D2C227265636F6D6D656E646174696F6E223A22796573222C227469746C65223A22536C6F74206D61726B20636F6E73656E74227D", + "sha256": "14b8ffbdb61106dbc06f69f3fc8e2072af7a205d766e4cc040e89733013d54dc" + }, + { + "name": "heimdall_C_option_order_changed", + "heimdall": "C", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "no", + "text": "Keep it local." + }, + { + "id": "yes", + "text": "Share it." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "canonical": "{\"context\":\"Should a slot mark be shared?\",\"depends\":[\"gamecult-chat:question:file-zone-audience\"],\"options\":[{\"id\":\"no\",\"text\":\"Keep it local.\"},{\"id\":\"yes\",\"text\":\"Share it.\"}],\"recommendation\":\"no\",\"title\":\"Slot mark consent\"}", + "canonicalHex": "7B22636F6E74657874223A2253686F756C64206120736C6F74206D61726B206265207368617265643F222C22646570656E6473223A5B2267616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365225D2C226F7074696F6E73223A5B7B226964223A226E6F222C2274657874223A224B656570206974206C6F63616C2E227D2C7B226964223A22796573222C2274657874223A2253686172652069742E227D5D2C227265636F6D6D656E646174696F6E223A226E6F222C227469746C65223A22536C6F74206D61726B20636F6E73656E74227D", + "sha256": "ac67b41539f869f6203ac0908819df046e4328b27795743a919bd4ab390596b5" + }, + { + "name": "heimdall_D_nfc_escapes_astral", + "heimdall": "D", + "title": "Café “desk”", + "context": "Line one\nLine two — 😀", + "options": [ + { + "id": "a", + "text": "x" + }, + { + "id": "b", + "text": "y" + } + ], + "recommendation": "a", + "depends": [], + "canonical": "{\"context\":\"Line one\\nLine two — 😀\",\"depends\":[],\"options\":[{\"id\":\"a\",\"text\":\"x\"},{\"id\":\"b\",\"text\":\"y\"}],\"recommendation\":\"a\",\"title\":\"Café “desk”\"}", + "canonicalHex": "7B22636F6E74657874223A224C696E65206F6E655C6E4C696E652074776F20E2809420F09F9880222C22646570656E6473223A5B5D2C226F7074696F6E73223A5B7B226964223A2261222C2274657874223A2278227D2C7B226964223A2262222C2274657874223A2279227D5D2C227265636F6D6D656E646174696F6E223A2261222C227469746C65223A22436166C3A920E2809C6465736BE2809D227D", + "sha256": "71d5c413478ad06aaa1cbdd9e91c53f0cb4cef1dbd4c51bf3d473bccc6a5a272" + }, + { + "name": "heimdall_E_decomposed_equals_D", + "heimdall": "E", + "title": "Café “desk”", + "context": "Line one\nLine two — 😀", + "options": [ + { + "id": "a", + "text": "x" + }, + { + "id": "b", + "text": "y" + } + ], + "recommendation": "a", + "depends": [], + "canonical": "{\"context\":\"Line one\\nLine two — 😀\",\"depends\":[],\"options\":[{\"id\":\"a\",\"text\":\"x\"},{\"id\":\"b\",\"text\":\"y\"}],\"recommendation\":\"a\",\"title\":\"Café “desk”\"}", + "canonicalHex": "7B22636F6E74657874223A224C696E65206F6E655C6E4C696E652074776F20E2809420F09F9880222C22646570656E6473223A5B5D2C226F7074696F6E73223A5B7B226964223A2261222C2274657874223A2278227D2C7B226964223A2262222C2274657874223A2279227D5D2C227265636F6D6D656E646174696F6E223A2261222C227469746C65223A22436166C3A920E2809C6465736BE2809D227D", + "sha256": "71d5c413478ad06aaa1cbdd9e91c53f0cb4cef1dbd4c51bf3d473bccc6a5a272" + }, + { + "name": "heimdall_F_option_id_changed", + "heimdall": "F", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "share", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "canonical": "{\"context\":\"Should a slot mark be shared?\",\"depends\":[\"gamecult-chat:question:file-zone-audience\"],\"options\":[{\"id\":\"share\",\"text\":\"Share it.\"},{\"id\":\"no\",\"text\":\"Keep it local.\"}],\"recommendation\":\"no\",\"title\":\"Slot mark consent\"}", + "canonicalHex": "7B22636F6E74657874223A2253686F756C64206120736C6F74206D61726B206265207368617265643F222C22646570656E6473223A5B2267616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365225D2C226F7074696F6E73223A5B7B226964223A227368617265222C2274657874223A2253686172652069742E227D2C7B226964223A226E6F222C2274657874223A224B656570206974206C6F63616C2E227D5D2C227265636F6D6D656E646174696F6E223A226E6F222C227469746C65223A22536C6F74206D61726B20636F6E73656E74227D", + "sha256": "fb53aa3a9f7e74029f755d774064eedd240ab90d8fc64723f97c31b3d2846d40" + }, + { + "name": "heimdall_G_depends_emptied", + "heimdall": "G", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [], + "canonical": "{\"context\":\"Should a slot mark be shared?\",\"depends\":[],\"options\":[{\"id\":\"yes\",\"text\":\"Share it.\"},{\"id\":\"no\",\"text\":\"Keep it local.\"}],\"recommendation\":\"no\",\"title\":\"Slot mark consent\"}", + "canonicalHex": "7B22636F6E74657874223A2253686F756C64206120736C6F74206D61726B206265207368617265643F222C22646570656E6473223A5B5D2C226F7074696F6E73223A5B7B226964223A22796573222C2274657874223A2253686172652069742E227D2C7B226964223A226E6F222C2274657874223A224B656570206974206C6F63616C2E227D5D2C227265636F6D6D656E646174696F6E223A226E6F222C227469746C65223A22536C6F74206D61726B20636F6E73656E74227D", + "sha256": "f856edce121db4121b5f1af400b1c471ebbfa6579a042aca9a34ae31c15f5c86" + }, + { + "name": "option_text_changed", + "heimdall": null, + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it!" + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "canonical": "{\"context\":\"Should a slot mark be shared?\",\"depends\":[\"gamecult-chat:question:file-zone-audience\"],\"options\":[{\"id\":\"yes\",\"text\":\"Share it!\"},{\"id\":\"no\",\"text\":\"Keep it local.\"}],\"recommendation\":\"no\",\"title\":\"Slot mark consent\"}", + "canonicalHex": "7B22636F6E74657874223A2253686F756C64206120736C6F74206D61726B206265207368617265643F222C22646570656E6473223A5B2267616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365225D2C226F7074696F6E73223A5B7B226964223A22796573222C2274657874223A22536861726520697421227D2C7B226964223A226E6F222C2274657874223A224B656570206974206C6F63616C2E227D5D2C227265636F6D6D656E646174696F6E223A226E6F222C227469746C65223A22536C6F74206D61726B20636F6E73656E74227D", + "sha256": "9ce0d2770497fdd2d5338e66f9f1ae1741d87301db4c793407c25762929392b6" + }, + { + "name": "title_changed", + "heimdall": null, + "title": "Slot mark consent?", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "canonical": "{\"context\":\"Should a slot mark be shared?\",\"depends\":[\"gamecult-chat:question:file-zone-audience\"],\"options\":[{\"id\":\"yes\",\"text\":\"Share it.\"},{\"id\":\"no\",\"text\":\"Keep it local.\"}],\"recommendation\":\"no\",\"title\":\"Slot mark consent?\"}", + "canonicalHex": "7B22636F6E74657874223A2253686F756C64206120736C6F74206D61726B206265207368617265643F222C22646570656E6473223A5B2267616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365225D2C226F7074696F6E73223A5B7B226964223A22796573222C2274657874223A2253686172652069742E227D2C7B226964223A226E6F222C2274657874223A224B656570206974206C6F63616C2E227D5D2C227265636F6D6D656E646174696F6E223A226E6F222C227469746C65223A22536C6F74206D61726B20636F6E73656E743F227D", + "sha256": "30fe0c5bc88bd21d64592a495316c30e80fcee6cacf5c63fd9a818ca5cf87d0f" + }, + { + "name": "context_changed", + "heimdall": null, + "title": "Slot mark consent", + "context": "Should slot marks be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "canonical": "{\"context\":\"Should slot marks be shared?\",\"depends\":[\"gamecult-chat:question:file-zone-audience\"],\"options\":[{\"id\":\"yes\",\"text\":\"Share it.\"},{\"id\":\"no\",\"text\":\"Keep it local.\"}],\"recommendation\":\"no\",\"title\":\"Slot mark consent\"}", + "canonicalHex": "7B22636F6E74657874223A2253686F756C6420736C6F74206D61726B73206265207368617265643F222C22646570656E6473223A5B2267616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365225D2C226F7074696F6E73223A5B7B226964223A22796573222C2274657874223A2253686172652069742E227D2C7B226964223A226E6F222C2274657874223A224B656570206974206C6F63616C2E227D5D2C227265636F6D6D656E646174696F6E223A226E6F222C227469746C65223A22536C6F74206D61726B20636F6E73656E74227D", + "sha256": "716de65096e34fd89cc92848b667d90f5c27eeb66929da3d340cabb80155e173" + }, + { + "name": "depends_order_changed", + "heimdall": null, + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience", + "x:question:y" + ], + "canonical": "{\"context\":\"Should a slot mark be shared?\",\"depends\":[\"gamecult-chat:question:file-zone-audience\",\"x:question:y\"],\"options\":[{\"id\":\"yes\",\"text\":\"Share it.\"},{\"id\":\"no\",\"text\":\"Keep it local.\"}],\"recommendation\":\"no\",\"title\":\"Slot mark consent\"}", + "canonicalHex": "7B22636F6E74657874223A2253686F756C64206120736C6F74206D61726B206265207368617265643F222C22646570656E6473223A5B2267616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365222C22783A7175657374696F6E3A79225D2C226F7074696F6E73223A5B7B226964223A22796573222C2274657874223A2253686172652069742E227D2C7B226964223A226E6F222C2274657874223A224B656570206974206C6F63616C2E227D5D2C227265636F6D6D656E646174696F6E223A226E6F222C227469746C65223A22536C6F74206D61726B20636F6E73656E74227D", + "sha256": "fc3f651c6f498385256fc1232d5db31ece9dcc542e391f97245d27bc9e385b3d" + }, + { + "name": "depends_order_changed_reversed", + "heimdall": null, + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "x:question:y", + "gamecult-chat:question:file-zone-audience" + ], + "canonical": "{\"context\":\"Should a slot mark be shared?\",\"depends\":[\"x:question:y\",\"gamecult-chat:question:file-zone-audience\"],\"options\":[{\"id\":\"yes\",\"text\":\"Share it.\"},{\"id\":\"no\",\"text\":\"Keep it local.\"}],\"recommendation\":\"no\",\"title\":\"Slot mark consent\"}", + "canonicalHex": "7B22636F6E74657874223A2253686F756C64206120736C6F74206D61726B206265207368617265643F222C22646570656E6473223A5B22783A7175657374696F6E3A79222C2267616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365225D2C226F7074696F6E73223A5B7B226964223A22796573222C2274657874223A2253686172652069742E227D2C7B226964223A226E6F222C2274657874223A224B656570206974206C6F63616C2E227D5D2C227265636F6D6D656E646174696F6E223A226E6F222C227469746C65223A22536C6F74206D61726B20636F6E73656E74227D", + "sha256": "b22642e30f8d68839e3a0df5bc23b83ff50a839fcecf0366a65fa6e3cf873796" + }, + { + "name": "hidden_members_do_not_move_it", + "heimdall": null, + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "canonical": "{\"context\":\"Should a slot mark be shared?\",\"depends\":[\"gamecult-chat:question:file-zone-audience\"],\"options\":[{\"id\":\"yes\",\"text\":\"Share it.\"},{\"id\":\"no\",\"text\":\"Keep it local.\"}],\"recommendation\":\"no\",\"title\":\"Slot mark consent\"}", + "canonicalHex": "7B22636F6E74657874223A2253686F756C64206120736C6F74206D61726B206265207368617265643F222C22646570656E6473223A5B2267616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365225D2C226F7074696F6E73223A5B7B226964223A22796573222C2274657874223A2253686172652069742E227D2C7B226964223A226E6F222C2274657874223A224B656570206974206C6F63616C2E227D5D2C227265636F6D6D656E646174696F6E223A226E6F222C227469746C65223A22536C6F74206D61726B20636F6E73656E74227D", + "sha256": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + { + "name": "control_characters_and_markup_are_escaped_minimally", + "heimdall": null, + "title": "q \" b \\ t \t n \n bell \u0007 del ", + "context": " 
 &", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "canonical": "{\"context\":\" 
 &\",\"depends\":[\"gamecult-chat:question:file-zone-audience\"],\"options\":[{\"id\":\"yes\",\"text\":\"Share it.\"},{\"id\":\"no\",\"text\":\"Keep it local.\"}],\"recommendation\":\"no\",\"title\":\"q \\\" b \\\\ t \\t n \\n bell \\u0007 del \"}", + "canonicalHex": "7B22636F6E74657874223A223C2F7363726970743E20E280A82026222C22646570656E6473223A5B2267616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365225D2C226F7074696F6E73223A5B7B226964223A22796573222C2274657874223A2253686172652069742E227D2C7B226964223A226E6F222C2274657874223A224B656570206974206C6F63616C2E227D5D2C227265636F6D6D656E646174696F6E223A226E6F222C227469746C65223A2271205C222062205C5C2074205C74206E205C6E2062656C6C205C75303030372064656C207F227D", + "sha256": "f2d5a09513ce728129f6ca13f7123aa75b2bb77541b1de8946f39239634b40fe" + }, + { + "name": "astral_option_id_and_text", + "heimdall": null, + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "😀", + "text": "𝄞" + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "canonical": "{\"context\":\"Should a slot mark be shared?\",\"depends\":[\"gamecult-chat:question:file-zone-audience\"],\"options\":[{\"id\":\"😀\",\"text\":\"𝄞\"},{\"id\":\"no\",\"text\":\"Keep it local.\"}],\"recommendation\":\"no\",\"title\":\"Slot mark consent\"}", + "canonicalHex": "7B22636F6E74657874223A2253686F756C64206120736C6F74206D61726B206265207368617265643F222C22646570656E6473223A5B2267616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365225D2C226F7074696F6E73223A5B7B226964223A22F09F9880222C2274657874223A22F09D849E227D2C7B226964223A226E6F222C2274657874223A224B656570206974206C6F63616C2E227D5D2C227265636F6D6D656E646174696F6E223A226E6F222C227469746C65223A22536C6F74206D61726B20636F6E73656E74227D", + "sha256": "836fbfce16127132bae100aa773d328a85ad08d241ae9bf47ea7f84766762cac" + } + ], + "bodyDigests": [ + { + "name": "plain_answer_choice_A", + "status": "main_7ea11b4_must_not_move", + "why": "a plain answer: choice and note as sent, retracts null; no illegal or retracted key", + "input": { + "fork": "eureka-state:question:Q1", + "options_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "answered_at": "2026-11-10T12:00:00Z", + "choice": "A", + "note": null, + "retracts": null, + "illegal_retraction": null, + "retracted": null + }, + "canonical": "{\"fork\":\"eureka-state:question:Q1\",\"options_hash\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"answered_at\":\"2026-11-10T12:00:00Z\",\"choice\":\"A\",\"note\":null,\"retracts\":null}", + "sha256": "8a4d5b2ed0b3452bfed2f3a276b2828974e6824a3edcc643cfe9cfcdda34173a" + }, + { + "name": "legal_retraction", + "status": "main_7ea11b4_must_not_move", + "why": "a legal retraction as Huginn main pins it: retracts is `Q1.1`, a string no door produces (the key grammar is .n); kept for the frozen-form test", + "input": { + "fork": "eureka-state:question:Q1", + "options_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "answered_at": "2026-11-10T12:00:00Z", + "choice": null, + "note": null, + "retracts": "eureka-state:desk_answer:Q1.1", + "illegal_retraction": null, + "retracted": null + }, + "canonical": "{\"fork\":\"eureka-state:question:Q1\",\"options_hash\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"answered_at\":\"2026-11-10T12:00:00Z\",\"choice\":null,\"note\":null,\"retracts\":\"eureka-state:desk_answer:Q1.1\"}", + "sha256": "9f8075382556898c0bea364ed7c6deb2a86d8c78288a1f34e06da6e9bf5f58a3" + }, + { + "name": "illegal_i_and_iii_alike_on_main", + "status": "main_7ea11b4_must_not_move", + "why": "main writes `illegal_retraction: true` and no `retracted`, so (i) and (iii) collide: the defect the fix removes", + "input": { + "fork": "eureka-state:question:Q1", + "options_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "answered_at": "2026-11-10T12:00:00Z", + "choice": "A", + "note": null, + "retracts": "eureka-state:desk_answer:Q1.n1", + "illegal_retraction": true, + "retracted": null + }, + "canonical": "{\"fork\":\"eureka-state:question:Q1\",\"options_hash\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"answered_at\":\"2026-11-10T12:00:00Z\",\"choice\":\"A\",\"note\":null,\"retracts\":\"eureka-state:desk_answer:Q1.n1\",\"illegal_retraction\":true}", + "sha256": "69074cc60829ef5edb12358746f7549ad08da97653d59359c945cea102f53565" + }, + { + "name": "legal_retraction_door_form", + "status": "main_7ea11b4_must_not_move", + "why": "a legal retraction as the door produces it: the wire's n1 converted to the full stored key Q1.n1 before the digest; the golden that stands for the real form (digest unaffected by .n vs .1)", + "input": { + "fork": "eureka-state:question:Q1", + "options_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "answered_at": "2026-11-10T12:00:00Z", + "choice": null, + "note": null, + "retracts": "eureka-state:desk_answer:Q1.n1", + "illegal_retraction": null, + "retracted": null + }, + "canonical": "{\"fork\":\"eureka-state:question:Q1\",\"options_hash\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"answered_at\":\"2026-11-10T12:00:00Z\",\"choice\":null,\"note\":null,\"retracts\":\"eureka-state:desk_answer:Q1.n1\"}", + "sha256": "84576840247830b9b75754b460dc4ea52883ca1e410f9f011521e0f69b99eee1" + }, + { + "name": "illegal_ii_on_main", + "status": "main_7ea11b4_must_not_move", + "why": "(ii) no answer, no target, on main", + "input": { + "fork": "eureka-state:question:Q1", + "options_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "answered_at": "2026-11-10T12:00:00Z", + "choice": null, + "note": null, + "retracts": null, + "illegal_retraction": true, + "retracted": null + }, + "canonical": "{\"fork\":\"eureka-state:question:Q1\",\"options_hash\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"answered_at\":\"2026-11-10T12:00:00Z\",\"choice\":null,\"note\":null,\"retracts\":null,\"illegal_retraction\":true}", + "sha256": "1a36c4aba0d1c30d0736f20243ebfdb84fe84a5eabd4f69709a096a32c3cc6e3" + }, + { + "name": "illegal_i_retraction_with_an_answer_retracted_true", + "status": "reproduced_on_huginn_main_f0ca504_reported_by_eureka_not_rerun_here", + "why": "(i) answer present, retracted true, target Q1.n1", + "input": { + "fork": "eureka-state:question:Q1", + "options_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "answered_at": "2026-11-10T12:00:00Z", + "choice": "A", + "note": null, + "retracts": "eureka-state:desk_answer:Q1.n1", + "illegal_retraction": true, + "retracted": true + }, + "canonical": "{\"fork\":\"eureka-state:question:Q1\",\"options_hash\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"answered_at\":\"2026-11-10T12:00:00Z\",\"choice\":\"A\",\"note\":null,\"retracts\":\"eureka-state:desk_answer:Q1.n1\",\"illegal_retraction\":true,\"retracted\":true}", + "sha256": "935cab1a0ae34606b809d8ebca7cffa2889d20b3585d2622d32153a4d6898af1" + }, + { + "name": "illegal_iii_plain_answer_with_a_target_retracted_false", + "status": "reproduced_on_huginn_main_f0ca504_reported_by_eureka_not_rerun_here", + "why": "(iii) the same bytes as (i) with retracted false: another body, so request_id_conflict", + "input": { + "fork": "eureka-state:question:Q1", + "options_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "answered_at": "2026-11-10T12:00:00Z", + "choice": "A", + "note": null, + "retracts": "eureka-state:desk_answer:Q1.n1", + "illegal_retraction": true, + "retracted": false + }, + "canonical": "{\"fork\":\"eureka-state:question:Q1\",\"options_hash\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"answered_at\":\"2026-11-10T12:00:00Z\",\"choice\":\"A\",\"note\":null,\"retracts\":\"eureka-state:desk_answer:Q1.n1\",\"illegal_retraction\":true,\"retracted\":false}", + "sha256": "82b24cccb4ed2535a549f95f844fcd935074c3712b5b832f0b2cbd89c71fd9a6" + }, + { + "name": "illegal_i_retraction_with_an_answer_and_no_target_retracted_true", + "status": "reproduced_on_huginn_main_f0ca504_reported_by_eureka_not_rerun_here", + "why": "(i) with the target null (the order doc: retracts set or null); Eureka re-derived it with sha256sum (order doc addendum)", + "input": { + "fork": "eureka-state:question:Q1", + "options_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "answered_at": "2026-11-10T12:00:00Z", + "choice": "A", + "note": null, + "retracts": null, + "illegal_retraction": true, + "retracted": true + }, + "canonical": "{\"fork\":\"eureka-state:question:Q1\",\"options_hash\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"answered_at\":\"2026-11-10T12:00:00Z\",\"choice\":\"A\",\"note\":null,\"retracts\":null,\"illegal_retraction\":true,\"retracted\":true}", + "sha256": "79eee623fd6712db6537a4867cedf54ca49ee4885416c2c8e4acec59f811b894" + }, + { + "name": "illegal_ii_no_answer_no_target_retracted_true", + "status": "reproduced_on_huginn_main_f0ca504_reported_by_eureka_not_rerun_here", + "why": "(ii) no answer, no target, retracted true", + "input": { + "fork": "eureka-state:question:Q1", + "options_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "answered_at": "2026-11-10T12:00:00Z", + "choice": null, + "note": null, + "retracts": null, + "illegal_retraction": true, + "retracted": true + }, + "canonical": "{\"fork\":\"eureka-state:question:Q1\",\"options_hash\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"answered_at\":\"2026-11-10T12:00:00Z\",\"choice\":null,\"note\":null,\"retracts\":null,\"illegal_retraction\":true,\"retracted\":true}", + "sha256": "34ef86ed63b9318459965d4b85e794d2636ffc15736da866036b087c9c4a30eb" + } + ], + "card": { + "valid": [ + { + "name": "operator_fork", + "message": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "messagePackHex": "DE0010AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E636172642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A57469746C65B1536C6F74206D61726B20636F6E73656E74A7636F6E74657874BD53686F756C64206120736C6F74206D61726B206265207368617265643FA76F7074696F6E739282A26964A3796573A474657874A953686172652069742E82A26964A26E6FA474657874AE4B656570206974206C6F63616C2EAE7265636F6D6D656E646174696F6EA26E6FA7646570656E647391D92967616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365A56C65616E73C0A4666C6167C0A97261697365645F696E82A46B696E64A743757453706563A26964D92B63756C746C69622D676170733A6375745F737065633A6375742D736C6F742D746F6C6572616E63652E7234A8646563696465727391D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A6737461747573A46F70656EA672756C696E67C0AA72756C65645F66726F6D90A56F7264657203AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438" + }, + { + "name": "leans_flag_closed_two_deciders", + "message": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [], + "leans": "yes", + "flag": "provisional: Self expects this to move", + "raised_in": null, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "9f8e7d6c-5b4a-4392-8170-0f1e2d3c4b5a" + ], + "status": "closed", + "ruling": "x:resolution:y", + "ruled_from": [ + "n1" + ], + "order": 0, + "options_hash": "f856edce121db4121b5f1af400b1c471ebbfa6579a042aca9a34ae31c15f5c86" + }, + "messagePackHex": "DE0010AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E636172642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A57469746C65B1536C6F74206D61726B20636F6E73656E74A7636F6E74657874BD53686F756C64206120736C6F74206D61726B206265207368617265643FA76F7074696F6E739282A26964A3796573A474657874A953686172652069742E82A26964A26E6FA474657874AE4B656570206974206C6F63616C2EAE7265636F6D6D656E646174696F6EA26E6FA7646570656E647390A56C65616E73A3796573A4666C6167D92670726F766973696F6E616C3A2053656C662065787065637473207468697320746F206D6F7665A97261697365645F696EC0A8646563696465727392D92465306431633262332D613439352D343638372D383837392D366135623463336432653166D92439663865376436632D356234612D343339322D383137302D306631653264336334623561A6737461747573A6636C6F736564A672756C696E67AE783A7265736F6C7574696F6E3A79AA72756C65645F66726F6D91A26E31A56F7264657200AC6F7074696F6E735F68617368D94066383536656463653132316462343132316235663161663430306231633437316562626661363537396130343261636139613334616533316331356635633836" + }, + { + "name": "depends_at_both_caps", + "message": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "a", + "a", + "a", + "a", + "a", + "a", + "a", + "éééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "52163c7ae8a2c15d4380e7854505bedb5b3c84976ed301d86afc99a7bd0e03c8" + }, + "messagePackHex": "DE0010AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E636172642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A57469746C65B1536C6F74206D61726B20636F6E73656E74A7636F6E74657874BD53686F756C64206120736C6F74206D61726B206265207368617265643FA76F7074696F6E739282A26964A3796573A474657874A953686172652069742E82A26964A26E6FA474657874AE4B656570206974206C6F63616C2EAE7265636F6D6D656E646174696F6EA26E6FA7646570656E647398A161A161A161A161A161A161A161DA03E8C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9A56C65616E73C0A4666C6167C0A97261697365645F696E82A46B696E64A743757453706563A26964D92B63756C746C69622D676170733A6375745F737065633A6375742D736C6F742D746F6C6572616E63652E7234A8646563696465727391D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A6737461747573A46F70656EA672756C696E67C0AA72756C65645F66726F6D90A56F7264657203AC6F7074696F6E735F68617368D94035323136336337616538613263313564343338306537383534353035626564623562336338343937366564333031643836616663393961376264306530336338" + }, + { + "name": "closed_without_a_ruling_the_question_was_withdrawn", + "message": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "closed", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "messagePackHex": "DE0010AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E636172642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A57469746C65B1536C6F74206D61726B20636F6E73656E74A7636F6E74657874BD53686F756C64206120736C6F74206D61726B206265207368617265643FA76F7074696F6E739282A26964A3796573A474657874A953686172652069742E82A26964A26E6FA474657874AE4B656570206974206C6F63616C2EAE7265636F6D6D656E646174696F6EA26E6FA7646570656E647391D92967616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365A56C65616E73C0A4666C6167C0A97261697365645F696E82A46B696E64A743757453706563A26964D92B63756C746C69622D676170733A6375745F737065633A6375742D736C6F742D746F6C6572616E63652E7234A8646563696465727391D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A6737461747573A6636C6F736564A672756C696E67C0AA72756C65645F66726F6D90A56F7264657203AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438" + }, + { + "name": "deciders_at_the_cap_of_64", + "message": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "00000001-0000-4000-8000-000000000001", + "00000002-0000-4000-8000-000000000002", + "00000003-0000-4000-8000-000000000003", + "00000004-0000-4000-8000-000000000004", + "00000005-0000-4000-8000-000000000005", + "00000006-0000-4000-8000-000000000006", + "00000007-0000-4000-8000-000000000007", + "00000008-0000-4000-8000-000000000008", + "00000009-0000-4000-8000-000000000009", + "0000000a-0000-4000-8000-00000000000a", + "0000000b-0000-4000-8000-00000000000b", + "0000000c-0000-4000-8000-00000000000c", + "0000000d-0000-4000-8000-00000000000d", + "0000000e-0000-4000-8000-00000000000e", + "0000000f-0000-4000-8000-00000000000f", + "00000010-0000-4000-8000-000000000010", + "00000011-0000-4000-8000-000000000011", + "00000012-0000-4000-8000-000000000012", + "00000013-0000-4000-8000-000000000013", + "00000014-0000-4000-8000-000000000014", + "00000015-0000-4000-8000-000000000015", + "00000016-0000-4000-8000-000000000016", + "00000017-0000-4000-8000-000000000017", + "00000018-0000-4000-8000-000000000018", + "00000019-0000-4000-8000-000000000019", + "0000001a-0000-4000-8000-00000000001a", + "0000001b-0000-4000-8000-00000000001b", + "0000001c-0000-4000-8000-00000000001c", + "0000001d-0000-4000-8000-00000000001d", + "0000001e-0000-4000-8000-00000000001e", + "0000001f-0000-4000-8000-00000000001f", + "00000020-0000-4000-8000-000000000020", + "00000021-0000-4000-8000-000000000021", + "00000022-0000-4000-8000-000000000022", + "00000023-0000-4000-8000-000000000023", + "00000024-0000-4000-8000-000000000024", + "00000025-0000-4000-8000-000000000025", + "00000026-0000-4000-8000-000000000026", + "00000027-0000-4000-8000-000000000027", + "00000028-0000-4000-8000-000000000028", + "00000029-0000-4000-8000-000000000029", + "0000002a-0000-4000-8000-00000000002a", + "0000002b-0000-4000-8000-00000000002b", + "0000002c-0000-4000-8000-00000000002c", + "0000002d-0000-4000-8000-00000000002d", + "0000002e-0000-4000-8000-00000000002e", + "0000002f-0000-4000-8000-00000000002f", + "00000030-0000-4000-8000-000000000030", + "00000031-0000-4000-8000-000000000031", + "00000032-0000-4000-8000-000000000032", + "00000033-0000-4000-8000-000000000033", + "00000034-0000-4000-8000-000000000034", + "00000035-0000-4000-8000-000000000035", + "00000036-0000-4000-8000-000000000036", + "00000037-0000-4000-8000-000000000037", + "00000038-0000-4000-8000-000000000038", + "00000039-0000-4000-8000-000000000039", + "0000003a-0000-4000-8000-00000000003a", + "0000003b-0000-4000-8000-00000000003b", + "0000003c-0000-4000-8000-00000000003c", + "0000003d-0000-4000-8000-00000000003d", + "0000003e-0000-4000-8000-00000000003e", + "0000003f-0000-4000-8000-00000000003f", + "00000040-0000-4000-8000-000000000040" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "messagePackHex": "DE0010AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E636172642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A57469746C65B1536C6F74206D61726B20636F6E73656E74A7636F6E74657874BD53686F756C64206120736C6F74206D61726B206265207368617265643FA76F7074696F6E739282A26964A3796573A474657874A953686172652069742E82A26964A26E6FA474657874AE4B656570206974206C6F63616C2EAE7265636F6D6D656E646174696F6EA26E6FA7646570656E647391D92967616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365A56C65616E73C0A4666C6167C0A97261697365645F696E82A46B696E64A743757453706563A26964D92B63756C746C69622D676170733A6375745F737065633A6375742D736C6F742D746F6C6572616E63652E7234A86465636964657273DC0040D92430303030303030312D303030302D343030302D383030302D303030303030303030303031D92430303030303030322D303030302D343030302D383030302D303030303030303030303032D92430303030303030332D303030302D343030302D383030302D303030303030303030303033D92430303030303030342D303030302D343030302D383030302D303030303030303030303034D92430303030303030352D303030302D343030302D383030302D303030303030303030303035D92430303030303030362D303030302D343030302D383030302D303030303030303030303036D92430303030303030372D303030302D343030302D383030302D303030303030303030303037D92430303030303030382D303030302D343030302D383030302D303030303030303030303038D92430303030303030392D303030302D343030302D383030302D303030303030303030303039D92430303030303030612D303030302D343030302D383030302D303030303030303030303061D92430303030303030622D303030302D343030302D383030302D303030303030303030303062D92430303030303030632D303030302D343030302D383030302D303030303030303030303063D92430303030303030642D303030302D343030302D383030302D303030303030303030303064D92430303030303030652D303030302D343030302D383030302D303030303030303030303065D92430303030303030662D303030302D343030302D383030302D303030303030303030303066D92430303030303031302D303030302D343030302D383030302D303030303030303030303130D92430303030303031312D303030302D343030302D383030302D303030303030303030303131D92430303030303031322D303030302D343030302D383030302D303030303030303030303132D92430303030303031332D303030302D343030302D383030302D303030303030303030303133D92430303030303031342D303030302D343030302D383030302D303030303030303030303134D92430303030303031352D303030302D343030302D383030302D303030303030303030303135D92430303030303031362D303030302D343030302D383030302D303030303030303030303136D92430303030303031372D303030302D343030302D383030302D303030303030303030303137D92430303030303031382D303030302D343030302D383030302D303030303030303030303138D92430303030303031392D303030302D343030302D383030302D303030303030303030303139D92430303030303031612D303030302D343030302D383030302D303030303030303030303161D92430303030303031622D303030302D343030302D383030302D303030303030303030303162D92430303030303031632D303030302D343030302D383030302D303030303030303030303163D92430303030303031642D303030302D343030302D383030302D303030303030303030303164D92430303030303031652D303030302D343030302D383030302D303030303030303030303165D92430303030303031662D303030302D343030302D383030302D303030303030303030303166D92430303030303032302D303030302D343030302D383030302D303030303030303030303230D92430303030303032312D303030302D343030302D383030302D303030303030303030303231D92430303030303032322D303030302D343030302D383030302D303030303030303030303232D92430303030303032332D303030302D343030302D383030302D303030303030303030303233D92430303030303032342D303030302D343030302D383030302D303030303030303030303234D92430303030303032352D303030302D343030302D383030302D303030303030303030303235D92430303030303032362D303030302D343030302D383030302D303030303030303030303236D92430303030303032372D303030302D343030302D383030302D303030303030303030303237D92430303030303032382D303030302D343030302D383030302D303030303030303030303238D92430303030303032392D303030302D343030302D383030302D303030303030303030303239D92430303030303032612D303030302D343030302D383030302D303030303030303030303261D92430303030303032622D303030302D343030302D383030302D303030303030303030303262D92430303030303032632D303030302D343030302D383030302D303030303030303030303263D92430303030303032642D303030302D343030302D383030302D303030303030303030303264D92430303030303032652D303030302D343030302D383030302D303030303030303030303265D92430303030303032662D303030302D343030302D383030302D303030303030303030303266D92430303030303033302D303030302D343030302D383030302D303030303030303030303330D92430303030303033312D303030302D343030302D383030302D303030303030303030303331D92430303030303033322D303030302D343030302D383030302D303030303030303030303332D92430303030303033332D303030302D343030302D383030302D303030303030303030303333D92430303030303033342D303030302D343030302D383030302D303030303030303030303334D92430303030303033352D303030302D343030302D383030302D303030303030303030303335D92430303030303033362D303030302D343030302D383030302D303030303030303030303336D92430303030303033372D303030302D343030302D383030302D303030303030303030303337D92430303030303033382D303030302D343030302D383030302D303030303030303030303338D92430303030303033392D303030302D343030302D383030302D303030303030303030303339D92430303030303033612D303030302D343030302D383030302D303030303030303030303361D92430303030303033622D303030302D343030302D383030302D303030303030303030303362D92430303030303033632D303030302D343030302D383030302D303030303030303030303363D92430303030303033642D303030302D343030302D383030302D303030303030303030303364D92430303030303033652D303030302D343030302D383030302D303030303030303030303365D92430303030303033662D303030302D343030302D383030302D303030303030303030303366D92430303030303034302D303030302D343030302D383030302D303030303030303030303430A6737461747573A46F70656EA672756C696E67C0AA72756C65645F66726F6D90A56F7264657203AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438" + } + ], + "invalid": [ + { + "name": "depends_nine_lines", + "why": "depends holds at most 8 lines (the stored question's limit)", + "message": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "a", + "a", + "a", + "a", + "a", + "a", + "a", + "a", + "a" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "d0f895ec9cf7e17f4e053e3065b41b3ad34a808b5a5a2912dce4fd6a0a0a890f" + } + }, + { + "name": "depends_line_1001_code_points", + "why": "a depends line is at most 1000 characters", + "message": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "47d1a35dffaa09893998b3110ebe9c0fa4dab410b3f08a4c6ab78ed609072c99" + } + }, + { + "name": "depends_line_1001_bytes", + "why": "rule: one byte over the limit (500 two-byte characters and one more; not expressible in the schema)", + "message": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééa" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "63d01e8c6dfcfddda061522e3c4bab7d54d1f1b3edf384a74725f034e8dc1f88" + } + }, + { + "name": "depends_line_over_1000_bytes", + "why": "rule: a depends line is at most 1000 UTF-8 bytes (the stored Line limit; not expressible in the schema)", + "message": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "e9459375267b29ebaacd409b005b6b2e4901fe8ff67b80163fba3573a804974e" + } + }, + { + "name": "one_option", + "why": "two or more options", + "message": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + } + ], + "recommendation": "yes", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "950f1ed77895e730a7e456e5d17801385b30b8b67d7185d977d0d0a6c6c6b5c2" + } + }, + { + "name": "no_deciders", + "why": "a fork with no decider cannot be answered", + "message": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + } + }, + { + "name": "null_recommendation", + "why": "every card recommends an option", + "message": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": null, + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + } + }, + { + "name": "unknown_key", + "why": "closed document", + "message": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "note": "x" + } + }, + { + "name": "option_with_a_label", + "why": "options are {id,text}; the old format's display label is not a member (fork-card.md, Old format)", + "message": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it.", + "label": "Yes" + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + } + }, + { + "name": "status_unknown", + "why": "status is open or closed", + "message": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "provisional", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + } + }, + { + "name": "options_hash_not_hex", + "why": "options_hash is 64 lowercase hex", + "message": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "XYZ" + } + }, + { + "name": "duplicate_option_ids", + "why": "rule: option ids are unique (not expressible in the schema)", + "message": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "yes", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "d5aaa1c4c0e464c2015044f907fea8f6b7100ff7181677d14ce0982e059aac40" + } + }, + { + "name": "recommendation_not_an_option", + "why": "rule: recommendation is an option id (not expressible in the schema)", + "message": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "maybe", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "9077264ad75c31722641565acd76aab2dc248560aa8e90210973553414aba3f5" + } + }, + { + "name": "leans_not_an_option", + "why": "rule: leans is nil or an option id (not expressible in the schema)", + "message": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": "maybe", + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + } + }, + { + "name": "duplicate_deciders", + "why": "rule: deciders are unique (not expressible in the schema)", + "message": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + } + }, + { + "name": "ruled_from_on_an_open_card", + "why": "rule: a ruling that answers a fork closes it, so an open card cites no answer in ruled_from (not expressible in the schema)", + "message": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [ + "n1" + ], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + } + }, + { + "name": "open_card_names_a_ruling", + "why": "rule: only a closed card names a ruling (not expressible in the schema)", + "message": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": "cultlib-gaps:resolution:question.slot-mark-consent.n1", + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + } + }, + { + "name": "ruled_from_without_a_ruling", + "why": "rule: ruled_from lists answers a ruling was admitted from, so it needs the ruling (not expressible in the schema)", + "message": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "closed", + "ruling": null, + "ruled_from": [ + "n1" + ], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + } + }, + { + "name": "options_hash_of_another_card", + "why": "rule: options_hash equals the hash of the shown members (not expressible in the schema)", + "message": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "14b8ffbdb61106dbc06f69f3fc8e2072af7a205d766e4cc040e89733013d54dc" + } + }, + { + "name": "deciders_65", + "why": "deciders holds at most 64 (Huginn's bound)", + "message": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "00000001-0000-4000-8000-000000000001", + "00000002-0000-4000-8000-000000000002", + "00000003-0000-4000-8000-000000000003", + "00000004-0000-4000-8000-000000000004", + "00000005-0000-4000-8000-000000000005", + "00000006-0000-4000-8000-000000000006", + "00000007-0000-4000-8000-000000000007", + "00000008-0000-4000-8000-000000000008", + "00000009-0000-4000-8000-000000000009", + "0000000a-0000-4000-8000-00000000000a", + "0000000b-0000-4000-8000-00000000000b", + "0000000c-0000-4000-8000-00000000000c", + "0000000d-0000-4000-8000-00000000000d", + "0000000e-0000-4000-8000-00000000000e", + "0000000f-0000-4000-8000-00000000000f", + "00000010-0000-4000-8000-000000000010", + "00000011-0000-4000-8000-000000000011", + "00000012-0000-4000-8000-000000000012", + "00000013-0000-4000-8000-000000000013", + "00000014-0000-4000-8000-000000000014", + "00000015-0000-4000-8000-000000000015", + "00000016-0000-4000-8000-000000000016", + "00000017-0000-4000-8000-000000000017", + "00000018-0000-4000-8000-000000000018", + "00000019-0000-4000-8000-000000000019", + "0000001a-0000-4000-8000-00000000001a", + "0000001b-0000-4000-8000-00000000001b", + "0000001c-0000-4000-8000-00000000001c", + "0000001d-0000-4000-8000-00000000001d", + "0000001e-0000-4000-8000-00000000001e", + "0000001f-0000-4000-8000-00000000001f", + "00000020-0000-4000-8000-000000000020", + "00000021-0000-4000-8000-000000000021", + "00000022-0000-4000-8000-000000000022", + "00000023-0000-4000-8000-000000000023", + "00000024-0000-4000-8000-000000000024", + "00000025-0000-4000-8000-000000000025", + "00000026-0000-4000-8000-000000000026", + "00000027-0000-4000-8000-000000000027", + "00000028-0000-4000-8000-000000000028", + "00000029-0000-4000-8000-000000000029", + "0000002a-0000-4000-8000-00000000002a", + "0000002b-0000-4000-8000-00000000002b", + "0000002c-0000-4000-8000-00000000002c", + "0000002d-0000-4000-8000-00000000002d", + "0000002e-0000-4000-8000-00000000002e", + "0000002f-0000-4000-8000-00000000002f", + "00000030-0000-4000-8000-000000000030", + "00000031-0000-4000-8000-000000000031", + "00000032-0000-4000-8000-000000000032", + "00000033-0000-4000-8000-000000000033", + "00000034-0000-4000-8000-000000000034", + "00000035-0000-4000-8000-000000000035", + "00000036-0000-4000-8000-000000000036", + "00000037-0000-4000-8000-000000000037", + "00000038-0000-4000-8000-000000000038", + "00000039-0000-4000-8000-000000000039", + "0000003a-0000-4000-8000-00000000003a", + "0000003b-0000-4000-8000-00000000003b", + "0000003c-0000-4000-8000-00000000003c", + "0000003d-0000-4000-8000-00000000003d", + "0000003e-0000-4000-8000-00000000003e", + "0000003f-0000-4000-8000-00000000003f", + "00000040-0000-4000-8000-000000000040", + "00000041-0000-4000-8000-000000000041" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + } + } + ] + }, + "request": { + "valid": [ + { + "name": "choice_and_note", + "message": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "messagePackHex": "88AD736368656D6156657273696F6EBF67616D6563756C742E666F726B2E616E737765725F726571756573742E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A6616E7377657282A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EA9726574726163746564C2A87265747261637473C0AB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031" + }, + { + "name": "choice_alone", + "message": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": null + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000005-0000-4000-8000-000000000005" + }, + "messagePackHex": "88AD736368656D6156657273696F6EBF67616D6563756C742E666F726B2E616E737765725F726571756573742E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A6616E7377657282A663686F696365A26E6FA46E6F7465C0A9726574726163746564C2A87265747261637473C0AB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAA726571756573745F6964D92430303030303030352D303030302D343030302D383030302D303030303030303030303035" + }, + { + "name": "note_alone_clear_pick", + "message": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": null, + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000006-0000-4000-8000-000000000006" + }, + "messagePackHex": "88AD736368656D6156657273696F6EBF67616D6563756C742E666F726B2E616E737765725F726571756573742E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A6616E7377657282A663686F696365C0A46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EA9726574726163746564C2A87265747261637473C0AB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAA726571756573745F6964D92430303030303030362D303030302D343030302D383030302D303030303030303030303036" + }, + { + "name": "retraction", + "message": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": null, + "retracted": true, + "retracts": "n1", + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000023-0000-4000-8000-000000000023" + }, + "messagePackHex": "88AD736368656D6156657273696F6EBF67616D6563756C742E666F726B2E616E737765725F726571756573742E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A6616E73776572C0A9726574726163746564C3A87265747261637473A26E31AB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAA726571756573745F6964D92430303030303032332D303030302D343030302D383030302D303030303030303030303233" + }, + { + "name": "note_4000_ascii_bytes", + "message": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": null, + "note": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000024-0000-4000-8000-000000000024" + }, + "messagePackHex": "88AD736368656D6156657273696F6EBF67616D6563756C742E666F726B2E616E737765725F726571756573742E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A6616E7377657282A663686F696365C0A46E6F7465DA0FA078787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878A9726574726163746564C2A87265747261637473C0AB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAA726571756573745F6964D92430303030303032342D303030302D343030302D383030302D303030303030303030303234" + }, + { + "name": "note_astral_4000_bytes", + "message": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": null, + "note": "😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀" + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000025-0000-4000-8000-000000000025" + }, + "messagePackHex": "88AD736368656D6156657273696F6EBF67616D6563756C742E666F726B2E616E737765725F726571756573742E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A6616E7377657282A663686F696365C0A46E6F7465DA0FA0F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880A9726574726163746564C2A87265747261637473C0AB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAA726571756573745F6964D92430303030303032352D303030302D343030302D383030302D303030303030303030303235" + }, + { + "name": "note_combining_4000_bytes", + "message": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": null, + "note": "éééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééx" + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000026-0000-4000-8000-000000000026" + }, + "messagePackHex": "88AD736368656D6156657273696F6EBF67616D6563756C742E666F726B2E616E737765725F726571756573742E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A6616E7377657282A663686F696365C0A46E6F7465DA0FA065CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8178A9726574726163746564C2A87265747261637473C0AB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAA726571756573745F6964D92430303030303032362D303030302D343030302D383030302D303030303030303030303236" + } + ], + "invalid": [ + { + "name": "answer_with_neither", + "why": "an answer object holds a choice, a note, or both", + "message": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": null, + "note": null + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000027-0000-4000-8000-000000000027" + }, + "messagePackHex": "88AD736368656D6156657273696F6EBF67616D6563756C742E666F726B2E616E737765725F726571756573742E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A6616E7377657282A663686F696365C0A46E6F7465C0A9726574726163746564C2A87265747261637473C0AB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAA726571756573745F6964D92430303030303032372D303030302D343030302D383030302D303030303030303030303237" + }, + { + "name": "blank_note", + "why": "a blank note is no answer", + "message": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": null, + "note": "   " + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000028-0000-4000-8000-000000000028" + }, + "messagePackHex": "88AD736368656D6156657273696F6EBF67616D6563756C742E666F726B2E616E737765725F726571756573742E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A6616E7377657282A663686F696365C0A46E6F7465A52020C2A020A9726574726163746564C2A87265747261637473C0AB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAA726571756573745F6964D92430303030303032382D303030302D343030302D383030302D303030303030303030303238" + }, + { + "name": "request_id_missing", + "why": "map-mode writes every key; a retry needs the id", + "message": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z" + }, + "messagePackHex": "87AD736368656D6156657273696F6EBF67616D6563756C742E666F726B2E616E737765725F726571756573742E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A6616E7377657282A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EA9726574726163746564C2A87265747261637473C0AB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305A" + }, + { + "name": "request_id_uppercase", + "why": "request_id is lowercase", + "message": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "0000000A-0000-4000-8000-00000000000B" + }, + "messagePackHex": "88AD736368656D6156657273696F6EBF67616D6563756C742E666F726B2E616E737765725F726571756573742E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A6616E7377657282A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EA9726574726163746564C2A87265747261637473C0AB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAA726571756573745F6964D92430303030303030412D303030302D343030302D383030302D303030303030303030303042" + }, + { + "name": "request_id_not_a_uuid", + "why": "request_id is a lowercase D-format UUID", + "message": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "retry-1" + }, + "messagePackHex": "88AD736368656D6156657273696F6EBF67616D6563756C742E666F726B2E616E737765725F726571756573742E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A6616E7377657282A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EA9726574726163746564C2A87265747261637473C0AB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAA726571756573745F6964A772657472792D31" + }, + { + "name": "note_over_4000", + "why": "notes are at most 4000 UTF-8 bytes", + "message": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": null, + "note": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000029-0000-4000-8000-000000000029" + }, + "messagePackHex": "88AD736368656D6156657273696F6EBF67616D6563756C742E666F726B2E616E737765725F726571756573742E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A6616E7377657282A663686F696365C0A46E6F7465DA0FA17878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878A9726574726163746564C2A87265747261637473C0AB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAA726571756573745F6964D92430303030303032392D303030302D343030302D383030302D303030303030303030303239" + }, + { + "name": "note_multibyte_4001_bytes", + "why": "a note is counted in UTF-8 bytes: 1999 e-acute is 3998 bytes, plus three ASCII is 4001 (rule: not expressible in the schema, 2002 characters)", + "message": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": null, + "note": "éééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééabc" + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "0000002a-0000-4000-8000-00000000002a" + }, + "messagePackHex": "88AD736368656D6156657273696F6EBF67616D6563756C742E666F726B2E616E737765725F726571756573742E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A6616E7377657282A663686F696365C0A46E6F7465DA0FA1C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9C3A9616263A9726574726163746564C2A87265747261637473C0AB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAA726571756573745F6964D92430303030303032612D303030302D343030302D383030302D303030303030303030303261" + }, + { + "name": "note_astral_4004_bytes", + "why": "1001 astral characters are 4004 bytes though only 1001 code points (rule: not expressible in the schema)", + "message": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": null, + "note": "😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀" + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "0000002b-0000-4000-8000-00000000002b" + }, + "messagePackHex": "88AD736368656D6156657273696F6EBF67616D6563756C742E666F726B2E616E737765725F726571756573742E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A6616E7377657282A663686F696365C0A46E6F7465DA0FA4F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880F09F9880A9726574726163746564C2A87265747261637473C0AB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAA726571756573745F6964D92430303030303032622D303030302D343030302D383030302D303030303030303030303262" + }, + { + "name": "note_combining_4002_bytes", + "why": "e plus a combining acute is 3 bytes: 1334 of them are 4002 (rule: not expressible in the schema)", + "message": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": null, + "note": "éééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé" + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "0000002c-0000-4000-8000-00000000002c" + }, + "messagePackHex": "88AD736368656D6156657273696F6EBF67616D6563756C742E666F726B2E616E737765725F726571756573742E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A6616E7377657282A663686F696365C0A46E6F7465DA0FA265CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC8165CC81A9726574726163746564C2A87265747261637473C0AB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAA726571756573745F6964D92430303030303032632D303030302D343030302D383030302D303030303030303030303263" + }, + { + "name": "options_hash_uppercase", + "why": "hash is lowercase hex", + "message": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7D95FC87D532420611FE415C1DEF27A3447617BB68235F46E3AF130AEA361C48", + "answer": { + "choice": "no", + "note": null + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "0000002d-0000-4000-8000-00000000002d" + }, + "messagePackHex": "88AD736368656D6156657273696F6EBF67616D6563756C742E666F726B2E616E737765725F726571756573742E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037443935464338374435333234323036313146453431354331444546323741333434373631374242363832333546343645334146313330414541333631433438A6616E7377657282A663686F696365A26E6FA46E6F7465C0A9726574726163746564C2A87265747261637473C0AB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAA726571756573745F6964D92430303030303032642D303030302D343030302D383030302D303030303030303030303264" + }, + { + "name": "answerer_key", + "why": "answerer is server-set; the door answers body_forbidden_field", + "message": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + }, + "messagePackHex": "89AD736368656D6156657273696F6EBF67616D6563756C742E666F726B2E616E737765725F726571756573742E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A6616E7377657282A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EA9726574726163746564C2A87265747261637473C0AB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166" + }, + { + "name": "authority_key", + "why": "authority is server-set; the door answers body_forbidden_field", + "message": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001", + "authority": "Operator" + }, + "messagePackHex": "89AD736368656D6156657273696F6EBF67616D6563756C742E666F726B2E616E737765725F726571756573742E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A6616E7377657282A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EA9726574726163746564C2A87265747261637473C0AB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031A9617574686F72697479A84F70657261746F72" + }, + { + "name": "answer_object_with_a_key", + "why": "the answer object is closed", + "message": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": null, + "weight": 1 + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "0000002e-0000-4000-8000-00000000002e" + }, + "messagePackHex": "88AD736368656D6156657273696F6EBF67616D6563756C742E666F726B2E616E737765725F726571756573742E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A6616E7377657283A663686F696365A26E6FA46E6F7465C0A677656967687401A9726574726163746564C2A87265747261637473C0AB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAA726571756573745F6964D92430303030303032652D303030302D343030302D383030302D303030303030303030303265" + }, + { + "name": "answered_at_without_zone", + "why": "instants are UTC with a Z", + "message": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": null + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00", + "request_id": "0000002f-0000-4000-8000-00000000002f" + }, + "messagePackHex": "88AD736368656D6156657273696F6EBF67616D6563756C742E666F726B2E616E737765725F726571756573742E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A6616E7377657282A663686F696365A26E6FA46E6F7465C0A9726574726163746564C2A87265747261637473C0AB616E7377657265645F6174B3323032362D31312D31325431393A30323A3030AA726571756573745F6964D92430303030303032662D303030302D343030302D383030302D303030303030303030303266" + }, + { + "name": "retracted_not_a_boolean", + "why": "retracted is a boolean", + "message": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": "no", + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "messagePackHex": "88AD736368656D6156657273696F6EBF67616D6563756C742E666F726B2E616E737765725F726571756573742E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A6616E7377657282A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EA9726574726163746564A26E6FA87265747261637473C0AB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031" + }, + { + "name": "fork_id_empty", + "why": "fork_id names a fork", + "message": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": null + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000030-0000-4000-8000-000000000030" + }, + "messagePackHex": "88AD736368656D6156657273696F6EBF67616D6563756C742E666F726B2E616E737765725F726571756573742E7630A7666F726B5F6964A0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A6616E7377657282A663686F696365A26E6FA46E6F7465C0A9726574726163746564C2A87265747261637473C0AB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAA726571756573745F6964D92430303030303033302D303030302D343030302D383030302D303030303030303030303330" + } + ] + }, + "deskAnswer": { + "valid": [ + { + "name": "counted", + "message": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "messagePackHex": "DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031" + }, + { + "name": "revision", + "message": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n2", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Revision", + "of": "n1", + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "yes", + "note": "On reflection, share it.", + "answered_at": "2026-11-12T19:06:00Z", + "received_at": "2026-11-12T19:06:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000002-0000-4000-8000-000000000002" + }, + "messagePackHex": "DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E32A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A85265766973696F6EA26F66A26E31A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A3796573A46E6F7465B84F6E207265666C656374696F6E2C2073686172652069742EAB616E7377657265645F6174B4323032362D31312D31325431393A30363A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30363A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030322D303030302D343030302D383030302D303030303030303030303032" + }, + { + "name": "retraction", + "message": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n8", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Retraction", + "of": null, + "retracts": "n1", + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": null, + "note": null, + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:09:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000031-0000-4000-8000-000000000031" + }, + "messagePackHex": "DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E38A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65AA52657472616374696F6EA26F66C0A87265747261637473A26E31A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365C0A46E6F7465C0AB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30393A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303033312D303030302D343030302D383030302D303030303030303030303331" + }, + { + "name": "refused_stale", + "message": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": null, + "code": "stale_fork", + "options_hash": "14b8ffbdb61106dbc06f69f3fc8e2072af7a205d766e4cc040e89733013d54dc", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000032-0000-4000-8000-000000000032" + }, + "messagePackHex": "DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A752656675736564A26F66C0A87265747261637473C0A4636F6465AA7374616C655F666F726BAC6F7074696F6E735F68617368D94031346238666662646236313130366462633036663639663366633865323037326166376132303564373636653463633034306538393733333031336435346463A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303033322D303030302D343030302D383030302D303030303030303030303332" + }, + { + "name": "refused_closed_retraction_names_its_target", + "message": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n9", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": "n1", + "code": "fork_closed", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": null, + "note": null, + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:09:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000033-0000-4000-8000-000000000033" + }, + "messagePackHex": "DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E39A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A752656675736564A26F66C0A87265747261637473A26E31A4636F6465AB666F726B5F636C6F736564AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365C0A46E6F7465C0AB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30393A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303033332D303030302D343030302D383030302D303030303030303030303333" + } + ], + "invalid": [ + { + "name": "counted_with_a_code", + "why": "only Refused carries a code", + "message": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": "fork_closed", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "messagePackHex": "DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465AB666F726B5F636C6F736564AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031" + }, + { + "name": "refused_without_a_code", + "why": "Refused carries a reason code", + "message": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n5", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:07:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "messagePackHex": "DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E35A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A752656675736564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30373A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031" + }, + { + "name": "refused_with_an_unknown_code", + "why": "the code set is closed", + "message": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n5", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": null, + "code": "because", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:07:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "messagePackHex": "DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E35A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A752656675736564A26F66C0A87265747261637473C0A4636F6465A762656361757365AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30373A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031" + }, + { + "name": "counted_naming_another_answer", + "why": "Counted names none", + "message": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": "n0", + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "messagePackHex": "DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7436F756E746564A26F66A26E30A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031" + }, + { + "name": "revision_naming_none", + "why": "a Revision names the counted answer", + "message": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n2", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Revision", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "yes", + "note": "On reflection, share it.", + "answered_at": "2026-11-12T19:06:00Z", + "received_at": "2026-11-12T19:06:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000002-0000-4000-8000-000000000002" + }, + "messagePackHex": "DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E32A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A85265766973696F6EA26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A3796573A46E6F7465B84F6E207265666C656374696F6E2C2073686172652069742EAB616E7377657265645F6174B4323032362D31312D31325431393A30363A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30363A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030322D303030302D343030302D383030302D303030303030303030303032" + }, + { + "name": "revision_that_also_retracts", + "why": "a variant carries only its own link (a retraction with no target, or a target with no retraction, cannot exist)", + "message": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n2", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Revision", + "of": "n1", + "retracts": "n1", + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "yes", + "note": "On reflection, share it.", + "answered_at": "2026-11-12T19:06:00Z", + "received_at": "2026-11-12T19:06:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000002-0000-4000-8000-000000000002" + }, + "messagePackHex": "DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E32A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A85265766973696F6EA26F66A26E31A87265747261637473A26E31A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A3796573A46E6F7465B84F6E207265666C656374696F6E2C2073686172652069742EAB616E7377657265645F6174B4323032362D31312D31325431393A30363A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30363A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030322D303030302D343030302D383030302D303030303030303030303032" + }, + { + "name": "retraction_naming_no_target", + "why": "a Retraction names the answer it retracts", + "message": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n8", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Retraction", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": null, + "note": null, + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:09:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000034-0000-4000-8000-000000000034" + }, + "messagePackHex": "DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E38A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65AA52657472616374696F6EA26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365C0A46E6F7465C0AB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30393A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303033342D303030302D343030302D383030302D303030303030303030303334" + }, + { + "name": "refused_naming_another_answer_as_of", + "why": "a Refused answer names no counted answer (`of` is a Revision's)", + "message": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n5", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": "n1", + "retracts": null, + "code": "fork_closed", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:07:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "messagePackHex": "DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E35A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A752656675736564A26F66A26E31A87265747261637473C0A4636F6465AB666F726B5F636C6F736564AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30373A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031" + }, + { + "name": "refused_with_a_target_that_is_not_an_answer_id", + "why": "a Refused answer's retracts is nil or an answer id (n), never the stored key", + "message": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n5", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": "cultlib-gaps:desk_answer:slot-mark-consent.n1", + "code": "fork_closed", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:07:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "messagePackHex": "DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E35A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A752656675736564A26F66C0A87265747261637473D92D63756C746C69622D676170733A6465736B5F616E737765723A736C6F742D6D61726B2D636F6E73656E742E6E31A4636F6465AB666F726B5F636C6F736564AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30373A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031" + }, + { + "name": "retraction_carrying_a_choice", + "why": "a Retraction carries no choice or note", + "message": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n8", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Retraction", + "of": null, + "retracts": "n1", + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": null, + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:09:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000035-0000-4000-8000-000000000035" + }, + "messagePackHex": "DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E38A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65AA52657472616374696F6EA26F66C0A87265747261637473A26E31A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465C0AB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30393A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303033352D303030302D343030302D383030302D303030303030303030303335" + }, + { + "name": "outcome_lower_case", + "why": "outcome tags are Counted, Revision, Retraction, Refused", + "message": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "messagePackHex": "DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7636F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031" + }, + { + "name": "no_answerer", + "why": "answerer is stamped on every stored answer", + "message": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "messagePackHex": "DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572A0A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031" + } + ] + }, + "results": [ + { + "name": "stale_fork", + "httpStatus": 409, + "message": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": null, + "code": "stale_fork", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "counted_answer_id": null + }, + "messagePackHex": "84AD736368656D6156657273696F6EBE67616D6563756C742E666F726B2E616E737765725F726573756C742E7630A6616E73776572DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A752656675736564A26F66C0A87265747261637473C0A4636F6465AA7374616C655F666F726BAC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031A463617264DE0010AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E636172642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A57469746C65B1536C6F74206D61726B20636F6E73656E74A7636F6E74657874BD53686F756C64206120736C6F74206D61726B206265207368617265643FA76F7074696F6E739282A26964A3796573A474657874A953686172652069742E82A26964A26E6FA474657874AE4B656570206974206C6F63616C2EAE7265636F6D6D656E646174696F6EA26E6FA7646570656E647391D92967616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365A56C65616E73C0A4666C6167C0A97261697365645F696E82A46B696E64A743757453706563A26964D92B63756C746C69622D676170733A6375745F737065633A6375742D736C6F742D746F6C6572616E63652E7234A8646563696465727391D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A6737461747573A46F70656EA672756C696E67C0AA72756C65645F66726F6D90A56F7264657203AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964C0" + }, + { + "name": "not_a_decider", + "httpStatus": 403, + "message": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": null, + "code": "not_a_decider", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": null + }, + "messagePackHex": "84AD736368656D6156657273696F6EBE67616D6563756C742E666F726B2E616E737765725F726573756C742E7630A6616E73776572DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A752656675736564A26F66C0A87265747261637473C0A4636F6465AD6E6F745F615F64656369646572AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031A463617264C0B1636F756E7465645F616E737765725F6964C0" + }, + { + "name": "fork_closed", + "httpStatus": 409, + "message": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": null, + "code": "fork_closed", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": null + }, + "messagePackHex": "84AD736368656D6156657273696F6EBE67616D6563756C742E666F726B2E616E737765725F726573756C742E7630A6616E73776572DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A752656675736564A26F66C0A87265747261637473C0A4636F6465AB666F726B5F636C6F736564AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031A463617264C0B1636F756E7465645F616E737765725F6964C0" + }, + { + "name": "choice_invalid", + "httpStatus": 400, + "message": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": null, + "code": "choice_invalid", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": null + }, + "messagePackHex": "84AD736368656D6156657273696F6EBE67616D6563756C742E666F726B2E616E737765725F726573756C742E7630A6616E73776572DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A752656675736564A26F66C0A87265747261637473C0A4636F6465AE63686F6963655F696E76616C6964AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031A463617264C0B1636F756E7465645F616E737765725F6964C0" + }, + { + "name": "bad_retraction", + "httpStatus": 400, + "message": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": null, + "code": "bad_retraction", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": null + }, + "messagePackHex": "84AD736368656D6156657273696F6EBE67616D6563756C742E666F726B2E616E737765725F726573756C742E7630A6616E73776572DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A752656675736564A26F66C0A87265747261637473C0A4636F6465AE6261645F72657472616374696F6EAC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031A463617264C0B1636F756E7465645F616E737765725F6964C0" + }, + { + "name": "already_retracted", + "httpStatus": 409, + "message": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": null, + "code": "already_retracted", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": null + }, + "messagePackHex": "84AD736368656D6156657273696F6EBE67616D6563756C742E666F726B2E616E737765725F726573756C742E7630A6616E73776572DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A752656675736564A26F66C0A87265747261637473C0A4636F6465B1616C72656164795F726574726163746564AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031A463617264C0B1636F756E7465645F616E737765725F6964C0" + } + ], + "doorRefusals": [ + { + "name": "unauthenticated", + "httpStatus": 401, + "message": { + "schemaVersion": "gamecult.fork.refusal.v0", + "code": "unauthenticated", + "error": "unauthenticated: a verified session is required" + }, + "messagePackHex": "83AD736368656D6156657273696F6EB867616D6563756C742E666F726B2E7265667573616C2E7630A4636F6465AF756E61757468656E74696361746564A56572726F72D92F756E61757468656E746963617465643A20612076657269666965642073657373696F6E206973207265717569726564" + }, + { + "name": "body_forbidden_field", + "httpStatus": 400, + "message": { + "schemaVersion": "gamecult.fork.refusal.v0", + "code": "body_forbidden_field", + "error": "body_forbidden_field: answerer and authority are set by the receiver" + }, + "messagePackHex": "83AD736368656D6156657273696F6EB867616D6563756C742E666F726B2E7265667573616C2E7630A4636F6465B4626F64795F666F7262696464656E5F6669656C64A56572726F72D944626F64795F666F7262696464656E5F6669656C643A20616E73776572657220616E6420617574686F72697479206172652073657420627920746865207265636569766572" + }, + { + "name": "body_unknown_field", + "httpStatus": 400, + "message": { + "schemaVersion": "gamecult.fork.refusal.v0", + "code": "body_unknown_field", + "error": "body_unknown_field: the body is a closed shape" + }, + "messagePackHex": "83AD736368656D6156657273696F6EB867616D6563756C742E666F726B2E7265667573616C2E7630A4636F6465B2626F64795F756E6B6E6F776E5F6669656C64A56572726F72D92E626F64795F756E6B6E6F776E5F6669656C643A2074686520626F6479206973206120636C6F736564207368617065" + }, + { + "name": "body_invalid", + "httpStatus": 400, + "message": { + "schemaVersion": "gamecult.fork.refusal.v0", + "code": "body_invalid", + "error": "body_invalid: the body does not satisfy gamecult.fork.answer_request.v0" + }, + "messagePackHex": "83AD736368656D6156657273696F6EB867616D6563756C742E666F726B2E7265667573616C2E7630A4636F6465AC626F64795F696E76616C6964A56572726F72D947626F64795F696E76616C69643A2074686520626F647920646F6573206E6F7420736174697366792067616D6563756C742E666F726B2E616E737765725F726571756573742E7630" + }, + { + "name": "fork_unknown", + "httpStatus": 404, + "message": { + "schemaVersion": "gamecult.fork.refusal.v0", + "code": "fork_unknown", + "error": "fork_unknown: no such fork" + }, + "messagePackHex": "83AD736368656D6156657273696F6EB867616D6563756C742E666F726B2E7265667573616C2E7630A4636F6465AC666F726B5F756E6B6E6F776EA56572726F72BA666F726B5F756E6B6E6F776E3A206E6F207375636820666F726B" + }, + { + "name": "insufficient_scope", + "httpStatus": 403, + "message": { + "schemaVersion": "gamecult.fork.refusal.v0", + "code": "insufficient_scope", + "error": "insufficient_scope: the token lacks the scope this route needs" + }, + "messagePackHex": "83AD736368656D6156657273696F6EB867616D6563756C742E666F726B2E7265667573616C2E7630A4636F6465B2696E73756666696369656E745F73636F7065A56572726F72D93E696E73756666696369656E745F73636F70653A2074686520746F6B656E206C61636B73207468652073636F7065207468697320726F757465206E65656473" + }, + { + "name": "forbidden", + "httpStatus": 403, + "message": { + "schemaVersion": "gamecult.fork.refusal.v0", + "code": "forbidden", + "error": "forbidden: only Active members may use the desk" + }, + "messagePackHex": "83AD736368656D6156657273696F6EB867616D6563756C742E666F726B2E7265667573616C2E7630A4636F6465A9666F7262696464656EA56572726F72D92F666F7262696464656E3A206F6E6C7920416374697665206D656D62657273206D61792075736520746865206465736B" + }, + { + "name": "request_id_conflict", + "httpStatus": 409, + "message": { + "schemaVersion": "gamecult.fork.refusal.v0", + "code": "request_id_conflict", + "error": "request_id_conflict: this request_id was used for a different body" + }, + "messagePackHex": "83AD736368656D6156657273696F6EB867616D6563756C742E666F726B2E7265667573616C2E7630A4636F6465B3726571756573745F69645F636F6E666C696374A56572726F72D942726571756573745F69645F636F6E666C6963743A207468697320726571756573745F696420776173207573656420666F72206120646966666572656E7420626F6479" + }, + { + "name": "request_invalid", + "httpStatus": 400, + "message": { + "schemaVersion": "gamecult.fork.refusal.v0", + "code": "request_invalid", + "error": "request_invalid: the request does not satisfy its document" + }, + "messagePackHex": "83AD736368656D6156657273696F6EB867616D6563756C742E666F726B2E7265667573616C2E7630A4636F6465AF726571756573745F696E76616C6964A56572726F72D93A726571756573745F696E76616C69643A20746865207265717565737420646F6573206E6F7420736174697366792069747320646F63756D656E74" + } + ], + "unavailable": [ + { + "name": "second_door_down", + "httpStatus": 503, + "message": { + "schemaVersion": "gamecult.fork.unavailable.v0", + "code": "unavailable", + "error": "unavailable: the outcome is not confirmed, try again with the same request_id", + "retryAfterSeconds": 30 + }, + "messagePackHex": "84AD736368656D6156657273696F6EBC67616D6563756C742E666F726B2E756E617661696C61626C652E7630A4636F6465AB756E617661696C61626C65A56572726F72D94D756E617661696C61626C653A20746865206F7574636F6D65206973206E6F7420636F6E6669726D65642C2074727920616761696E2077697468207468652073616D6520726571756573745F6964B1726574727941667465725365636F6E64731E" + }, + { + "name": "second_door_down_no_hint", + "httpStatus": 503, + "message": { + "schemaVersion": "gamecult.fork.unavailable.v0", + "code": "unavailable", + "error": "unavailable: the outcome is not confirmed, try again with the same request_id", + "retryAfterSeconds": null + }, + "messagePackHex": "84AD736368656D6156657273696F6EBC67616D6563756C742E666F726B2E756E617661696C61626C652E7630A4636F6465AB756E617661696C61626C65A56572726F72D94D756E617661696C61626C653A20746865206F7574636F6D65206973206E6F7420636F6E6669726D65642C2074727920616761696E2077697468207468652073616D6520726571756573745F6964B1726574727941667465725365636F6E6473C0" + } + ], + "rulingCites": [ + { + "name": "operator_cites_a_counted_operator_answer", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + } + ], + "message": { + "schemaVersion": "gamecult.fork.ruling_cite.v0", + "ruling_id": "cultlib-gaps:resolution:question.slot-mark-consent.n1", + "authority": "Operator", + "answer_id": "n1" + }, + "admit": true, + "messagePackHex": "84AD736368656D6156657273696F6EBC67616D6563756C742E666F726B2E72756C696E675F636974652E7630A972756C696E675F6964D93563756C746C69622D676170733A7265736F6C7574696F6E3A7175657374696F6E2E736C6F742D6D61726B2D636F6E73656E742E6E31A9617574686F72697479A84F70657261746F72A9616E737765725F6964A26E31" + }, + { + "name": "operator_cites_nothing", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + } + ], + "message": { + "schemaVersion": "gamecult.fork.ruling_cite.v0", + "ruling_id": "cultlib-gaps:resolution:question.slot-mark-consent.n1", + "authority": "Operator", + "answer_id": null + }, + "admit": false, + "messagePackHex": "84AD736368656D6156657273696F6EBC67616D6563756C742E666F726B2E72756C696E675F636974652E7630A972756C696E675F6964D93563756C746C69622D676170733A7265736F6C7574696F6E3A7175657374696F6E2E736C6F742D6D61726B2D636F6E73656E742E6E31A9617574686F72697479A84F70657261746F72A9616E737765725F6964C0" + }, + { + "name": "operator_cites_a_revision", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n2", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Revision", + "of": "n1", + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "yes", + "note": "On reflection, share it.", + "answered_at": "2026-11-12T19:06:00Z", + "received_at": "2026-11-12T19:06:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000002-0000-4000-8000-000000000002" + } + ], + "message": { + "schemaVersion": "gamecult.fork.ruling_cite.v0", + "ruling_id": "cultlib-gaps:resolution:question.slot-mark-consent.n1", + "authority": "Operator", + "answer_id": "n2" + }, + "admit": false, + "messagePackHex": "84AD736368656D6156657273696F6EBC67616D6563756C742E666F726B2E72756C696E675F636974652E7630A972756C696E675F6964D93563756C746C69622D676170733A7265736F6C7574696F6E3A7175657374696F6E2E736C6F742D6D61726B2D636F6E73656E742E6E31A9617574686F72697479A84F70657261746F72A9616E737765725F6964A26E32" + }, + { + "name": "operator_cites_a_refused_answer", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n5", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": null, + "code": "fork_closed", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:07:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + } + ], + "message": { + "schemaVersion": "gamecult.fork.ruling_cite.v0", + "ruling_id": "cultlib-gaps:resolution:question.slot-mark-consent.n1", + "authority": "Operator", + "answer_id": "n5" + }, + "admit": false, + "messagePackHex": "84AD736368656D6156657273696F6EBC67616D6563756C742E666F726B2E72756C696E675F636974652E7630A972756C696E675F6964D93563756C746C69622D676170733A7265736F6C7574696F6E3A7175657374696F6E2E736C6F742D6D61726B2D636F6E73656E742E6E31A9617574686F72697479A84F70657261746F72A9616E737765725F6964A26E35" + }, + { + "name": "operator_cites_a_counted_answer_by_a_non_operator", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n4", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "yes", + "note": "Share, please.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:03:05Z", + "claim_in_future": false, + "answerer": "11111111-2222-4333-8444-555555555555", + "authority": "Member", + "request_id": "00000004-0000-4000-8000-000000000004" + } + ], + "message": { + "schemaVersion": "gamecult.fork.ruling_cite.v0", + "ruling_id": "cultlib-gaps:resolution:question.slot-mark-consent.n1", + "authority": "Operator", + "answer_id": "n4" + }, + "admit": false, + "messagePackHex": "84AD736368656D6156657273696F6EBC67616D6563756C742E666F726B2E72756C696E675F636974652E7630A972756C696E675F6964D93563756C746C69622D676170733A7265736F6C7574696F6E3A7175657374696F6E2E736C6F742D6D61726B2D636F6E73656E742E6E31A9617574686F72697479A84F70657261746F72A9616E737765725F6964A26E34" + }, + { + "name": "operator_cites_an_answer_that_does_not_exist", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + } + ], + "message": { + "schemaVersion": "gamecult.fork.ruling_cite.v0", + "ruling_id": "cultlib-gaps:resolution:question.slot-mark-consent.n1", + "authority": "Operator", + "answer_id": "n99" + }, + "admit": false, + "messagePackHex": "84AD736368656D6156657273696F6EBC67616D6563756C742E666F726B2E72756C696E675F636974652E7630A972756C696E675F6964D93563756C746C69622D676170733A7265736F6C7574696F6E3A7175657374696F6E2E736C6F742D6D61726B2D636F6E73656E742E6E31A9617574686F72697479A84F70657261746F72A9616E737765725F6964A36E3939" + }, + { + "name": "steward_needs_no_citation", + "answers": [], + "message": { + "schemaVersion": "gamecult.fork.ruling_cite.v0", + "ruling_id": "cultlib-gaps:resolution:question.slot-mark-consent.n1", + "authority": "Steward", + "answer_id": null + }, + "admit": true, + "messagePackHex": "84AD736368656D6156657273696F6EBC67616D6563756C742E666F726B2E72756C696E675F636974652E7630A972756C696E675F6964D93563756C746C69622D676170733A7265736F6C7574696F6E3A7175657374696F6E2E736C6F742D6D61726B2D636F6E73656E742E6E31A9617574686F72697479A753746577617264A9616E737765725F6964C0" + } + ], + "transport": { + "listRequest": { + "valid": [ + { + "name": "list_open_page", + "message": { + "schemaVersion": "gamecult.fork.list_request.v0", + "mind": "gamecult-chat", + "status": "open", + "cursor": null, + "limit": 2 + }, + "messagePackHex": "85AD736368656D6156657273696F6EBD67616D6563756C742E666F726B2E6C6973745F726571756573742E7630A46D696E64AD67616D6563756C742D63686174A6737461747573A46F70656EA6637572736F72C0A56C696D697402" + }, + { + "name": "list_next_page", + "message": { + "schemaVersion": "gamecult.fork.list_request.v0", + "mind": "gamecult-chat", + "status": "open", + "cursor": "c4", + "limit": 2 + }, + "messagePackHex": "85AD736368656D6156657273696F6EBD67616D6563756C742E666F726B2E6C6973745F726571756573742E7630A46D696E64AD67616D6563756C742D63686174A6737461747573A46F70656EA6637572736F72A26334A56C696D697402" + }, + { + "name": "list_count_only", + "message": { + "schemaVersion": "gamecult.fork.list_request.v0", + "mind": "gamecult-chat", + "status": null, + "cursor": null, + "limit": 0 + }, + "messagePackHex": "85AD736368656D6156657273696F6EBD67616D6563756C742E666F726B2E6C6973745F726571756573742E7630A46D696E64AD67616D6563756C742D63686174A6737461747573C0A6637572736F72C0A56C696D697400" + } + ], + "invalid": [ + { + "name": "limit_51", + "why": "a page is at most 50 forks", + "message": { + "schemaVersion": "gamecult.fork.list_request.v0", + "mind": "gamecult-chat", + "status": null, + "cursor": null, + "limit": 51 + }, + "messagePackHex": "85AD736368656D6156657273696F6EBD67616D6563756C742E666F726B2E6C6973745F726571756573742E7630A46D696E64AD67616D6563756C742D63686174A6737461747573C0A6637572736F72C0A56C696D697433" + }, + { + "name": "limit_negative", + "why": "limit is 0 to 50", + "message": { + "schemaVersion": "gamecult.fork.list_request.v0", + "mind": "gamecult-chat", + "status": null, + "cursor": null, + "limit": -1 + }, + "messagePackHex": "85AD736368656D6156657273696F6EBD67616D6563756C742E666F726B2E6C6973745F726571756573742E7630A46D696E64AD67616D6563756C742D63686174A6737461747573C0A6637572736F72C0A56C696D6974FF" + }, + { + "name": "status_all", + "why": "status is open, recorded or nil", + "message": { + "schemaVersion": "gamecult.fork.list_request.v0", + "mind": "gamecult-chat", + "status": "all", + "cursor": null, + "limit": 10 + }, + "messagePackHex": "85AD736368656D6156657273696F6EBD67616D6563756C742E666F726B2E6C6973745F726571756573742E7630A46D696E64AD67616D6563756C742D63686174A6737461747573A3616C6CA6637572736F72C0A56C696D69740A" + }, + { + "name": "cursor_malformed", + "why": "a cursor is c", + "message": { + "schemaVersion": "gamecult.fork.list_request.v0", + "mind": "gamecult-chat", + "status": null, + "cursor": "next", + "limit": 10 + }, + "messagePackHex": "85AD736368656D6156657273696F6EBD67616D6563756C742E666F726B2E6C6973745F726571756573742E7630A46D696E64AD67616D6563756C742D63686174A6737461747573C0A6637572736F72A46E657874A56C696D69740A" + }, + { + "name": "cursor_zero", + "why": "a cursor names a positive order", + "message": { + "schemaVersion": "gamecult.fork.list_request.v0", + "mind": "gamecult-chat", + "status": null, + "cursor": "c0", + "limit": 10 + }, + "messagePackHex": "85AD736368656D6156657273696F6EBD67616D6563756C742E666F726B2E6C6973745F726571756573742E7630A46D696E64AD67616D6563756C742D63686174A6737461747573C0A6637572736F72A26330A56C696D69740A" + }, + { + "name": "mind_empty", + "why": "a list names a Mind", + "message": { + "schemaVersion": "gamecult.fork.list_request.v0", + "mind": "", + "status": null, + "cursor": null, + "limit": 10 + }, + "messagePackHex": "85AD736368656D6156657273696F6EBD67616D6563756C742E666F726B2E6C6973745F726571756573742E7630A46D696E64A0A6737461747573C0A6637572736F72C0A56C696D69740A" + }, + { + "name": "unknown_key", + "why": "closed document", + "message": { + "schemaVersion": "gamecult.fork.list_request.v0", + "mind": "gamecult-chat", + "status": null, + "cursor": null, + "limit": 10, + "sort": "oldest" + }, + "messagePackHex": "86AD736368656D6156657273696F6EBD67616D6563756C742E666F726B2E6C6973745F726571756573742E7630A46D696E64AD67616D6563756C742D63686174A6737461747573C0A6637572736F72C0A56C696D69740AA4736F7274A66F6C64657374" + } + ] + }, + "readRequest": { + "valid": [ + { + "name": "read_one", + "message": { + "schemaVersion": "gamecult.fork.read_request.v0", + "mind": "gamecult-chat", + "fork_id": "gamecult-chat:question:slot-mark-consent" + }, + "messagePackHex": "83AD736368656D6156657273696F6EBD67616D6563756C742E666F726B2E726561645F726571756573742E7630A46D696E64AD67616D6563756C742D63686174A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74" + } + ], + "invalid": [ + { + "name": "fork_id_empty", + "why": "a read names a fork", + "message": { + "schemaVersion": "gamecult.fork.read_request.v0", + "mind": "gamecult-chat", + "fork_id": "" + }, + "messagePackHex": "83AD736368656D6156657273696F6EBD67616D6563756C742E666F726B2E726561645F726571756573742E7630A46D696E64AD67616D6563756C742D63686174A7666F726B5F6964A0" + }, + { + "name": "mind_empty", + "why": "a read names a Mind", + "message": { + "schemaVersion": "gamecult.fork.read_request.v0", + "mind": "", + "fork_id": "gamecult-chat:question:slot-mark-consent" + }, + "messagePackHex": "83AD736368656D6156657273696F6EBD67616D6563756C742E666F726B2E726561645F726571756573742E7630A46D696E64A0A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74" + }, + { + "name": "mind_missing", + "why": "map-mode writes every key; the Mind is the route's", + "message": { + "schemaVersion": "gamecult.fork.read_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent" + }, + "messagePackHex": "82AD736368656D6156657273696F6EBD67616D6563756C742E666F726B2E726561645F726571756573742E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74" + } + ] + }, + "entry": { + "valid": [ + { + "name": "open_no_answers", + "message": { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q4", + "title": "Fork 4", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 4, + "options_hash": "a64604237f906b6fec9cb292863685d966abf8048f276afa24cd4f1e1c4c74aa" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q4", + "options_hash": "a64604237f906b6fec9cb292863685d966abf8048f276afa24cd4f1e1c4c74aa", + "counted_answer_id": null, + "answers": [] + } + }, + "messagePackHex": "83AD736368656D6156657273696F6EB667616D6563756C742E666F726B2E656E7472792E7630A463617264DE0010AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E636172642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7134A57469746C65A6466F726B2034A7636F6E74657874BD53686F756C64206120736C6F74206D61726B206265207368617265643FA76F7074696F6E739282A26964A3796573A474657874A953686172652069742E82A26964A26E6FA474657874AE4B656570206974206C6F63616C2EAE7265636F6D6D656E646174696F6EA26E6FA7646570656E647391D92967616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365A56C65616E73C0A4666C6167C0A97261697365645F696E82A46B696E64A743757453706563A26964D92B63756C746C69622D676170733A6375745F737065633A6375742D736C6F742D746F6C6572616E63652E7234A8646563696465727391D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A6737461747573A46F70656EA672756C696E67C0AA72756C65645F66726F6D90A56F7264657204AC6F7074696F6E735F68617368D94061363436303432333766393036623666656339636232393238363336383564393636616266383034386632373661666132346364346631653163346337346161A67265636F726485AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7134AC6F7074696F6E735F68617368D94061363436303432333766393036623666656339636232393238363336383564393636616266383034386632373661666132346364346631653163346337346161B1636F756E7465645F616E737765725F6964C0A7616E737765727390" + }, + { + "name": "open_counted", + "message": { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q5", + "title": "Fork 5", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 5, + "options_hash": "16d20a6dc44b6cc00afd378e9f0c8b51dfe5b07e869e1a2aa9588de1de587fab" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q5", + "options_hash": "16d20a6dc44b6cc00afd378e9f0c8b51dfe5b07e869e1a2aa9588de1de587fab", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:q5", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "16d20a6dc44b6cc00afd378e9f0c8b51dfe5b07e869e1a2aa9588de1de587fab", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000020-0000-4000-8000-000000000020" + } + ] + } + }, + "messagePackHex": "83AD736368656D6156657273696F6EB667616D6563756C742E666F726B2E656E7472792E7630A463617264DE0010AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E636172642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7135A57469746C65A6466F726B2035A7636F6E74657874BD53686F756C64206120736C6F74206D61726B206265207368617265643FA76F7074696F6E739282A26964A3796573A474657874A953686172652069742E82A26964A26E6FA474657874AE4B656570206974206C6F63616C2EAE7265636F6D6D656E646174696F6EA26E6FA7646570656E647391D92967616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365A56C65616E73C0A4666C6167C0A97261697365645F696E82A46B696E64A743757453706563A26964D92B63756C746C69622D676170733A6375745F737065633A6375742D736C6F742D746F6C6572616E63652E7234A8646563696465727391D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A6737461747573A46F70656EA672756C696E67C0AA72756C65645F66726F6D90A56F7264657205AC6F7074696F6E735F68617368D94031366432306136646334346236636330306166643337386539663063386235316466653562303765383639653161326161393538386465316465353837666162A67265636F726485AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7135AC6F7074696F6E735F68617368D94031366432306136646334346236636330306166643337386539663063386235316466653562303765383639653161326161393538386465316465353837666162B1636F756E7465645F616E737765725F6964A26E31A7616E737765727391DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7135A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94031366432306136646334346236636330306166643337386539663063386235316466653562303765383639653161326161393538386465316465353837666162A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303032302D303030302D343030302D383030302D303030303030303030303230" + }, + { + "name": "closed_with_ruling_and_a_refusal", + "message": { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q3", + "title": "Fork 3", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "closed", + "ruling": "cultlib-gaps:resolution:question.q3.n1", + "ruled_from": [ + "n1" + ], + "order": 3, + "options_hash": "ced892eb663c22e3d7ece602708043ee545f56b9d4e4421d7f5bfaa86c418ce2" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q3", + "options_hash": "ced892eb663c22e3d7ece602708043ee545f56b9d4e4421d7f5bfaa86c418ce2", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:q3", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "ced892eb663c22e3d7ece602708043ee545f56b9d4e4421d7f5bfaa86c418ce2", + "choice": "yes", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000021-0000-4000-8000-000000000021" + }, + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n2", + "fork_id": "gamecult-chat:question:q3", + "outcome": "Refused", + "of": null, + "retracts": null, + "code": "fork_closed", + "options_hash": "ced892eb663c22e3d7ece602708043ee545f56b9d4e4421d7f5bfaa86c418ce2", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:04:05Z", + "claim_in_future": false, + "answerer": "9f8e7d6c-5b4a-4392-8170-0f1e2d3c4b5a", + "authority": "Steward", + "request_id": "00000022-0000-4000-8000-000000000022" + } + ] + } + }, + "messagePackHex": "83AD736368656D6156657273696F6EB667616D6563756C742E666F726B2E656E7472792E7630A463617264DE0010AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E636172642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7133A57469746C65A6466F726B2033A7636F6E74657874BD53686F756C64206120736C6F74206D61726B206265207368617265643FA76F7074696F6E739282A26964A3796573A474657874A953686172652069742E82A26964A26E6FA474657874AE4B656570206974206C6F63616C2EAE7265636F6D6D656E646174696F6EA26E6FA7646570656E647391D92967616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365A56C65616E73C0A4666C6167C0A97261697365645F696E82A46B696E64A743757453706563A26964D92B63756C746C69622D676170733A6375745F737065633A6375742D736C6F742D746F6C6572616E63652E7234A8646563696465727391D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A6737461747573A6636C6F736564A672756C696E67D92663756C746C69622D676170733A7265736F6C7574696F6E3A7175657374696F6E2E71332E6E31AA72756C65645F66726F6D91A26E31A56F7264657203AC6F7074696F6E735F68617368D94063656438393265623636336332326533643765636536303237303830343365653534356635366239643465343432316437663562666161383663343138636532A67265636F726485AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7133AC6F7074696F6E735F68617368D94063656438393265623636336332326533643765636536303237303830343365653534356635366239643465343432316437663562666161383663343138636532B1636F756E7465645F616E737765725F6964A26E31A7616E737765727392DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7133A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94063656438393265623636336332326533643765636536303237303830343365653534356635366239643465343432316437663562666161383663343138636532A663686F696365A3796573A46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303032312D303030302D343030302D383030302D303030303030303030303231DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E32A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7133A76F7574636F6D65A752656675736564A26F66C0A87265747261637473C0A4636F6465AB666F726B5F636C6F736564AC6F7074696F6E735F68617368D94063656438393265623636336332326533643765636536303237303830343365653534356635366239643465343432316437663562666161383663343138636532A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30343A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92439663865376436632D356234612D343339322D383137302D306631653264336334623561A9617574686F72697479A753746577617264AA726571756573745F6964D92430303030303032322D303030302D343030302D383030302D303030303030303030303232" + } + ], + "invalid": [ + { + "name": "entry_without_record", + "why": "an entry holds the card and its record", + "message": { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q5", + "title": "Fork 5", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 5, + "options_hash": "16d20a6dc44b6cc00afd378e9f0c8b51dfe5b07e869e1a2aa9588de1de587fab" + } + }, + "messagePackHex": "82AD736368656D6156657273696F6EB667616D6563756C742E666F726B2E656E7472792E7630A463617264DE0010AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E636172642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7135A57469746C65A6466F726B2035A7636F6E74657874BD53686F756C64206120736C6F74206D61726B206265207368617265643FA76F7074696F6E739282A26964A3796573A474657874A953686172652069742E82A26964A26E6FA474657874AE4B656570206974206C6F63616C2EAE7265636F6D6D656E646174696F6EA26E6FA7646570656E647391D92967616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365A56C65616E73C0A4666C6167C0A97261697365645F696E82A46B696E64A743757453706563A26964D92B63756C746C69622D676170733A6375745F737065633A6375742D736C6F742D746F6C6572616E63652E7234A8646563696465727391D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A6737461747573A46F70656EA672756C696E67C0AA72756C65645F66726F6D90A56F7264657205AC6F7074696F6E735F68617368D94031366432306136646334346236636330306166643337386539663063386235316466653562303765383639653161326161393538386465316465353837666162" + }, + { + "name": "record_of_another_fork", + "why": "rule: the record names the card's fork (not expressible in the schema)", + "message": { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q5", + "title": "Fork 5", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 5, + "options_hash": "16d20a6dc44b6cc00afd378e9f0c8b51dfe5b07e869e1a2aa9588de1de587fab" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q9", + "options_hash": "16d20a6dc44b6cc00afd378e9f0c8b51dfe5b07e869e1a2aa9588de1de587fab", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:q5", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "16d20a6dc44b6cc00afd378e9f0c8b51dfe5b07e869e1a2aa9588de1de587fab", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000020-0000-4000-8000-000000000020" + } + ] + } + }, + "messagePackHex": "83AD736368656D6156657273696F6EB667616D6563756C742E666F726B2E656E7472792E7630A463617264DE0010AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E636172642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7135A57469746C65A6466F726B2035A7636F6E74657874BD53686F756C64206120736C6F74206D61726B206265207368617265643FA76F7074696F6E739282A26964A3796573A474657874A953686172652069742E82A26964A26E6FA474657874AE4B656570206974206C6F63616C2EAE7265636F6D6D656E646174696F6EA26E6FA7646570656E647391D92967616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365A56C65616E73C0A4666C6167C0A97261697365645F696E82A46B696E64A743757453706563A26964D92B63756C746C69622D676170733A6375745F737065633A6375742D736C6F742D746F6C6572616E63652E7234A8646563696465727391D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A6737461747573A46F70656EA672756C696E67C0AA72756C65645F66726F6D90A56F7264657205AC6F7074696F6E735F68617368D94031366432306136646334346236636330306166643337386539663063386235316466653562303765383639653161326161393538386465316465353837666162A67265636F726485AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7139AC6F7074696F6E735F68617368D94031366432306136646334346236636330306166643337386539663063386235316466653562303765383639653161326161393538386465316465353837666162B1636F756E7465645F616E737765725F6964A26E31A7616E737765727391DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7135A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94031366432306136646334346236636330306166643337386539663063386235316466653562303765383639653161326161393538386465316465353837666162A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303032302D303030302D343030302D383030302D303030303030303030303230" + }, + { + "name": "record_under_another_hash", + "why": "rule: the record is under the card's options_hash (not expressible in the schema)", + "message": { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q5", + "title": "Fork 5", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 5, + "options_hash": "16d20a6dc44b6cc00afd378e9f0c8b51dfe5b07e869e1a2aa9588de1de587fab" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q5", + "options_hash": "14b8ffbdb61106dbc06f69f3fc8e2072af7a205d766e4cc040e89733013d54dc", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:q5", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "16d20a6dc44b6cc00afd378e9f0c8b51dfe5b07e869e1a2aa9588de1de587fab", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000020-0000-4000-8000-000000000020" + } + ] + } + }, + "messagePackHex": "83AD736368656D6156657273696F6EB667616D6563756C742E666F726B2E656E7472792E7630A463617264DE0010AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E636172642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7135A57469746C65A6466F726B2035A7636F6E74657874BD53686F756C64206120736C6F74206D61726B206265207368617265643FA76F7074696F6E739282A26964A3796573A474657874A953686172652069742E82A26964A26E6FA474657874AE4B656570206974206C6F63616C2EAE7265636F6D6D656E646174696F6EA26E6FA7646570656E647391D92967616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365A56C65616E73C0A4666C6167C0A97261697365645F696E82A46B696E64A743757453706563A26964D92B63756C746C69622D676170733A6375745F737065633A6375742D736C6F742D746F6C6572616E63652E7234A8646563696465727391D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A6737461747573A46F70656EA672756C696E67C0AA72756C65645F66726F6D90A56F7264657205AC6F7074696F6E735F68617368D94031366432306136646334346236636330306166643337386539663063386235316466653562303765383639653161326161393538386465316465353837666162A67265636F726485AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7135AC6F7074696F6E735F68617368D94031346238666662646236313130366462633036663639663366633865323037326166376132303564373636653463633034306538393733333031336435346463B1636F756E7465645F616E737765725F6964A26E31A7616E737765727391DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7135A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94031366432306136646334346236636330306166643337386539663063386235316466653562303765383639653161326161393538386465316465353837666162A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303032302D303030302D343030302D383030302D303030303030303030303230" + }, + { + "name": "counted_answer_not_derived", + "why": "rule: counted_answer_id is the first live Counted or Revision answer under the card's hash (not expressible in the schema)", + "message": { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q5", + "title": "Fork 5", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 5, + "options_hash": "16d20a6dc44b6cc00afd378e9f0c8b51dfe5b07e869e1a2aa9588de1de587fab" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q5", + "options_hash": "16d20a6dc44b6cc00afd378e9f0c8b51dfe5b07e869e1a2aa9588de1de587fab", + "counted_answer_id": null, + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:q5", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "16d20a6dc44b6cc00afd378e9f0c8b51dfe5b07e869e1a2aa9588de1de587fab", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000020-0000-4000-8000-000000000020" + } + ] + } + }, + "messagePackHex": "83AD736368656D6156657273696F6EB667616D6563756C742E666F726B2E656E7472792E7630A463617264DE0010AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E636172642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7135A57469746C65A6466F726B2035A7636F6E74657874BD53686F756C64206120736C6F74206D61726B206265207368617265643FA76F7074696F6E739282A26964A3796573A474657874A953686172652069742E82A26964A26E6FA474657874AE4B656570206974206C6F63616C2EAE7265636F6D6D656E646174696F6EA26E6FA7646570656E647391D92967616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365A56C65616E73C0A4666C6167C0A97261697365645F696E82A46B696E64A743757453706563A26964D92B63756C746C69622D676170733A6375745F737065633A6375742D736C6F742D746F6C6572616E63652E7234A8646563696465727391D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A6737461747573A46F70656EA672756C696E67C0AA72756C65645F66726F6D90A56F7264657205AC6F7074696F6E735F68617368D94031366432306136646334346236636330306166643337386539663063386235316466653562303765383639653161326161393538386465316465353837666162A67265636F726485AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7135AC6F7074696F6E735F68617368D94031366432306136646334346236636330306166643337386539663063386235316466653562303765383639653161326161393538386465316465353837666162B1636F756E7465645F616E737765725F6964C0A7616E737765727391DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7135A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94031366432306136646334346236636330306166643337386539663063386235316466653562303765383639653161326161393538386465316465353837666162A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303032302D303030302D343030302D383030302D303030303030303030303230" + }, + { + "name": "card_hash_wrong", + "why": "rule: the server computes options_hash and a client recomputes it; a card with another hash is no card", + "message": { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q4", + "title": "Fork 4", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 4, + "options_hash": "14b8ffbdb61106dbc06f69f3fc8e2072af7a205d766e4cc040e89733013d54dc" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q4", + "options_hash": "14b8ffbdb61106dbc06f69f3fc8e2072af7a205d766e4cc040e89733013d54dc", + "counted_answer_id": null, + "answers": [] + } + }, + "messagePackHex": "83AD736368656D6156657273696F6EB667616D6563756C742E666F726B2E656E7472792E7630A463617264DE0010AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E636172642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7134A57469746C65A6466F726B2034A7636F6E74657874BD53686F756C64206120736C6F74206D61726B206265207368617265643FA76F7074696F6E739282A26964A3796573A474657874A953686172652069742E82A26964A26E6FA474657874AE4B656570206974206C6F63616C2EAE7265636F6D6D656E646174696F6EA26E6FA7646570656E647391D92967616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365A56C65616E73C0A4666C6167C0A97261697365645F696E82A46B696E64A743757453706563A26964D92B63756C746C69622D676170733A6375745F737065633A6375742D736C6F742D746F6C6572616E63652E7234A8646563696465727391D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A6737461747573A46F70656EA672756C696E67C0AA72756C65645F66726F6D90A56F7264657204AC6F7074696F6E735F68617368D94031346238666662646236313130366462633036663639663366633865323037326166376132303564373636653463633034306538393733333031336435346463A67265636F726485AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7134AC6F7074696F6E735F68617368D94031346238666662646236313130366462633036663639663366633865323037326166376132303564373636653463633034306538393733333031336435346463B1636F756E7465645F616E737765725F6964C0A7616E737765727390" + } + ] + }, + "listInvalid": [ + { + "name": "next_cursor_malformed", + "why": "a cursor is c", + "message": { + "schemaVersion": "gamecult.fork.list.v0", + "mind": "gamecult-chat", + "stamp": "s412", + "open_count": 3, + "recorded_count": 2, + "forks": [ + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q5", + "title": "Fork 5", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 5, + "options_hash": "16d20a6dc44b6cc00afd378e9f0c8b51dfe5b07e869e1a2aa9588de1de587fab" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q5", + "options_hash": "16d20a6dc44b6cc00afd378e9f0c8b51dfe5b07e869e1a2aa9588de1de587fab", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:q5", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "16d20a6dc44b6cc00afd378e9f0c8b51dfe5b07e869e1a2aa9588de1de587fab", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000020-0000-4000-8000-000000000020" + } + ] + } + }, + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q4", + "title": "Fork 4", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 4, + "options_hash": "a64604237f906b6fec9cb292863685d966abf8048f276afa24cd4f1e1c4c74aa" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q4", + "options_hash": "a64604237f906b6fec9cb292863685d966abf8048f276afa24cd4f1e1c4c74aa", + "counted_answer_id": null, + "answers": [] + } + } + ], + "next_cursor": "more" + }, + "messagePackHex": "87AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E6C6973742E7630A46D696E64AD67616D6563756C742D63686174A57374616D70A473343132AA6F70656E5F636F756E7403AE7265636F726465645F636F756E7402A5666F726B739283AD736368656D6156657273696F6EB667616D6563756C742E666F726B2E656E7472792E7630A463617264DE0010AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E636172642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7135A57469746C65A6466F726B2035A7636F6E74657874BD53686F756C64206120736C6F74206D61726B206265207368617265643FA76F7074696F6E739282A26964A3796573A474657874A953686172652069742E82A26964A26E6FA474657874AE4B656570206974206C6F63616C2EAE7265636F6D6D656E646174696F6EA26E6FA7646570656E647391D92967616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365A56C65616E73C0A4666C6167C0A97261697365645F696E82A46B696E64A743757453706563A26964D92B63756C746C69622D676170733A6375745F737065633A6375742D736C6F742D746F6C6572616E63652E7234A8646563696465727391D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A6737461747573A46F70656EA672756C696E67C0AA72756C65645F66726F6D90A56F7264657205AC6F7074696F6E735F68617368D94031366432306136646334346236636330306166643337386539663063386235316466653562303765383639653161326161393538386465316465353837666162A67265636F726485AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7135AC6F7074696F6E735F68617368D94031366432306136646334346236636330306166643337386539663063386235316466653562303765383639653161326161393538386465316465353837666162B1636F756E7465645F616E737765725F6964A26E31A7616E737765727391DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7135A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94031366432306136646334346236636330306166643337386539663063386235316466653562303765383639653161326161393538386465316465353837666162A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303032302D303030302D343030302D383030302D30303030303030303030323083AD736368656D6156657273696F6EB667616D6563756C742E666F726B2E656E7472792E7630A463617264DE0010AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E636172642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7134A57469746C65A6466F726B2034A7636F6E74657874BD53686F756C64206120736C6F74206D61726B206265207368617265643FA76F7074696F6E739282A26964A3796573A474657874A953686172652069742E82A26964A26E6FA474657874AE4B656570206974206C6F63616C2EAE7265636F6D6D656E646174696F6EA26E6FA7646570656E647391D92967616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365A56C65616E73C0A4666C6167C0A97261697365645F696E82A46B696E64A743757453706563A26964D92B63756C746C69622D676170733A6375745F737065633A6375742D736C6F742D746F6C6572616E63652E7234A8646563696465727391D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A6737461747573A46F70656EA672756C696E67C0AA72756C65645F66726F6D90A56F7264657204AC6F7074696F6E735F68617368D94061363436303432333766393036623666656339636232393238363336383564393636616266383034386632373661666132346364346631653163346337346161A67265636F726485AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7134AC6F7074696F6E735F68617368D94061363436303432333766393036623666656339636232393238363336383564393636616266383034386632373661666132346364346631653163346337346161B1636F756E7465645F616E737765725F6964C0A7616E737765727390AB6E6578745F637572736F72A46D6F7265" + }, + { + "name": "stamp_malformed", + "why": "a stamp is s", + "message": { + "schemaVersion": "gamecult.fork.list.v0", + "mind": "gamecult-chat", + "stamp": "412", + "open_count": 3, + "recorded_count": 2, + "forks": [ + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q5", + "title": "Fork 5", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 5, + "options_hash": "16d20a6dc44b6cc00afd378e9f0c8b51dfe5b07e869e1a2aa9588de1de587fab" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q5", + "options_hash": "16d20a6dc44b6cc00afd378e9f0c8b51dfe5b07e869e1a2aa9588de1de587fab", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:q5", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "16d20a6dc44b6cc00afd378e9f0c8b51dfe5b07e869e1a2aa9588de1de587fab", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000020-0000-4000-8000-000000000020" + } + ] + } + }, + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q4", + "title": "Fork 4", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 4, + "options_hash": "a64604237f906b6fec9cb292863685d966abf8048f276afa24cd4f1e1c4c74aa" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q4", + "options_hash": "a64604237f906b6fec9cb292863685d966abf8048f276afa24cd4f1e1c4c74aa", + "counted_answer_id": null, + "answers": [] + } + } + ], + "next_cursor": "c4" + }, + "messagePackHex": "87AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E6C6973742E7630A46D696E64AD67616D6563756C742D63686174A57374616D70A3343132AA6F70656E5F636F756E7403AE7265636F726465645F636F756E7402A5666F726B739283AD736368656D6156657273696F6EB667616D6563756C742E666F726B2E656E7472792E7630A463617264DE0010AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E636172642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7135A57469746C65A6466F726B2035A7636F6E74657874BD53686F756C64206120736C6F74206D61726B206265207368617265643FA76F7074696F6E739282A26964A3796573A474657874A953686172652069742E82A26964A26E6FA474657874AE4B656570206974206C6F63616C2EAE7265636F6D6D656E646174696F6EA26E6FA7646570656E647391D92967616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365A56C65616E73C0A4666C6167C0A97261697365645F696E82A46B696E64A743757453706563A26964D92B63756C746C69622D676170733A6375745F737065633A6375742D736C6F742D746F6C6572616E63652E7234A8646563696465727391D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A6737461747573A46F70656EA672756C696E67C0AA72756C65645F66726F6D90A56F7264657205AC6F7074696F6E735F68617368D94031366432306136646334346236636330306166643337386539663063386235316466653562303765383639653161326161393538386465316465353837666162A67265636F726485AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7135AC6F7074696F6E735F68617368D94031366432306136646334346236636330306166643337386539663063386235316466653562303765383639653161326161393538386465316465353837666162B1636F756E7465645F616E737765725F6964A26E31A7616E737765727391DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7135A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94031366432306136646334346236636330306166643337386539663063386235316466653562303765383639653161326161393538386465316465353837666162A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303032302D303030302D343030302D383030302D30303030303030303030323083AD736368656D6156657273696F6EB667616D6563756C742E666F726B2E656E7472792E7630A463617264DE0010AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E636172642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7134A57469746C65A6466F726B2034A7636F6E74657874BD53686F756C64206120736C6F74206D61726B206265207368617265643FA76F7074696F6E739282A26964A3796573A474657874A953686172652069742E82A26964A26E6FA474657874AE4B656570206974206C6F63616C2EAE7265636F6D6D656E646174696F6EA26E6FA7646570656E647391D92967616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365A56C65616E73C0A4666C6167C0A97261697365645F696E82A46B696E64A743757453706563A26964D92B63756C746C69622D676170733A6375745F737065633A6375742D736C6F742D746F6C6572616E63652E7234A8646563696465727391D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A6737461747573A46F70656EA672756C696E67C0AA72756C65645F66726F6D90A56F7264657204AC6F7074696F6E735F68617368D94061363436303432333766393036623666656339636232393238363336383564393636616266383034386632373661666132346364346631653163346337346161A67265636F726485AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7134AC6F7074696F6E735F68617368D94061363436303432333766393036623666656339636232393238363336383564393636616266383034386632373661666132346364346631653163346337346161B1636F756E7465645F616E737765725F6964C0A7616E737765727390AB6E6578745F637572736F72A26334" + }, + { + "name": "count_negative", + "why": "counts are not negative", + "message": { + "schemaVersion": "gamecult.fork.list.v0", + "mind": "gamecult-chat", + "stamp": "s412", + "open_count": -1, + "recorded_count": 2, + "forks": [ + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q5", + "title": "Fork 5", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 5, + "options_hash": "16d20a6dc44b6cc00afd378e9f0c8b51dfe5b07e869e1a2aa9588de1de587fab" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q5", + "options_hash": "16d20a6dc44b6cc00afd378e9f0c8b51dfe5b07e869e1a2aa9588de1de587fab", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:q5", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "16d20a6dc44b6cc00afd378e9f0c8b51dfe5b07e869e1a2aa9588de1de587fab", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000020-0000-4000-8000-000000000020" + } + ] + } + }, + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q4", + "title": "Fork 4", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 4, + "options_hash": "a64604237f906b6fec9cb292863685d966abf8048f276afa24cd4f1e1c4c74aa" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q4", + "options_hash": "a64604237f906b6fec9cb292863685d966abf8048f276afa24cd4f1e1c4c74aa", + "counted_answer_id": null, + "answers": [] + } + } + ], + "next_cursor": "c4" + }, + "messagePackHex": "87AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E6C6973742E7630A46D696E64AD67616D6563756C742D63686174A57374616D70A473343132AA6F70656E5F636F756E74FFAE7265636F726465645F636F756E7402A5666F726B739283AD736368656D6156657273696F6EB667616D6563756C742E666F726B2E656E7472792E7630A463617264DE0010AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E636172642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7135A57469746C65A6466F726B2035A7636F6E74657874BD53686F756C64206120736C6F74206D61726B206265207368617265643FA76F7074696F6E739282A26964A3796573A474657874A953686172652069742E82A26964A26E6FA474657874AE4B656570206974206C6F63616C2EAE7265636F6D6D656E646174696F6EA26E6FA7646570656E647391D92967616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365A56C65616E73C0A4666C6167C0A97261697365645F696E82A46B696E64A743757453706563A26964D92B63756C746C69622D676170733A6375745F737065633A6375742D736C6F742D746F6C6572616E63652E7234A8646563696465727391D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A6737461747573A46F70656EA672756C696E67C0AA72756C65645F66726F6D90A56F7264657205AC6F7074696F6E735F68617368D94031366432306136646334346236636330306166643337386539663063386235316466653562303765383639653161326161393538386465316465353837666162A67265636F726485AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7135AC6F7074696F6E735F68617368D94031366432306136646334346236636330306166643337386539663063386235316466653562303765383639653161326161393538386465316465353837666162B1636F756E7465645F616E737765725F6964A26E31A7616E737765727391DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7135A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94031366432306136646334346236636330306166643337386539663063386235316466653562303765383639653161326161393538386465316465353837666162A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303032302D303030302D343030302D383030302D30303030303030303030323083AD736368656D6156657273696F6EB667616D6563756C742E666F726B2E656E7472792E7630A463617264DE0010AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E636172642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7134A57469746C65A6466F726B2034A7636F6E74657874BD53686F756C64206120736C6F74206D61726B206265207368617265643FA76F7074696F6E739282A26964A3796573A474657874A953686172652069742E82A26964A26E6FA474657874AE4B656570206974206C6F63616C2EAE7265636F6D6D656E646174696F6EA26E6FA7646570656E647391D92967616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365A56C65616E73C0A4666C6167C0A97261697365645F696E82A46B696E64A743757453706563A26964D92B63756C746C69622D676170733A6375745F737065633A6375742D736C6F742D746F6C6572616E63652E7234A8646563696465727391D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A6737461747573A46F70656EA672756C696E67C0AA72756C65645F66726F6D90A56F7264657204AC6F7074696F6E735F68617368D94061363436303432333766393036623666656339636232393238363336383564393636616266383034386632373661666132346364346631653163346337346161A67265636F726485AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7134AC6F7074696F6E735F68617368D94061363436303432333766393036623666656339636232393238363336383564393636616266383034386632373661666132346364346631653163346337346161B1636F756E7465645F616E737765725F6964C0A7616E737765727390AB6E6578745F637572736F72A26334" + } + ], + "listScenarios": [ + { + "name": "newest_first_paged_open_only", + "ordinal": 412, + "entries": [ + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q5", + "title": "Fork 5", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 5, + "options_hash": "16d20a6dc44b6cc00afd378e9f0c8b51dfe5b07e869e1a2aa9588de1de587fab" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q5", + "options_hash": "16d20a6dc44b6cc00afd378e9f0c8b51dfe5b07e869e1a2aa9588de1de587fab", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:q5", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "16d20a6dc44b6cc00afd378e9f0c8b51dfe5b07e869e1a2aa9588de1de587fab", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000020-0000-4000-8000-000000000020" + } + ] + } + }, + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q4", + "title": "Fork 4", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 4, + "options_hash": "a64604237f906b6fec9cb292863685d966abf8048f276afa24cd4f1e1c4c74aa" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q4", + "options_hash": "a64604237f906b6fec9cb292863685d966abf8048f276afa24cd4f1e1c4c74aa", + "counted_answer_id": null, + "answers": [] + } + }, + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q3", + "title": "Fork 3", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "closed", + "ruling": "cultlib-gaps:resolution:question.q3.n1", + "ruled_from": [ + "n1" + ], + "order": 3, + "options_hash": "ced892eb663c22e3d7ece602708043ee545f56b9d4e4421d7f5bfaa86c418ce2" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q3", + "options_hash": "ced892eb663c22e3d7ece602708043ee545f56b9d4e4421d7f5bfaa86c418ce2", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:q3", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "ced892eb663c22e3d7ece602708043ee545f56b9d4e4421d7f5bfaa86c418ce2", + "choice": "yes", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000021-0000-4000-8000-000000000021" + }, + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n2", + "fork_id": "gamecult-chat:question:q3", + "outcome": "Refused", + "of": null, + "retracts": null, + "code": "fork_closed", + "options_hash": "ced892eb663c22e3d7ece602708043ee545f56b9d4e4421d7f5bfaa86c418ce2", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:04:05Z", + "claim_in_future": false, + "answerer": "9f8e7d6c-5b4a-4392-8170-0f1e2d3c4b5a", + "authority": "Steward", + "request_id": "00000022-0000-4000-8000-000000000022" + } + ] + } + }, + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q2", + "title": "Fork 2", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 2, + "options_hash": "5c7c2945e8e8689c6c928b11d7e0a033640b0c66afb7fcffb9e740c019d63e79" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q2", + "options_hash": "5c7c2945e8e8689c6c928b11d7e0a033640b0c66afb7fcffb9e740c019d63e79", + "counted_answer_id": null, + "answers": [] + } + }, + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q1", + "title": "Fork 1", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "closed", + "ruling": "cultlib-gaps:resolution:question.q1.n1", + "ruled_from": [], + "order": 1, + "options_hash": "61959964bd214ea1b961554c43ec46a9faf156dd547cc50c8c16bbdcd55f223a" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q1", + "options_hash": "61959964bd214ea1b961554c43ec46a9faf156dd547cc50c8c16bbdcd55f223a", + "counted_answer_id": null, + "answers": [] + } + } + ], + "steps": [ + { + "request": { + "schemaVersion": "gamecult.fork.list_request.v0", + "mind": "gamecult-chat", + "status": "open", + "cursor": null, + "limit": 2 + }, + "response": { + "schemaVersion": "gamecult.fork.list.v0", + "mind": "gamecult-chat", + "stamp": "s412", + "open_count": 3, + "recorded_count": 2, + "forks": [ + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q5", + "title": "Fork 5", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 5, + "options_hash": "16d20a6dc44b6cc00afd378e9f0c8b51dfe5b07e869e1a2aa9588de1de587fab" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q5", + "options_hash": "16d20a6dc44b6cc00afd378e9f0c8b51dfe5b07e869e1a2aa9588de1de587fab", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:q5", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "16d20a6dc44b6cc00afd378e9f0c8b51dfe5b07e869e1a2aa9588de1de587fab", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000020-0000-4000-8000-000000000020" + } + ] + } + }, + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q4", + "title": "Fork 4", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 4, + "options_hash": "a64604237f906b6fec9cb292863685d966abf8048f276afa24cd4f1e1c4c74aa" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q4", + "options_hash": "a64604237f906b6fec9cb292863685d966abf8048f276afa24cd4f1e1c4c74aa", + "counted_answer_id": null, + "answers": [] + } + } + ], + "next_cursor": "c4" + }, + "responseHex": "87AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E6C6973742E7630A46D696E64AD67616D6563756C742D63686174A57374616D70A473343132AA6F70656E5F636F756E7403AE7265636F726465645F636F756E7402A5666F726B739283AD736368656D6156657273696F6EB667616D6563756C742E666F726B2E656E7472792E7630A463617264DE0010AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E636172642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7135A57469746C65A6466F726B2035A7636F6E74657874BD53686F756C64206120736C6F74206D61726B206265207368617265643FA76F7074696F6E739282A26964A3796573A474657874A953686172652069742E82A26964A26E6FA474657874AE4B656570206974206C6F63616C2EAE7265636F6D6D656E646174696F6EA26E6FA7646570656E647391D92967616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365A56C65616E73C0A4666C6167C0A97261697365645F696E82A46B696E64A743757453706563A26964D92B63756C746C69622D676170733A6375745F737065633A6375742D736C6F742D746F6C6572616E63652E7234A8646563696465727391D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A6737461747573A46F70656EA672756C696E67C0AA72756C65645F66726F6D90A56F7264657205AC6F7074696F6E735F68617368D94031366432306136646334346236636330306166643337386539663063386235316466653562303765383639653161326161393538386465316465353837666162A67265636F726485AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7135AC6F7074696F6E735F68617368D94031366432306136646334346236636330306166643337386539663063386235316466653562303765383639653161326161393538386465316465353837666162B1636F756E7465645F616E737765725F6964A26E31A7616E737765727391DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7135A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94031366432306136646334346236636330306166643337386539663063386235316466653562303765383639653161326161393538386465316465353837666162A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303032302D303030302D343030302D383030302D30303030303030303030323083AD736368656D6156657273696F6EB667616D6563756C742E666F726B2E656E7472792E7630A463617264DE0010AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E636172642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7134A57469746C65A6466F726B2034A7636F6E74657874BD53686F756C64206120736C6F74206D61726B206265207368617265643FA76F7074696F6E739282A26964A3796573A474657874A953686172652069742E82A26964A26E6FA474657874AE4B656570206974206C6F63616C2EAE7265636F6D6D656E646174696F6EA26E6FA7646570656E647391D92967616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365A56C65616E73C0A4666C6167C0A97261697365645F696E82A46B696E64A743757453706563A26964D92B63756C746C69622D676170733A6375745F737065633A6375742D736C6F742D746F6C6572616E63652E7234A8646563696465727391D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A6737461747573A46F70656EA672756C696E67C0AA72756C65645F66726F6D90A56F7264657204AC6F7074696F6E735F68617368D94061363436303432333766393036623666656339636232393238363336383564393636616266383034386632373661666132346364346631653163346337346161A67265636F726485AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7134AC6F7074696F6E735F68617368D94061363436303432333766393036623666656339636232393238363336383564393636616266383034386632373661666132346364346631653163346337346161B1636F756E7465645F616E737765725F6964C0A7616E737765727390AB6E6578745F637572736F72A26334" + }, + { + "request": { + "schemaVersion": "gamecult.fork.list_request.v0", + "mind": "gamecult-chat", + "status": "open", + "cursor": "c4", + "limit": 2 + }, + "response": { + "schemaVersion": "gamecult.fork.list.v0", + "mind": "gamecult-chat", + "stamp": "s412", + "open_count": 3, + "recorded_count": 2, + "forks": [ + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q2", + "title": "Fork 2", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 2, + "options_hash": "5c7c2945e8e8689c6c928b11d7e0a033640b0c66afb7fcffb9e740c019d63e79" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q2", + "options_hash": "5c7c2945e8e8689c6c928b11d7e0a033640b0c66afb7fcffb9e740c019d63e79", + "counted_answer_id": null, + "answers": [] + } + } + ], + "next_cursor": null + }, + "responseHex": "87AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E6C6973742E7630A46D696E64AD67616D6563756C742D63686174A57374616D70A473343132AA6F70656E5F636F756E7403AE7265636F726465645F636F756E7402A5666F726B739183AD736368656D6156657273696F6EB667616D6563756C742E666F726B2E656E7472792E7630A463617264DE0010AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E636172642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7132A57469746C65A6466F726B2032A7636F6E74657874BD53686F756C64206120736C6F74206D61726B206265207368617265643FA76F7074696F6E739282A26964A3796573A474657874A953686172652069742E82A26964A26E6FA474657874AE4B656570206974206C6F63616C2EAE7265636F6D6D656E646174696F6EA26E6FA7646570656E647391D92967616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365A56C65616E73C0A4666C6167C0A97261697365645F696E82A46B696E64A743757453706563A26964D92B63756C746C69622D676170733A6375745F737065633A6375742D736C6F742D746F6C6572616E63652E7234A8646563696465727391D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A6737461747573A46F70656EA672756C696E67C0AA72756C65645F66726F6D90A56F7264657202AC6F7074696F6E735F68617368D94035633763323934356538653836383963366339323862313164376530613033333634306230633636616662376663666662396537343063303139643633653739A67265636F726485AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7132AC6F7074696F6E735F68617368D94035633763323934356538653836383963366339323862313164376530613033333634306230633636616662376663666662396537343063303139643633653739B1636F756E7465645F616E737765725F6964C0A7616E737765727390AB6E6578745F637572736F72C0" + } + ] + }, + { + "name": "all_forks_one_page", + "ordinal": 412, + "entries": [ + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q5", + "title": "Fork 5", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 5, + "options_hash": "16d20a6dc44b6cc00afd378e9f0c8b51dfe5b07e869e1a2aa9588de1de587fab" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q5", + "options_hash": "16d20a6dc44b6cc00afd378e9f0c8b51dfe5b07e869e1a2aa9588de1de587fab", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:q5", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "16d20a6dc44b6cc00afd378e9f0c8b51dfe5b07e869e1a2aa9588de1de587fab", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000020-0000-4000-8000-000000000020" + } + ] + } + }, + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q4", + "title": "Fork 4", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 4, + "options_hash": "a64604237f906b6fec9cb292863685d966abf8048f276afa24cd4f1e1c4c74aa" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q4", + "options_hash": "a64604237f906b6fec9cb292863685d966abf8048f276afa24cd4f1e1c4c74aa", + "counted_answer_id": null, + "answers": [] + } + }, + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q3", + "title": "Fork 3", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "closed", + "ruling": "cultlib-gaps:resolution:question.q3.n1", + "ruled_from": [ + "n1" + ], + "order": 3, + "options_hash": "ced892eb663c22e3d7ece602708043ee545f56b9d4e4421d7f5bfaa86c418ce2" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q3", + "options_hash": "ced892eb663c22e3d7ece602708043ee545f56b9d4e4421d7f5bfaa86c418ce2", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:q3", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "ced892eb663c22e3d7ece602708043ee545f56b9d4e4421d7f5bfaa86c418ce2", + "choice": "yes", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000021-0000-4000-8000-000000000021" + }, + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n2", + "fork_id": "gamecult-chat:question:q3", + "outcome": "Refused", + "of": null, + "retracts": null, + "code": "fork_closed", + "options_hash": "ced892eb663c22e3d7ece602708043ee545f56b9d4e4421d7f5bfaa86c418ce2", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:04:05Z", + "claim_in_future": false, + "answerer": "9f8e7d6c-5b4a-4392-8170-0f1e2d3c4b5a", + "authority": "Steward", + "request_id": "00000022-0000-4000-8000-000000000022" + } + ] + } + }, + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q2", + "title": "Fork 2", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 2, + "options_hash": "5c7c2945e8e8689c6c928b11d7e0a033640b0c66afb7fcffb9e740c019d63e79" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q2", + "options_hash": "5c7c2945e8e8689c6c928b11d7e0a033640b0c66afb7fcffb9e740c019d63e79", + "counted_answer_id": null, + "answers": [] + } + }, + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q1", + "title": "Fork 1", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "closed", + "ruling": "cultlib-gaps:resolution:question.q1.n1", + "ruled_from": [], + "order": 1, + "options_hash": "61959964bd214ea1b961554c43ec46a9faf156dd547cc50c8c16bbdcd55f223a" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q1", + "options_hash": "61959964bd214ea1b961554c43ec46a9faf156dd547cc50c8c16bbdcd55f223a", + "counted_answer_id": null, + "answers": [] + } + } + ], + "steps": [ + { + "request": { + "schemaVersion": "gamecult.fork.list_request.v0", + "mind": "gamecult-chat", + "status": null, + "cursor": null, + "limit": 50 + }, + "response": { + "schemaVersion": "gamecult.fork.list.v0", + "mind": "gamecult-chat", + "stamp": "s412", + "open_count": 3, + "recorded_count": 2, + "forks": [ + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q5", + "title": "Fork 5", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 5, + "options_hash": "16d20a6dc44b6cc00afd378e9f0c8b51dfe5b07e869e1a2aa9588de1de587fab" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q5", + "options_hash": "16d20a6dc44b6cc00afd378e9f0c8b51dfe5b07e869e1a2aa9588de1de587fab", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:q5", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "16d20a6dc44b6cc00afd378e9f0c8b51dfe5b07e869e1a2aa9588de1de587fab", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000020-0000-4000-8000-000000000020" + } + ] + } + }, + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q4", + "title": "Fork 4", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 4, + "options_hash": "a64604237f906b6fec9cb292863685d966abf8048f276afa24cd4f1e1c4c74aa" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q4", + "options_hash": "a64604237f906b6fec9cb292863685d966abf8048f276afa24cd4f1e1c4c74aa", + "counted_answer_id": null, + "answers": [] + } + }, + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q3", + "title": "Fork 3", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "closed", + "ruling": "cultlib-gaps:resolution:question.q3.n1", + "ruled_from": [ + "n1" + ], + "order": 3, + "options_hash": "ced892eb663c22e3d7ece602708043ee545f56b9d4e4421d7f5bfaa86c418ce2" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q3", + "options_hash": "ced892eb663c22e3d7ece602708043ee545f56b9d4e4421d7f5bfaa86c418ce2", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:q3", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "ced892eb663c22e3d7ece602708043ee545f56b9d4e4421d7f5bfaa86c418ce2", + "choice": "yes", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000021-0000-4000-8000-000000000021" + }, + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n2", + "fork_id": "gamecult-chat:question:q3", + "outcome": "Refused", + "of": null, + "retracts": null, + "code": "fork_closed", + "options_hash": "ced892eb663c22e3d7ece602708043ee545f56b9d4e4421d7f5bfaa86c418ce2", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:04:05Z", + "claim_in_future": false, + "answerer": "9f8e7d6c-5b4a-4392-8170-0f1e2d3c4b5a", + "authority": "Steward", + "request_id": "00000022-0000-4000-8000-000000000022" + } + ] + } + }, + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q2", + "title": "Fork 2", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 2, + "options_hash": "5c7c2945e8e8689c6c928b11d7e0a033640b0c66afb7fcffb9e740c019d63e79" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q2", + "options_hash": "5c7c2945e8e8689c6c928b11d7e0a033640b0c66afb7fcffb9e740c019d63e79", + "counted_answer_id": null, + "answers": [] + } + }, + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q1", + "title": "Fork 1", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "closed", + "ruling": "cultlib-gaps:resolution:question.q1.n1", + "ruled_from": [], + "order": 1, + "options_hash": "61959964bd214ea1b961554c43ec46a9faf156dd547cc50c8c16bbdcd55f223a" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q1", + "options_hash": "61959964bd214ea1b961554c43ec46a9faf156dd547cc50c8c16bbdcd55f223a", + "counted_answer_id": null, + "answers": [] + } + } + ], + "next_cursor": null + }, + "responseHex": "87AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E6C6973742E7630A46D696E64AD67616D6563756C742D63686174A57374616D70A473343132AA6F70656E5F636F756E7403AE7265636F726465645F636F756E7402A5666F726B739583AD736368656D6156657273696F6EB667616D6563756C742E666F726B2E656E7472792E7630A463617264DE0010AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E636172642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7135A57469746C65A6466F726B2035A7636F6E74657874BD53686F756C64206120736C6F74206D61726B206265207368617265643FA76F7074696F6E739282A26964A3796573A474657874A953686172652069742E82A26964A26E6FA474657874AE4B656570206974206C6F63616C2EAE7265636F6D6D656E646174696F6EA26E6FA7646570656E647391D92967616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365A56C65616E73C0A4666C6167C0A97261697365645F696E82A46B696E64A743757453706563A26964D92B63756C746C69622D676170733A6375745F737065633A6375742D736C6F742D746F6C6572616E63652E7234A8646563696465727391D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A6737461747573A46F70656EA672756C696E67C0AA72756C65645F66726F6D90A56F7264657205AC6F7074696F6E735F68617368D94031366432306136646334346236636330306166643337386539663063386235316466653562303765383639653161326161393538386465316465353837666162A67265636F726485AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7135AC6F7074696F6E735F68617368D94031366432306136646334346236636330306166643337386539663063386235316466653562303765383639653161326161393538386465316465353837666162B1636F756E7465645F616E737765725F6964A26E31A7616E737765727391DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7135A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94031366432306136646334346236636330306166643337386539663063386235316466653562303765383639653161326161393538386465316465353837666162A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303032302D303030302D343030302D383030302D30303030303030303030323083AD736368656D6156657273696F6EB667616D6563756C742E666F726B2E656E7472792E7630A463617264DE0010AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E636172642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7134A57469746C65A6466F726B2034A7636F6E74657874BD53686F756C64206120736C6F74206D61726B206265207368617265643FA76F7074696F6E739282A26964A3796573A474657874A953686172652069742E82A26964A26E6FA474657874AE4B656570206974206C6F63616C2EAE7265636F6D6D656E646174696F6EA26E6FA7646570656E647391D92967616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365A56C65616E73C0A4666C6167C0A97261697365645F696E82A46B696E64A743757453706563A26964D92B63756C746C69622D676170733A6375745F737065633A6375742D736C6F742D746F6C6572616E63652E7234A8646563696465727391D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A6737461747573A46F70656EA672756C696E67C0AA72756C65645F66726F6D90A56F7264657204AC6F7074696F6E735F68617368D94061363436303432333766393036623666656339636232393238363336383564393636616266383034386632373661666132346364346631653163346337346161A67265636F726485AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7134AC6F7074696F6E735F68617368D94061363436303432333766393036623666656339636232393238363336383564393636616266383034386632373661666132346364346631653163346337346161B1636F756E7465645F616E737765725F6964C0A7616E73776572739083AD736368656D6156657273696F6EB667616D6563756C742E666F726B2E656E7472792E7630A463617264DE0010AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E636172642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7133A57469746C65A6466F726B2033A7636F6E74657874BD53686F756C64206120736C6F74206D61726B206265207368617265643FA76F7074696F6E739282A26964A3796573A474657874A953686172652069742E82A26964A26E6FA474657874AE4B656570206974206C6F63616C2EAE7265636F6D6D656E646174696F6EA26E6FA7646570656E647391D92967616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365A56C65616E73C0A4666C6167C0A97261697365645F696E82A46B696E64A743757453706563A26964D92B63756C746C69622D676170733A6375745F737065633A6375742D736C6F742D746F6C6572616E63652E7234A8646563696465727391D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A6737461747573A6636C6F736564A672756C696E67D92663756C746C69622D676170733A7265736F6C7574696F6E3A7175657374696F6E2E71332E6E31AA72756C65645F66726F6D91A26E31A56F7264657203AC6F7074696F6E735F68617368D94063656438393265623636336332326533643765636536303237303830343365653534356635366239643465343432316437663562666161383663343138636532A67265636F726485AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7133AC6F7074696F6E735F68617368D94063656438393265623636336332326533643765636536303237303830343365653534356635366239643465343432316437663562666161383663343138636532B1636F756E7465645F616E737765725F6964A26E31A7616E737765727392DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7133A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94063656438393265623636336332326533643765636536303237303830343365653534356635366239643465343432316437663562666161383663343138636532A663686F696365A3796573A46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303032312D303030302D343030302D383030302D303030303030303030303231DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E32A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7133A76F7574636F6D65A752656675736564A26F66C0A87265747261637473C0A4636F6465AB666F726B5F636C6F736564AC6F7074696F6E735F68617368D94063656438393265623636336332326533643765636536303237303830343365653534356635366239643465343432316437663562666161383663343138636532A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30343A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92439663865376436632D356234612D343339322D383137302D306631653264336334623561A9617574686F72697479A753746577617264AA726571756573745F6964D92430303030303032322D303030302D343030302D383030302D30303030303030303030323283AD736368656D6156657273696F6EB667616D6563756C742E666F726B2E656E7472792E7630A463617264DE0010AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E636172642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7132A57469746C65A6466F726B2032A7636F6E74657874BD53686F756C64206120736C6F74206D61726B206265207368617265643FA76F7074696F6E739282A26964A3796573A474657874A953686172652069742E82A26964A26E6FA474657874AE4B656570206974206C6F63616C2EAE7265636F6D6D656E646174696F6EA26E6FA7646570656E647391D92967616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365A56C65616E73C0A4666C6167C0A97261697365645F696E82A46B696E64A743757453706563A26964D92B63756C746C69622D676170733A6375745F737065633A6375742D736C6F742D746F6C6572616E63652E7234A8646563696465727391D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A6737461747573A46F70656EA672756C696E67C0AA72756C65645F66726F6D90A56F7264657202AC6F7074696F6E735F68617368D94035633763323934356538653836383963366339323862313164376530613033333634306230633636616662376663666662396537343063303139643633653739A67265636F726485AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7132AC6F7074696F6E735F68617368D94035633763323934356538653836383963366339323862313164376530613033333634306230633636616662376663666662396537343063303139643633653739B1636F756E7465645F616E737765725F6964C0A7616E73776572739083AD736368656D6156657273696F6EB667616D6563756C742E666F726B2E656E7472792E7630A463617264DE0010AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E636172642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7131A57469746C65A6466F726B2031A7636F6E74657874BD53686F756C64206120736C6F74206D61726B206265207368617265643FA76F7074696F6E739282A26964A3796573A474657874A953686172652069742E82A26964A26E6FA474657874AE4B656570206974206C6F63616C2EAE7265636F6D6D656E646174696F6EA26E6FA7646570656E647391D92967616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365A56C65616E73C0A4666C6167C0A97261697365645F696E82A46B696E64A743757453706563A26964D92B63756C746C69622D676170733A6375745F737065633A6375742D736C6F742D746F6C6572616E63652E7234A8646563696465727391D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A6737461747573A6636C6F736564A672756C696E67D92663756C746C69622D676170733A7265736F6C7574696F6E3A7175657374696F6E2E71312E6E31AA72756C65645F66726F6D90A56F7264657201AC6F7074696F6E735F68617368D94036313935393936346264323134656131623936313535346334336563343661396661663135366464353437636335306338633136626264636435356632323361A67265636F726485AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7131AC6F7074696F6E735F68617368D94036313935393936346264323134656131623936313535346334336563343661396661663135366464353437636335306338633136626264636435356632323361B1636F756E7465645F616E737765725F6964C0A7616E737765727390AB6E6578745F637572736F72C0" + } + ] + }, + { + "name": "recorded_only_paged", + "ordinal": 412, + "entries": [ + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q5", + "title": "Fork 5", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 5, + "options_hash": "16d20a6dc44b6cc00afd378e9f0c8b51dfe5b07e869e1a2aa9588de1de587fab" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q5", + "options_hash": "16d20a6dc44b6cc00afd378e9f0c8b51dfe5b07e869e1a2aa9588de1de587fab", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:q5", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "16d20a6dc44b6cc00afd378e9f0c8b51dfe5b07e869e1a2aa9588de1de587fab", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000020-0000-4000-8000-000000000020" + } + ] + } + }, + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q4", + "title": "Fork 4", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 4, + "options_hash": "a64604237f906b6fec9cb292863685d966abf8048f276afa24cd4f1e1c4c74aa" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q4", + "options_hash": "a64604237f906b6fec9cb292863685d966abf8048f276afa24cd4f1e1c4c74aa", + "counted_answer_id": null, + "answers": [] + } + }, + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q3", + "title": "Fork 3", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "closed", + "ruling": "cultlib-gaps:resolution:question.q3.n1", + "ruled_from": [ + "n1" + ], + "order": 3, + "options_hash": "ced892eb663c22e3d7ece602708043ee545f56b9d4e4421d7f5bfaa86c418ce2" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q3", + "options_hash": "ced892eb663c22e3d7ece602708043ee545f56b9d4e4421d7f5bfaa86c418ce2", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:q3", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "ced892eb663c22e3d7ece602708043ee545f56b9d4e4421d7f5bfaa86c418ce2", + "choice": "yes", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000021-0000-4000-8000-000000000021" + }, + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n2", + "fork_id": "gamecult-chat:question:q3", + "outcome": "Refused", + "of": null, + "retracts": null, + "code": "fork_closed", + "options_hash": "ced892eb663c22e3d7ece602708043ee545f56b9d4e4421d7f5bfaa86c418ce2", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:04:05Z", + "claim_in_future": false, + "answerer": "9f8e7d6c-5b4a-4392-8170-0f1e2d3c4b5a", + "authority": "Steward", + "request_id": "00000022-0000-4000-8000-000000000022" + } + ] + } + }, + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q2", + "title": "Fork 2", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 2, + "options_hash": "5c7c2945e8e8689c6c928b11d7e0a033640b0c66afb7fcffb9e740c019d63e79" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q2", + "options_hash": "5c7c2945e8e8689c6c928b11d7e0a033640b0c66afb7fcffb9e740c019d63e79", + "counted_answer_id": null, + "answers": [] + } + }, + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q1", + "title": "Fork 1", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "closed", + "ruling": "cultlib-gaps:resolution:question.q1.n1", + "ruled_from": [], + "order": 1, + "options_hash": "61959964bd214ea1b961554c43ec46a9faf156dd547cc50c8c16bbdcd55f223a" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q1", + "options_hash": "61959964bd214ea1b961554c43ec46a9faf156dd547cc50c8c16bbdcd55f223a", + "counted_answer_id": null, + "answers": [] + } + } + ], + "steps": [ + { + "request": { + "schemaVersion": "gamecult.fork.list_request.v0", + "mind": "gamecult-chat", + "status": "recorded", + "cursor": null, + "limit": 1 + }, + "response": { + "schemaVersion": "gamecult.fork.list.v0", + "mind": "gamecult-chat", + "stamp": "s412", + "open_count": 3, + "recorded_count": 2, + "forks": [ + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q3", + "title": "Fork 3", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "closed", + "ruling": "cultlib-gaps:resolution:question.q3.n1", + "ruled_from": [ + "n1" + ], + "order": 3, + "options_hash": "ced892eb663c22e3d7ece602708043ee545f56b9d4e4421d7f5bfaa86c418ce2" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q3", + "options_hash": "ced892eb663c22e3d7ece602708043ee545f56b9d4e4421d7f5bfaa86c418ce2", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:q3", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "ced892eb663c22e3d7ece602708043ee545f56b9d4e4421d7f5bfaa86c418ce2", + "choice": "yes", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000021-0000-4000-8000-000000000021" + }, + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n2", + "fork_id": "gamecult-chat:question:q3", + "outcome": "Refused", + "of": null, + "retracts": null, + "code": "fork_closed", + "options_hash": "ced892eb663c22e3d7ece602708043ee545f56b9d4e4421d7f5bfaa86c418ce2", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:04:05Z", + "claim_in_future": false, + "answerer": "9f8e7d6c-5b4a-4392-8170-0f1e2d3c4b5a", + "authority": "Steward", + "request_id": "00000022-0000-4000-8000-000000000022" + } + ] + } + } + ], + "next_cursor": "c3" + }, + "responseHex": "87AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E6C6973742E7630A46D696E64AD67616D6563756C742D63686174A57374616D70A473343132AA6F70656E5F636F756E7403AE7265636F726465645F636F756E7402A5666F726B739183AD736368656D6156657273696F6EB667616D6563756C742E666F726B2E656E7472792E7630A463617264DE0010AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E636172642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7133A57469746C65A6466F726B2033A7636F6E74657874BD53686F756C64206120736C6F74206D61726B206265207368617265643FA76F7074696F6E739282A26964A3796573A474657874A953686172652069742E82A26964A26E6FA474657874AE4B656570206974206C6F63616C2EAE7265636F6D6D656E646174696F6EA26E6FA7646570656E647391D92967616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365A56C65616E73C0A4666C6167C0A97261697365645F696E82A46B696E64A743757453706563A26964D92B63756C746C69622D676170733A6375745F737065633A6375742D736C6F742D746F6C6572616E63652E7234A8646563696465727391D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A6737461747573A6636C6F736564A672756C696E67D92663756C746C69622D676170733A7265736F6C7574696F6E3A7175657374696F6E2E71332E6E31AA72756C65645F66726F6D91A26E31A56F7264657203AC6F7074696F6E735F68617368D94063656438393265623636336332326533643765636536303237303830343365653534356635366239643465343432316437663562666161383663343138636532A67265636F726485AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7133AC6F7074696F6E735F68617368D94063656438393265623636336332326533643765636536303237303830343365653534356635366239643465343432316437663562666161383663343138636532B1636F756E7465645F616E737765725F6964A26E31A7616E737765727392DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7133A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94063656438393265623636336332326533643765636536303237303830343365653534356635366239643465343432316437663562666161383663343138636532A663686F696365A3796573A46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303032312D303030302D343030302D383030302D303030303030303030303231DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E32A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7133A76F7574636F6D65A752656675736564A26F66C0A87265747261637473C0A4636F6465AB666F726B5F636C6F736564AC6F7074696F6E735F68617368D94063656438393265623636336332326533643765636536303237303830343365653534356635366239643465343432316437663562666161383663343138636532A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30343A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92439663865376436632D356234612D343339322D383137302D306631653264336334623561A9617574686F72697479A753746577617264AA726571756573745F6964D92430303030303032322D303030302D343030302D383030302D303030303030303030303232AB6E6578745F637572736F72A26333" + }, + { + "request": { + "schemaVersion": "gamecult.fork.list_request.v0", + "mind": "gamecult-chat", + "status": "recorded", + "cursor": "c3", + "limit": 1 + }, + "response": { + "schemaVersion": "gamecult.fork.list.v0", + "mind": "gamecult-chat", + "stamp": "s412", + "open_count": 3, + "recorded_count": 2, + "forks": [ + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q1", + "title": "Fork 1", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "closed", + "ruling": "cultlib-gaps:resolution:question.q1.n1", + "ruled_from": [], + "order": 1, + "options_hash": "61959964bd214ea1b961554c43ec46a9faf156dd547cc50c8c16bbdcd55f223a" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q1", + "options_hash": "61959964bd214ea1b961554c43ec46a9faf156dd547cc50c8c16bbdcd55f223a", + "counted_answer_id": null, + "answers": [] + } + } + ], + "next_cursor": null + }, + "responseHex": "87AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E6C6973742E7630A46D696E64AD67616D6563756C742D63686174A57374616D70A473343132AA6F70656E5F636F756E7403AE7265636F726465645F636F756E7402A5666F726B739183AD736368656D6156657273696F6EB667616D6563756C742E666F726B2E656E7472792E7630A463617264DE0010AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E636172642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7131A57469746C65A6466F726B2031A7636F6E74657874BD53686F756C64206120736C6F74206D61726B206265207368617265643FA76F7074696F6E739282A26964A3796573A474657874A953686172652069742E82A26964A26E6FA474657874AE4B656570206974206C6F63616C2EAE7265636F6D6D656E646174696F6EA26E6FA7646570656E647391D92967616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365A56C65616E73C0A4666C6167C0A97261697365645F696E82A46B696E64A743757453706563A26964D92B63756C746C69622D676170733A6375745F737065633A6375742D736C6F742D746F6C6572616E63652E7234A8646563696465727391D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A6737461747573A6636C6F736564A672756C696E67D92663756C746C69622D676170733A7265736F6C7574696F6E3A7175657374696F6E2E71312E6E31AA72756C65645F66726F6D90A56F7264657201AC6F7074696F6E735F68617368D94036313935393936346264323134656131623936313535346334336563343661396661663135366464353437636335306338633136626264636435356632323361A67265636F726485AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7131AC6F7074696F6E735F68617368D94036313935393936346264323134656131623936313535346334336563343661396661663135366464353437636335306338633136626264636435356632323361B1636F756E7465645F616E737765725F6964C0A7616E737765727390AB6E6578745F637572736F72C0" + } + ] + }, + { + "name": "limit_zero_is_the_count_and_stamp_only", + "ordinal": 412, + "entries": [ + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q5", + "title": "Fork 5", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 5, + "options_hash": "16d20a6dc44b6cc00afd378e9f0c8b51dfe5b07e869e1a2aa9588de1de587fab" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q5", + "options_hash": "16d20a6dc44b6cc00afd378e9f0c8b51dfe5b07e869e1a2aa9588de1de587fab", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:q5", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "16d20a6dc44b6cc00afd378e9f0c8b51dfe5b07e869e1a2aa9588de1de587fab", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000020-0000-4000-8000-000000000020" + } + ] + } + }, + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q4", + "title": "Fork 4", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 4, + "options_hash": "a64604237f906b6fec9cb292863685d966abf8048f276afa24cd4f1e1c4c74aa" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q4", + "options_hash": "a64604237f906b6fec9cb292863685d966abf8048f276afa24cd4f1e1c4c74aa", + "counted_answer_id": null, + "answers": [] + } + }, + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q3", + "title": "Fork 3", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "closed", + "ruling": "cultlib-gaps:resolution:question.q3.n1", + "ruled_from": [ + "n1" + ], + "order": 3, + "options_hash": "ced892eb663c22e3d7ece602708043ee545f56b9d4e4421d7f5bfaa86c418ce2" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q3", + "options_hash": "ced892eb663c22e3d7ece602708043ee545f56b9d4e4421d7f5bfaa86c418ce2", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:q3", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "ced892eb663c22e3d7ece602708043ee545f56b9d4e4421d7f5bfaa86c418ce2", + "choice": "yes", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000021-0000-4000-8000-000000000021" + }, + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n2", + "fork_id": "gamecult-chat:question:q3", + "outcome": "Refused", + "of": null, + "retracts": null, + "code": "fork_closed", + "options_hash": "ced892eb663c22e3d7ece602708043ee545f56b9d4e4421d7f5bfaa86c418ce2", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:04:05Z", + "claim_in_future": false, + "answerer": "9f8e7d6c-5b4a-4392-8170-0f1e2d3c4b5a", + "authority": "Steward", + "request_id": "00000022-0000-4000-8000-000000000022" + } + ] + } + }, + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q2", + "title": "Fork 2", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 2, + "options_hash": "5c7c2945e8e8689c6c928b11d7e0a033640b0c66afb7fcffb9e740c019d63e79" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q2", + "options_hash": "5c7c2945e8e8689c6c928b11d7e0a033640b0c66afb7fcffb9e740c019d63e79", + "counted_answer_id": null, + "answers": [] + } + }, + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q1", + "title": "Fork 1", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "closed", + "ruling": "cultlib-gaps:resolution:question.q1.n1", + "ruled_from": [], + "order": 1, + "options_hash": "61959964bd214ea1b961554c43ec46a9faf156dd547cc50c8c16bbdcd55f223a" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q1", + "options_hash": "61959964bd214ea1b961554c43ec46a9faf156dd547cc50c8c16bbdcd55f223a", + "counted_answer_id": null, + "answers": [] + } + } + ], + "steps": [ + { + "request": { + "schemaVersion": "gamecult.fork.list_request.v0", + "mind": "gamecult-chat", + "status": null, + "cursor": null, + "limit": 0 + }, + "response": { + "schemaVersion": "gamecult.fork.list.v0", + "mind": "gamecult-chat", + "stamp": "s412", + "open_count": 3, + "recorded_count": 2, + "forks": [], + "next_cursor": null + }, + "responseHex": "87AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E6C6973742E7630A46D696E64AD67616D6563756C742D63686174A57374616D70A473343132AA6F70656E5F636F756E7403AE7265636F726465645F636F756E7402A5666F726B7390AB6E6578745F637572736F72C0" + } + ] + }, + { + "name": "a_mind_with_no_forks", + "ordinal": 7, + "entries": [], + "steps": [ + { + "request": { + "schemaVersion": "gamecult.fork.list_request.v0", + "mind": "nibu", + "status": null, + "cursor": null, + "limit": 10 + }, + "response": { + "schemaVersion": "gamecult.fork.list.v0", + "mind": "nibu", + "stamp": "s7", + "open_count": 0, + "recorded_count": 0, + "forks": [], + "next_cursor": null + }, + "responseHex": "87AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E6C6973742E7630A46D696E64A46E696275A57374616D70A27337AA6F70656E5F636F756E7400AE7265636F726465645F636F756E7400A5666F726B7390AB6E6578745F637572736F72C0" + } + ] + } + ], + "mindRoutes": { + "minds": { + "gamecult-chat": "huginn-gamecult-chat.sock", + "nibu": "huginn-nibu.sock" + }, + "forks": { + "gamecult-chat": [ + "gamecult-chat:question:q1", + "gamecult-chat:question:q2", + "gamecult-chat:question:slot-mark-consent" + ], + "nibu": [ + "nibu:question:glow-order" + ] + }, + "cases": [ + { + "name": "list_a_percent_encoded_configured_mind_is_decoded_once", + "route": "list", + "mind": "%6eibu", + "fork_id": null, + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.read" + ], + "active": true + }, + "expect": "ok", + "httpStatus": 200, + "opened": "huginn-nibu.sock", + "stored_in": null + }, + { + "name": "list_a_configured_mind", + "route": "list", + "mind": "gamecult-chat", + "fork_id": null, + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.read" + ], + "active": true + }, + "expect": "ok", + "httpStatus": 200, + "opened": "huginn-gamecult-chat.sock", + "stored_in": null + }, + { + "name": "list_another_configured_mind", + "route": "list", + "mind": "nibu", + "fork_id": null, + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.read" + ], + "active": true + }, + "expect": "ok", + "httpStatus": 200, + "opened": "huginn-nibu.sock", + "stored_in": null + }, + { + "name": "read_a_fork_the_mind_holds", + "route": "read", + "mind": "gamecult-chat", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.read" + ], + "active": true + }, + "expect": "ok", + "httpStatus": 200, + "opened": "huginn-gamecult-chat.sock", + "stored_in": null + }, + { + "name": "answer_a_fork_the_mind_holds_is_stored_in_that_mind", + "route": "answer", + "mind": "gamecult-chat", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.answer" + ], + "active": true + }, + "expect": "ok", + "httpStatus": 200, + "opened": "huginn-gamecult-chat.sock", + "stored_in": "gamecult-chat" + }, + { + "name": "answer_on_the_other_mind", + "route": "answer", + "mind": "nibu", + "fork_id": "nibu:question:glow-order", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.answer" + ], + "active": true + }, + "expect": "ok", + "httpStatus": 200, + "opened": "huginn-nibu.sock", + "stored_in": "nibu" + }, + { + "name": "M1_list_dot_dot_is_unknown_and_opens_nothing", + "route": "list", + "mind": "../x", + "fork_id": null, + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.read" + ], + "active": true + }, + "expect": "fork_unknown", + "httpStatus": 404, + "opened": null, + "stored_in": null + }, + { + "name": "M1_list_slash_is_unknown_and_opens_nothing", + "route": "list", + "mind": "a/b", + "fork_id": null, + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.read" + ], + "active": true + }, + "expect": "fork_unknown", + "httpStatus": 404, + "opened": null, + "stored_in": null + }, + { + "name": "M1_list_encoded_slash_is_unknown_and_opens_nothing", + "route": "list", + "mind": "a%2fb", + "fork_id": null, + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.read" + ], + "active": true + }, + "expect": "fork_unknown", + "httpStatus": 404, + "opened": null, + "stored_in": null + }, + { + "name": "M1_list_encoded_dot_dot_is_unknown_and_opens_nothing", + "route": "list", + "mind": "%2e%2e%2fx", + "fork_id": null, + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.read" + ], + "active": true + }, + "expect": "fork_unknown", + "httpStatus": 404, + "opened": null, + "stored_in": null + }, + { + "name": "M1_list_nul_is_unknown_and_opens_nothing", + "route": "list", + "mind": "eureka%00", + "fork_id": null, + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.read" + ], + "active": true + }, + "expect": "fork_unknown", + "httpStatus": 404, + "opened": null, + "stored_in": null + }, + { + "name": "M1_list_four_kb_is_unknown_and_opens_nothing", + "route": "list", + "mind": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fork_id": null, + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.read" + ], + "active": true + }, + "expect": "fork_unknown", + "httpStatus": 404, + "opened": null, + "stored_in": null + }, + { + "name": "M1_list_other_case_is_unknown_and_opens_nothing", + "route": "list", + "mind": "GAMECULT-CHAT", + "fork_id": null, + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.read" + ], + "active": true + }, + "expect": "fork_unknown", + "httpStatus": 404, + "opened": null, + "stored_in": null + }, + { + "name": "M1_list_trailing_space_is_unknown_and_opens_nothing", + "route": "list", + "mind": "nibu%20", + "fork_id": null, + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.read" + ], + "active": true + }, + "expect": "fork_unknown", + "httpStatus": 404, + "opened": null, + "stored_in": null + }, + { + "name": "M1_list_invalid_utf8_is_unknown_and_opens_nothing", + "route": "list", + "mind": "%ff", + "fork_id": null, + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.read" + ], + "active": true + }, + "expect": "fork_unknown", + "httpStatus": 404, + "opened": null, + "stored_in": null + }, + { + "name": "M1_list_dot_is_unknown_and_opens_nothing", + "route": "list", + "mind": ".", + "fork_id": null, + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.read" + ], + "active": true + }, + "expect": "fork_unknown", + "httpStatus": 404, + "opened": null, + "stored_in": null + }, + { + "name": "M1_list_dots_is_unknown_and_opens_nothing", + "route": "list", + "mind": "..", + "fork_id": null, + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.read" + ], + "active": true + }, + "expect": "fork_unknown", + "httpStatus": 404, + "opened": null, + "stored_in": null + }, + { + "name": "M1_list_double_encoded_is_unknown_and_opens_nothing", + "route": "list", + "mind": "%256eibu", + "fork_id": null, + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.read" + ], + "active": true + }, + "expect": "fork_unknown", + "httpStatus": 404, + "opened": null, + "stored_in": null + }, + { + "name": "M1_read_dot_dot_is_unknown_and_opens_nothing", + "route": "read", + "mind": "../x", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.read" + ], + "active": true + }, + "expect": "fork_unknown", + "httpStatus": 404, + "opened": null, + "stored_in": null + }, + { + "name": "M1_read_slash_is_unknown_and_opens_nothing", + "route": "read", + "mind": "a/b", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.read" + ], + "active": true + }, + "expect": "fork_unknown", + "httpStatus": 404, + "opened": null, + "stored_in": null + }, + { + "name": "M1_read_encoded_slash_is_unknown_and_opens_nothing", + "route": "read", + "mind": "a%2fb", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.read" + ], + "active": true + }, + "expect": "fork_unknown", + "httpStatus": 404, + "opened": null, + "stored_in": null + }, + { + "name": "M1_read_encoded_dot_dot_is_unknown_and_opens_nothing", + "route": "read", + "mind": "%2e%2e%2fx", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.read" + ], + "active": true + }, + "expect": "fork_unknown", + "httpStatus": 404, + "opened": null, + "stored_in": null + }, + { + "name": "M1_read_nul_is_unknown_and_opens_nothing", + "route": "read", + "mind": "eureka%00", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.read" + ], + "active": true + }, + "expect": "fork_unknown", + "httpStatus": 404, + "opened": null, + "stored_in": null + }, + { + "name": "M1_read_four_kb_is_unknown_and_opens_nothing", + "route": "read", + "mind": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.read" + ], + "active": true + }, + "expect": "fork_unknown", + "httpStatus": 404, + "opened": null, + "stored_in": null + }, + { + "name": "M1_read_other_case_is_unknown_and_opens_nothing", + "route": "read", + "mind": "GAMECULT-CHAT", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.read" + ], + "active": true + }, + "expect": "fork_unknown", + "httpStatus": 404, + "opened": null, + "stored_in": null + }, + { + "name": "M1_read_trailing_space_is_unknown_and_opens_nothing", + "route": "read", + "mind": "nibu%20", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.read" + ], + "active": true + }, + "expect": "fork_unknown", + "httpStatus": 404, + "opened": null, + "stored_in": null + }, + { + "name": "M1_read_invalid_utf8_is_unknown_and_opens_nothing", + "route": "read", + "mind": "%ff", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.read" + ], + "active": true + }, + "expect": "fork_unknown", + "httpStatus": 404, + "opened": null, + "stored_in": null + }, + { + "name": "M1_read_dot_is_unknown_and_opens_nothing", + "route": "read", + "mind": ".", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.read" + ], + "active": true + }, + "expect": "fork_unknown", + "httpStatus": 404, + "opened": null, + "stored_in": null + }, + { + "name": "M1_read_dots_is_unknown_and_opens_nothing", + "route": "read", + "mind": "..", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.read" + ], + "active": true + }, + "expect": "fork_unknown", + "httpStatus": 404, + "opened": null, + "stored_in": null + }, + { + "name": "M1_read_double_encoded_is_unknown_and_opens_nothing", + "route": "read", + "mind": "%256eibu", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.read" + ], + "active": true + }, + "expect": "fork_unknown", + "httpStatus": 404, + "opened": null, + "stored_in": null + }, + { + "name": "M1_answer_dot_dot_is_unknown_and_opens_nothing", + "route": "answer", + "mind": "../x", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.answer" + ], + "active": true + }, + "expect": "fork_unknown", + "httpStatus": 404, + "opened": null, + "stored_in": null + }, + { + "name": "M1_answer_slash_is_unknown_and_opens_nothing", + "route": "answer", + "mind": "a/b", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.answer" + ], + "active": true + }, + "expect": "fork_unknown", + "httpStatus": 404, + "opened": null, + "stored_in": null + }, + { + "name": "M1_answer_encoded_slash_is_unknown_and_opens_nothing", + "route": "answer", + "mind": "a%2fb", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.answer" + ], + "active": true + }, + "expect": "fork_unknown", + "httpStatus": 404, + "opened": null, + "stored_in": null + }, + { + "name": "M1_answer_encoded_dot_dot_is_unknown_and_opens_nothing", + "route": "answer", + "mind": "%2e%2e%2fx", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.answer" + ], + "active": true + }, + "expect": "fork_unknown", + "httpStatus": 404, + "opened": null, + "stored_in": null + }, + { + "name": "M1_answer_nul_is_unknown_and_opens_nothing", + "route": "answer", + "mind": "eureka%00", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.answer" + ], + "active": true + }, + "expect": "fork_unknown", + "httpStatus": 404, + "opened": null, + "stored_in": null + }, + { + "name": "M1_answer_four_kb_is_unknown_and_opens_nothing", + "route": "answer", + "mind": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.answer" + ], + "active": true + }, + "expect": "fork_unknown", + "httpStatus": 404, + "opened": null, + "stored_in": null + }, + { + "name": "M1_answer_other_case_is_unknown_and_opens_nothing", + "route": "answer", + "mind": "GAMECULT-CHAT", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.answer" + ], + "active": true + }, + "expect": "fork_unknown", + "httpStatus": 404, + "opened": null, + "stored_in": null + }, + { + "name": "M1_answer_trailing_space_is_unknown_and_opens_nothing", + "route": "answer", + "mind": "nibu%20", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.answer" + ], + "active": true + }, + "expect": "fork_unknown", + "httpStatus": 404, + "opened": null, + "stored_in": null + }, + { + "name": "M1_answer_invalid_utf8_is_unknown_and_opens_nothing", + "route": "answer", + "mind": "%ff", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.answer" + ], + "active": true + }, + "expect": "fork_unknown", + "httpStatus": 404, + "opened": null, + "stored_in": null + }, + { + "name": "M1_answer_dot_is_unknown_and_opens_nothing", + "route": "answer", + "mind": ".", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.answer" + ], + "active": true + }, + "expect": "fork_unknown", + "httpStatus": 404, + "opened": null, + "stored_in": null + }, + { + "name": "M1_answer_dots_is_unknown_and_opens_nothing", + "route": "answer", + "mind": "..", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.answer" + ], + "active": true + }, + "expect": "fork_unknown", + "httpStatus": 404, + "opened": null, + "stored_in": null + }, + { + "name": "M1_answer_double_encoded_is_unknown_and_opens_nothing", + "route": "answer", + "mind": "%256eibu", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.answer" + ], + "active": true + }, + "expect": "fork_unknown", + "httpStatus": 404, + "opened": null, + "stored_in": null + }, + { + "name": "M2_read_unknown_mind_known_fork", + "route": "read", + "mind": "ghost", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.read" + ], + "active": true + }, + "expect": "fork_unknown", + "httpStatus": 404, + "opened": null, + "stored_in": null + }, + { + "name": "M2_read_known_mind_unknown_fork", + "route": "read", + "mind": "gamecult-chat", + "fork_id": "gamecult-chat:question:ghost", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.read" + ], + "active": true + }, + "expect": "fork_unknown", + "httpStatus": 404, + "opened": "huginn-gamecult-chat.sock", + "stored_in": null + }, + { + "name": "M2_answer_unknown_mind_known_fork", + "route": "answer", + "mind": "ghost", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.answer" + ], + "active": true + }, + "expect": "fork_unknown", + "httpStatus": 404, + "opened": null, + "stored_in": null + }, + { + "name": "M2_answer_known_mind_unknown_fork", + "route": "answer", + "mind": "gamecult-chat", + "fork_id": "gamecult-chat:question:ghost", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.answer" + ], + "active": true + }, + "expect": "fork_unknown", + "httpStatus": 404, + "opened": "huginn-gamecult-chat.sock", + "stored_in": null + }, + { + "name": "M2_list_unknown_mind", + "route": "list", + "mind": "ghost", + "fork_id": null, + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.read" + ], + "active": true + }, + "expect": "fork_unknown", + "httpStatus": 404, + "opened": null, + "stored_in": null + }, + { + "name": "M3_answer_on_nibu_with_a_fork_gamecult_chat_holds", + "route": "answer", + "mind": "nibu", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.answer" + ], + "active": true + }, + "expect": "fork_unknown", + "httpStatus": 404, + "opened": "huginn-nibu.sock", + "stored_in": null + }, + { + "name": "M3_answer_on_gamecult_chat_with_a_fork_nibu_holds", + "route": "answer", + "mind": "gamecult-chat", + "fork_id": "nibu:question:glow-order", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.answer" + ], + "active": true + }, + "expect": "fork_unknown", + "httpStatus": 404, + "opened": "huginn-gamecult-chat.sock", + "stored_in": null + }, + { + "name": "M4_desk_read_lists_every_mind", + "route": "list", + "mind": "nibu", + "fork_id": null, + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.read" + ], + "active": true + }, + "expect": "ok", + "httpStatus": 200, + "opened": "huginn-nibu.sock", + "stored_in": null + }, + { + "name": "M4_desk_read_reads_every_mind", + "route": "read", + "mind": "nibu", + "fork_id": "nibu:question:glow-order", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.read" + ], + "active": true + }, + "expect": "ok", + "httpStatus": 200, + "opened": "huginn-nibu.sock", + "stored_in": null + }, + { + "name": "M4_desk_answer_cannot_list_any_mind", + "route": "list", + "mind": "gamecult-chat", + "fork_id": null, + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.answer" + ], + "active": true + }, + "expect": "insufficient_scope", + "httpStatus": 403, + "opened": null, + "stored_in": null + }, + { + "name": "M4_desk_answer_cannot_list_the_other_mind", + "route": "list", + "mind": "nibu", + "fork_id": null, + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.answer" + ], + "active": true + }, + "expect": "insufficient_scope", + "httpStatus": 403, + "opened": null, + "stored_in": null + }, + { + "name": "M4_desk_read_cannot_answer_on_any_mind", + "route": "answer", + "mind": "nibu", + "fork_id": "nibu:question:glow-order", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.read" + ], + "active": true + }, + "expect": "insufficient_scope", + "httpStatus": 403, + "opened": null, + "stored_in": null + }, + { + "name": "auth_comes_before_the_mind_lookup_no_bearer", + "route": "list", + "mind": "ghost", + "fork_id": null, + "token": null, + "expect": "unauthenticated", + "httpStatus": 401, + "opened": null, + "stored_in": null + }, + { + "name": "auth_comes_before_the_mind_lookup_scope", + "route": "answer", + "mind": "ghost", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.read" + ], + "active": true + }, + "expect": "insufficient_scope", + "httpStatus": 403, + "opened": null, + "stored_in": null + }, + { + "name": "auth_comes_before_the_mind_lookup_non_active", + "route": "read", + "mind": "ghost", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.read" + ], + "active": false + }, + "expect": "forbidden", + "httpStatus": 403, + "opened": null, + "stored_in": null + } + ], + "sameResponse": [ + [ + "M2_read_unknown_mind_known_fork", + "M2_read_known_mind_unknown_fork" + ], + [ + "M2_answer_unknown_mind_known_fork", + "M2_answer_known_mind_unknown_fork" + ], + [ + "M2_list_unknown_mind", + "M2_read_known_mind_unknown_fork" + ], + [ + "M3_answer_on_nibu_with_a_fork_gamecult_chat_holds", + "M2_answer_known_mind_unknown_fork" + ] + ] + }, + "authCases": [ + { + "name": "list_with_desk_read", + "route": "list", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.read" + ], + "active": true + }, + "expect": "ok", + "huginn_down": false + }, + { + "name": "read_with_desk_read", + "route": "read", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.read" + ], + "active": true + }, + "expect": "ok", + "huginn_down": false + }, + { + "name": "answer_with_desk_answer", + "route": "answer", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.answer" + ], + "active": true + }, + "expect": "ok", + "huginn_down": false + }, + { + "name": "answer_with_both_scopes", + "route": "answer", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.read", + "desk.answer" + ], + "active": true + }, + "expect": "ok", + "huginn_down": false + }, + { + "name": "no_bearer_list", + "route": "list", + "token": null, + "expect": "unauthenticated", + "huginn_down": false + }, + { + "name": "no_bearer_answer", + "route": "answer", + "token": null, + "expect": "unauthenticated", + "huginn_down": false + }, + { + "name": "the_MAS_token_is_refused", + "route": "list", + "token": { + "audience": "matrix-authentication-service", + "scopes": [ + "openid", + "profile" + ], + "active": true + }, + "expect": "unauthenticated", + "huginn_down": false + }, + { + "name": "a_votes_token_cannot_list", + "route": "list", + "token": { + "audience": "bifrost-votes", + "scopes": [ + "votes.read" + ], + "active": true + }, + "expect": "unauthenticated", + "huginn_down": false + }, + { + "name": "a_votes_token_cannot_read", + "route": "read", + "token": { + "audience": "bifrost-votes", + "scopes": [ + "votes.read" + ], + "active": true + }, + "expect": "unauthenticated", + "huginn_down": false + }, + { + "name": "a_votes_token_cannot_answer", + "route": "answer", + "token": { + "audience": "bifrost-votes", + "scopes": [ + "votes.cast" + ], + "active": true + }, + "expect": "unauthenticated", + "huginn_down": false + }, + { + "name": "desk_audience_with_a_votes_scope_only", + "route": "answer", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "votes.cast" + ], + "active": true + }, + "expect": "insufficient_scope", + "huginn_down": false + }, + { + "name": "desk_read_cannot_answer", + "route": "answer", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.read" + ], + "active": true + }, + "expect": "insufficient_scope", + "huginn_down": false + }, + { + "name": "desk_answer_cannot_list", + "route": "list", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.answer" + ], + "active": true + }, + "expect": "insufficient_scope", + "huginn_down": false + }, + { + "name": "a_non_active_member_cannot_list", + "route": "list", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.read" + ], + "active": false + }, + "expect": "forbidden", + "huginn_down": false + }, + { + "name": "a_non_active_member_cannot_read", + "route": "read", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.read" + ], + "active": false + }, + "expect": "forbidden", + "huginn_down": false + }, + { + "name": "a_non_active_member_cannot_answer", + "route": "answer", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.answer" + ], + "active": false + }, + "expect": "forbidden", + "huginn_down": false + }, + { + "name": "scope_is_judged_before_membership", + "route": "answer", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.read" + ], + "active": false + }, + "expect": "insufficient_scope", + "huginn_down": false + }, + { + "name": "huginn_down_list", + "route": "list", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.read" + ], + "active": true + }, + "expect": "unavailable", + "huginn_down": true + }, + { + "name": "huginn_down_read", + "route": "read", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.read" + ], + "active": true + }, + "expect": "unavailable", + "huginn_down": true + }, + { + "name": "huginn_down_answer", + "route": "answer", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.answer" + ], + "active": true + }, + "expect": "unavailable", + "huginn_down": true + }, + { + "name": "auth_is_judged_before_huginn_is_asked", + "route": "list", + "token": null, + "expect": "unauthenticated", + "huginn_down": true + }, + { + "name": "membership_is_judged_before_huginn_is_asked", + "route": "read", + "token": { + "audience": "bifrost-desk", + "scopes": [ + "desk.read" + ], + "active": false + }, + "expect": "forbidden", + "huginn_down": true + } + ] + }, + "scenarios": [ + { + "name": "operator_answers_and_is_counted", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": "n1" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964A26E31A7616E737765727391DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031" + }, + { + "name": "choice_alone_is_counted", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": null + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000005-0000-4000-8000-000000000005" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": null, + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000005-0000-4000-8000-000000000005" + }, + "card": null, + "counted_answer_id": "n1" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": null, + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000005-0000-4000-8000-000000000005" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964A26E31A7616E737765727391DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465C0AB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030352D303030302D343030302D383030302D303030303030303030303035" + }, + { + "name": "note_alone_is_counted_a_clear_pick", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": null, + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000006-0000-4000-8000-000000000006" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": null, + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000006-0000-4000-8000-000000000006" + }, + "card": null, + "counted_answer_id": "n1" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": null, + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000006-0000-4000-8000-000000000006" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964A26E31A7616E737765727391DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365C0A46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030362D303030302D343030302D383030302D303030303030303030303036" + }, + { + "name": "V1_answerer_in_the_body_is_refused_at_the_door_and_stores_nothing", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001", + "answerer": "x" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.refusal.v0", + "code": "body_forbidden_field", + "error": "body_forbidden_field: answerer and authority are set by the receiver" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": null, + "answers": [] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964C0A7616E737765727390" + }, + { + "name": "V1_authority_nested_in_answer_is_refused", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": null, + "authority": "Operator" + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.refusal.v0", + "code": "body_forbidden_field", + "error": "body_forbidden_field: answerer and authority are set by the receiver" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": null, + "answers": [] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964C0A7616E737765727390" + }, + { + "name": "V1_both_are_refused_not_stripped", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.refusal.v0", + "code": "body_forbidden_field", + "error": "body_forbidden_field: answerer and authority are set by the receiver" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": null, + "answers": [] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964C0A7616E737765727390" + }, + { + "name": "M5_a_body_naming_deciders_is_an_unknown_key_the_decider_list_is_never_the_bodys", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001", + "deciders": [ + "11111111-2222-4333-8444-555555555555" + ] + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.refusal.v0", + "code": "body_unknown_field", + "error": "body_unknown_field: the body is a closed shape" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": null, + "answers": [] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964C0A7616E737765727390" + }, + { + "name": "M1_a_body_naming_a_mind_is_an_unknown_key_the_mind_is_the_routes", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001", + "mind": "nibu" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.refusal.v0", + "code": "body_unknown_field", + "error": "body_unknown_field: the body is a closed shape" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": null, + "answers": [] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964C0A7616E737765727390" + }, + { + "name": "an_unknown_key_is_refused_the_body_is_closed", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001", + "mood": "happy" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.refusal.v0", + "code": "body_unknown_field", + "error": "body_unknown_field: the body is a closed shape" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": null, + "answers": [] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964C0A7616E737765727390" + }, + { + "name": "an_answer_with_neither_choice_nor_note_is_invalid", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": null, + "note": null + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000008-0000-4000-8000-000000000008" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.refusal.v0", + "code": "body_invalid", + "error": "body_invalid: the body does not satisfy gamecult.fork.answer_request.v0" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": null, + "answers": [] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964C0A7616E737765727390" + }, + { + "name": "a_non_retraction_without_an_answer_is_invalid", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": null, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000009-0000-4000-8000-000000000009" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.refusal.v0", + "code": "body_invalid", + "error": "body_invalid: the body does not satisfy gamecult.fork.answer_request.v0" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": null, + "answers": [] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964C0A7616E737765727390" + }, + { + "name": "unavailable_before_huginn_stored_nothing_is_retried_into_a_Counted_answer", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n9", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z", + "door_down": true, + "retry_after_seconds": 30 + }, + "response": { + "schemaVersion": "gamecult.fork.unavailable.v0", + "code": "unavailable", + "error": "unavailable: the outcome is not confirmed, try again with the same request_id", + "retryAfterSeconds": 30 + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": "n1" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964A26E31A7616E737765727391DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031" + }, + { + "name": "unavailable_after_huginn_stored_the_retry_with_the_same_request_id_returns_the_stored_outcome", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z", + "door_down": true, + "stored": true, + "retry_after_seconds": 30 + }, + "response": { + "schemaVersion": "gamecult.fork.unavailable.v0", + "code": "unavailable", + "error": "unavailable: the outcome is not confirmed, try again with the same request_id", + "retryAfterSeconds": 30 + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n9", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": "n1" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964A26E31A7616E737765727391DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031" + }, + { + "name": "a_retry_never_adds_a_Revision", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": "n1" + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n9", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": "n1" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964A26E31A7616E737765727391DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031" + }, + { + "name": "Q1_a_replayed_stale_fork_returns_the_CURRENT_card_and_stores_nothing_new", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "14b8ffbdb61106dbc06f69f3fc8e2072af7a205d766e4cc040e89733013d54dc", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000007-0000-4000-8000-000000000007" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": null, + "code": "stale_fork", + "options_hash": "14b8ffbdb61106dbc06f69f3fc8e2072af7a205d766e4cc040e89733013d54dc", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000007-0000-4000-8000-000000000007" + }, + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "counted_answer_id": null + } + }, + { + "kind": "card", + "body": null, + "server": null, + "response": null, + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent (revised)", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "e7696ae1d487f78b5dc99fb8ccbd684d6e6f1ea7d7e190c21f3c6b4d8c42aaf7" + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "14b8ffbdb61106dbc06f69f3fc8e2072af7a205d766e4cc040e89733013d54dc", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000007-0000-4000-8000-000000000007" + }, + "server": { + "answer_id": "n9", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": null, + "code": "stale_fork", + "options_hash": "14b8ffbdb61106dbc06f69f3fc8e2072af7a205d766e4cc040e89733013d54dc", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000007-0000-4000-8000-000000000007" + }, + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent (revised)", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "e7696ae1d487f78b5dc99fb8ccbd684d6e6f1ea7d7e190c21f3c6b4d8c42aaf7" + }, + "counted_answer_id": null + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "e7696ae1d487f78b5dc99fb8ccbd684d6e6f1ea7d7e190c21f3c6b4d8c42aaf7", + "counted_answer_id": null, + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": null, + "code": "stale_fork", + "options_hash": "14b8ffbdb61106dbc06f69f3fc8e2072af7a205d766e4cc040e89733013d54dc", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000007-0000-4000-8000-000000000007" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94065373639366165316434383766373862356463393966623863636264363834643665366631656137643765313930633231663363366234643863343261616637B1636F756E7465645F616E737765725F6964C0A7616E737765727391DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A752656675736564A26F66C0A87265747261637473C0A4636F6465AA7374616C655F666F726BAC6F7074696F6E735F68617368D94031346238666662646236313130366462633036663639663366633865323037326166376132303564373636653463633034306538393733333031336435346463A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030372D303030302D343030302D383030302D303030303030303030303037" + }, + { + "name": "V13_only_answered_at_changed_is_request_id_conflict", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": "n1" + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:03:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n9", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.refusal.v0", + "code": "request_id_conflict", + "error": "request_id_conflict: this request_id was used for a different body" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964A26E31A7616E737765727391DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031" + }, + { + "name": "V14_an_identical_body_from_another_account_gets_its_own_Counted_and_never_the_first_accounts_outcome", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "9f8e7d6c-5b4a-4392-8170-0f1e2d3c4b5a" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n1", + "answerer": "11111111-2222-4333-8444-555555555555", + "authority": "Member", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": null, + "code": "not_a_decider", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "11111111-2222-4333-8444-555555555555", + "authority": "Member", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": null + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n2", + "answerer": "9f8e7d6c-5b4a-4392-8170-0f1e2d3c4b5a", + "authority": "Steward", + "received_at": "2026-11-12T19:03:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n2", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:03:05Z", + "claim_in_future": false, + "answerer": "9f8e7d6c-5b4a-4392-8170-0f1e2d3c4b5a", + "authority": "Steward", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": "n2" + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n9", + "answerer": "11111111-2222-4333-8444-555555555555", + "authority": "Member", + "received_at": "2026-11-12T19:04:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": null, + "code": "not_a_decider", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "11111111-2222-4333-8444-555555555555", + "authority": "Member", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": "n2" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": "n2", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": null, + "code": "not_a_decider", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "11111111-2222-4333-8444-555555555555", + "authority": "Member", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n2", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:03:05Z", + "claim_in_future": false, + "answerer": "9f8e7d6c-5b4a-4392-8170-0f1e2d3c4b5a", + "authority": "Steward", + "request_id": "00000001-0000-4000-8000-000000000001" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964A26E32A7616E737765727392DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A752656675736564A26F66C0A87265747261637473C0A4636F6465AD6E6F745F615F64656369646572AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92431313131313131312D323232322D343333332D383434342D353535353535353535353535A9617574686F72697479A64D656D626572AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E32A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30333A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92439663865376436632D356234612D343339322D383137302D306631653264336334623561A9617574686F72697479A753746577617264AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031" + }, + { + "name": "V14_an_identical_body_from_another_decider_is_its_own_Revision_and_the_first_retry_still_returns_n1", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "9f8e7d6c-5b4a-4392-8170-0f1e2d3c4b5a" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": "n1" + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n3", + "answerer": "9f8e7d6c-5b4a-4392-8170-0f1e2d3c4b5a", + "authority": "Steward", + "received_at": "2026-11-12T19:05:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n3", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Revision", + "of": "n1", + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:05:05Z", + "claim_in_future": false, + "answerer": "9f8e7d6c-5b4a-4392-8170-0f1e2d3c4b5a", + "authority": "Steward", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": "n1" + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n9", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": "n1" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n3", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Revision", + "of": "n1", + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:05:05Z", + "claim_in_future": false, + "answerer": "9f8e7d6c-5b4a-4392-8170-0f1e2d3c4b5a", + "authority": "Steward", + "request_id": "00000001-0000-4000-8000-000000000001" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964A26E31A7616E737765727392DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E33A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A85265766973696F6EA26F66A26E31A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30353A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92439663865376436632D356234612D343339322D383137302D306631653264336334623561A9617574686F72697479A753746577617264AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031" + }, + { + "name": "V16_Q5_a_forbidden_request_binds_no_request_id", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n9", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z", + "active": false + }, + "response": { + "schemaVersion": "gamecult.fork.refusal.v0", + "code": "forbidden", + "error": "forbidden: only Active members may use the desk" + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": "n1" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964A26E31A7616E737765727391DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031" + }, + { + "name": "V17_Q6_a_body_refused_at_the_door_binds_no_request_id", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001", + "mood": "happy" + }, + "server": { + "answer_id": "n9", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.refusal.v0", + "code": "body_unknown_field", + "error": "body_unknown_field: the body is a closed shape" + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": "n1" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964A26E31A7616E737765727391DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031" + }, + { + "name": "V17_Q6_an_unknown_fork_binds_no_request_id", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:nope", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n9", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.refusal.v0", + "code": "fork_unknown", + "error": "fork_unknown: no such fork" + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": "n1" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964A26E31A7616E737765727391DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031" + }, + { + "name": "the_same_request_id_with_another_body_is_request_id_conflict", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": "n1" + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "yes", + "note": null + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n9", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.refusal.v0", + "code": "request_id_conflict", + "error": "request_id_conflict: this request_id was used for a different body" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964A26E31A7616E737765727391DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031" + }, + { + "name": "the_same_request_id_from_another_account_is_another_request", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "9f8e7d6c-5b4a-4392-8170-0f1e2d3c4b5a" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": "n1" + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "yes", + "note": "Share it, the audience is settled." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:05:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n3", + "answerer": "9f8e7d6c-5b4a-4392-8170-0f1e2d3c4b5a", + "authority": "Steward", + "received_at": "2026-11-12T19:05:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n3", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Revision", + "of": "n1", + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "yes", + "note": "Share it, the audience is settled.", + "answered_at": "2026-11-12T19:05:00Z", + "received_at": "2026-11-12T19:05:05Z", + "claim_in_future": false, + "answerer": "9f8e7d6c-5b4a-4392-8170-0f1e2d3c4b5a", + "authority": "Steward", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": "n1" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n3", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Revision", + "of": "n1", + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "yes", + "note": "Share it, the audience is settled.", + "answered_at": "2026-11-12T19:05:00Z", + "received_at": "2026-11-12T19:05:05Z", + "claim_in_future": false, + "answerer": "9f8e7d6c-5b4a-4392-8170-0f1e2d3c4b5a", + "authority": "Steward", + "request_id": "00000001-0000-4000-8000-000000000001" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964A26E31A7616E737765727392DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E33A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A85265766973696F6EA26F66A26E31A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A3796573A46E6F7465D92253686172652069742C207468652061756469656E636520697320736574746C65642EAB616E7377657265645F6174B4323032362D31312D31325431393A30353A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30353A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92439663865376436632D356234612D343339322D383137302D306631653264336334623561A9617574686F72697479A753746577617264AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031" + }, + { + "name": "a_non_active_member_is_forbidden_before_the_body_is_read", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001", + "answerer": "x" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z", + "active": false + }, + "response": { + "schemaVersion": "gamecult.fork.refusal.v0", + "code": "forbidden", + "error": "forbidden: only Active members may use the desk" + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z", + "active": false + }, + "response": { + "schemaVersion": "gamecult.fork.refusal.v0", + "code": "forbidden", + "error": "forbidden: only Active members may use the desk" + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001", + "mood": "happy" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z", + "active": false, + "door_down": true, + "retry_after_seconds": 30 + }, + "response": { + "schemaVersion": "gamecult.fork.refusal.v0", + "code": "forbidden", + "error": "forbidden: only Active members may use the desk" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": null, + "answers": [] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964C0A7616E737765727390" + }, + { + "name": "no_session_is_unauthenticated", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n1", + "answerer": null, + "authority": null, + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.refusal.v0", + "code": "unauthenticated", + "error": "unauthenticated: a verified session is required" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": null, + "answers": [] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964C0A7616E737765727390" + }, + { + "name": "an_unknown_fork_is_refused_at_the_door", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:nope", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "0000000a-0000-4000-8000-00000000000a" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.refusal.v0", + "code": "fork_unknown", + "error": "fork_unknown: no such fork" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": null, + "answers": [] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964C0A7616E737765727390" + }, + { + "name": "V2_stale_hash_is_refused_not_counted_and_stored_as_Refused", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "14b8ffbdb61106dbc06f69f3fc8e2072af7a205d766e4cc040e89733013d54dc", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000007-0000-4000-8000-000000000007" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": null, + "code": "stale_fork", + "options_hash": "14b8ffbdb61106dbc06f69f3fc8e2072af7a205d766e4cc040e89733013d54dc", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000007-0000-4000-8000-000000000007" + }, + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "counted_answer_id": null + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": null, + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": null, + "code": "stale_fork", + "options_hash": "14b8ffbdb61106dbc06f69f3fc8e2072af7a205d766e4cc040e89733013d54dc", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000007-0000-4000-8000-000000000007" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964C0A7616E737765727391DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A752656675736564A26F66C0A87265747261637473C0A4636F6465AA7374616C655F666F726BAC6F7074696F6E735F68617368D94031346238666662646236313130366462633036663639663366633865323037326166376132303564373636653463633034306538393733333031336435346463A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030372D303030302D343030302D383030302D303030303030303030303037" + }, + { + "name": "V2_a_stale_answer_then_a_fresh_one_the_fresh_one_is_counted", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "14b8ffbdb61106dbc06f69f3fc8e2072af7a205d766e4cc040e89733013d54dc", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "0000000b-0000-4000-8000-00000000000b" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": null, + "code": "stale_fork", + "options_hash": "14b8ffbdb61106dbc06f69f3fc8e2072af7a205d766e4cc040e89733013d54dc", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "0000000b-0000-4000-8000-00000000000b" + }, + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "counted_answer_id": null + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n2", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:06:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n2", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:06:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": "n2" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": "n2", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": null, + "code": "stale_fork", + "options_hash": "14b8ffbdb61106dbc06f69f3fc8e2072af7a205d766e4cc040e89733013d54dc", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "0000000b-0000-4000-8000-00000000000b" + }, + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n2", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:06:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964A26E32A7616E737765727392DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A752656675736564A26F66C0A87265747261637473C0A4636F6465AA7374616C655F666F726BAC6F7074696F6E735F68617368D94031346238666662646236313130366462633036663639663366633865323037326166376132303564373636653463633034306538393733333031336435346463A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030622D303030302D343030302D383030302D303030303030303030303062DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E32A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30363A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031" + }, + { + "name": "V3_an_answer_off_the_decider_list_is_Refused_not_a_decider_and_stored", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "yes", + "note": "Share, please." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000004-0000-4000-8000-000000000004" + }, + "server": { + "answer_id": "n4", + "answerer": "11111111-2222-4333-8444-555555555555", + "authority": "Member", + "received_at": "2026-11-12T19:03:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n4", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": null, + "code": "not_a_decider", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "yes", + "note": "Share, please.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:03:05Z", + "claim_in_future": false, + "answerer": "11111111-2222-4333-8444-555555555555", + "authority": "Member", + "request_id": "00000004-0000-4000-8000-000000000004" + }, + "card": null, + "counted_answer_id": null + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": null, + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n4", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": null, + "code": "not_a_decider", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "yes", + "note": "Share, please.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:03:05Z", + "claim_in_future": false, + "answerer": "11111111-2222-4333-8444-555555555555", + "authority": "Member", + "request_id": "00000004-0000-4000-8000-000000000004" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964C0A7616E737765727391DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E34A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A752656675736564A26F66C0A87265747261637473C0A4636F6465AD6E6F745F615F64656369646572AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A3796573A46E6F7465AE53686172652C20706C656173652EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30333A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92431313131313131312D323232322D343333332D383434342D353535353535353535353535A9617574686F72697479A64D656D626572AA726571756573745F6964D92430303030303030342D303030302D343030302D383030302D303030303030303030303034" + }, + { + "name": "a_non_decider_with_a_stale_hash_is_stale_first", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "14b8ffbdb61106dbc06f69f3fc8e2072af7a205d766e4cc040e89733013d54dc", + "answer": { + "choice": "yes", + "note": "Share, please." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000004-0000-4000-8000-000000000004" + }, + "server": { + "answer_id": "n4", + "answerer": "11111111-2222-4333-8444-555555555555", + "authority": "Member", + "received_at": "2026-11-12T19:03:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n4", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": null, + "code": "stale_fork", + "options_hash": "14b8ffbdb61106dbc06f69f3fc8e2072af7a205d766e4cc040e89733013d54dc", + "choice": "yes", + "note": "Share, please.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:03:05Z", + "claim_in_future": false, + "answerer": "11111111-2222-4333-8444-555555555555", + "authority": "Member", + "request_id": "00000004-0000-4000-8000-000000000004" + }, + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "counted_answer_id": null + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": null, + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n4", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": null, + "code": "stale_fork", + "options_hash": "14b8ffbdb61106dbc06f69f3fc8e2072af7a205d766e4cc040e89733013d54dc", + "choice": "yes", + "note": "Share, please.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:03:05Z", + "claim_in_future": false, + "answerer": "11111111-2222-4333-8444-555555555555", + "authority": "Member", + "request_id": "00000004-0000-4000-8000-000000000004" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964C0A7616E737765727391DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E34A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A752656675736564A26F66C0A87265747261637473C0A4636F6465AA7374616C655F666F726BAC6F7074696F6E735F68617368D94031346238666662646236313130366462633036663639663366633865323037326166376132303564373636653463633034306538393733333031336435346463A663686F696365A3796573A46E6F7465AE53686172652C20706C656173652EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30333A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92431313131313131312D323232322D343333332D383434342D353535353535353535353535A9617574686F72697479A64D656D626572AA726571756573745F6964D92430303030303030342D303030302D343030302D383030302D303030303030303030303034" + }, + { + "name": "V4_a_closed_fork_refuses_and_stores_the_refusal", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "closed", + "ruling": "cultlib-gaps:resolution:question.slot-mark-consent.n1", + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": null, + "code": "fork_closed", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": null + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": null, + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": null, + "code": "fork_closed", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964C0A7616E737765727391DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A752656675736564A26F66C0A87265747261637473C0A4636F6465AB666F726B5F636C6F736564AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031" + }, + { + "name": "V5_second_answer_from_one_decider_is_a_Revision_the_first_stays_counted", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": "n1" + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "yes", + "note": "On reflection, share it." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:06:00Z", + "request_id": "00000002-0000-4000-8000-000000000002" + }, + "server": { + "answer_id": "n2", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:06:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n2", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Revision", + "of": "n1", + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "yes", + "note": "On reflection, share it.", + "answered_at": "2026-11-12T19:06:00Z", + "received_at": "2026-11-12T19:06:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000002-0000-4000-8000-000000000002" + }, + "card": null, + "counted_answer_id": "n1" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n2", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Revision", + "of": "n1", + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "yes", + "note": "On reflection, share it.", + "answered_at": "2026-11-12T19:06:00Z", + "received_at": "2026-11-12T19:06:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000002-0000-4000-8000-000000000002" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964A26E31A7616E737765727392DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E32A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A85265766973696F6EA26F66A26E31A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A3796573A46E6F7465B84F6E207265666C656374696F6E2C2073686172652069742EAB616E7377657265645F6174B4323032362D31312D31325431393A30363A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30363A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030322D303030302D343030302D383030302D303030303030303030303032" + }, + { + "name": "V5_receiver_order_decides_not_answered_at", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "yes", + "note": "On reflection, share it." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:06:00Z", + "request_id": "00000002-0000-4000-8000-000000000002" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "yes", + "note": "On reflection, share it.", + "answered_at": "2026-11-12T19:06:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000002-0000-4000-8000-000000000002" + }, + "card": null, + "counted_answer_id": "n1" + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n2", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:03:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n2", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Revision", + "of": "n1", + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:03:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": "n1" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "yes", + "note": "On reflection, share it.", + "answered_at": "2026-11-12T19:06:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000002-0000-4000-8000-000000000002" + }, + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n2", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Revision", + "of": "n1", + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:03:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964A26E31A7616E737765727392DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A3796573A46E6F7465B84F6E207265666C656374696F6E2C2073686172652069742EAB616E7377657265645F6174B4323032362D31312D31325431393A30363A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030322D303030302D343030302D383030302D303030303030303030303032DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E32A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A85265766973696F6EA26F66A26E31A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30333A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031" + }, + { + "name": "V5_reversed_arrival_reverses_the_result", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": "n1" + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "yes", + "note": "On reflection, share it." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:06:00Z", + "request_id": "00000002-0000-4000-8000-000000000002" + }, + "server": { + "answer_id": "n2", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:03:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n2", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Revision", + "of": "n1", + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "yes", + "note": "On reflection, share it.", + "answered_at": "2026-11-12T19:06:00Z", + "received_at": "2026-11-12T19:03:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000002-0000-4000-8000-000000000002" + }, + "card": null, + "counted_answer_id": "n1" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n2", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Revision", + "of": "n1", + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "yes", + "note": "On reflection, share it.", + "answered_at": "2026-11-12T19:06:00Z", + "received_at": "2026-11-12T19:03:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000002-0000-4000-8000-000000000002" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964A26E31A7616E737765727392DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E32A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A85265766973696F6EA26F66A26E31A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A3796573A46E6F7465B84F6E207265666C656374696F6E2C2073686172652069742EAB616E7377657265645F6174B4323032362D31312D31325431393A30363A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30333A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030322D303030302D343030302D383030302D303030303030303030303032" + }, + { + "name": "a_second_decider_answer_is_recorded_as_a_Revision", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "9f8e7d6c-5b4a-4392-8170-0f1e2d3c4b5a" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": "n1" + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "yes", + "note": "Share it, the audience is settled." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:05:00Z", + "request_id": "00000003-0000-4000-8000-000000000003" + }, + "server": { + "answer_id": "n3", + "answerer": "9f8e7d6c-5b4a-4392-8170-0f1e2d3c4b5a", + "authority": "Steward", + "received_at": "2026-11-12T19:05:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n3", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Revision", + "of": "n1", + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "yes", + "note": "Share it, the audience is settled.", + "answered_at": "2026-11-12T19:05:00Z", + "received_at": "2026-11-12T19:05:05Z", + "claim_in_future": false, + "answerer": "9f8e7d6c-5b4a-4392-8170-0f1e2d3c4b5a", + "authority": "Steward", + "request_id": "00000003-0000-4000-8000-000000000003" + }, + "card": null, + "counted_answer_id": "n1" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n3", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Revision", + "of": "n1", + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "yes", + "note": "Share it, the audience is settled.", + "answered_at": "2026-11-12T19:05:00Z", + "received_at": "2026-11-12T19:05:05Z", + "claim_in_future": false, + "answerer": "9f8e7d6c-5b4a-4392-8170-0f1e2d3c4b5a", + "authority": "Steward", + "request_id": "00000003-0000-4000-8000-000000000003" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964A26E31A7616E737765727392DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E33A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A85265766973696F6EA26F66A26E31A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A3796573A46E6F7465D92253686172652069742C207468652061756469656E636520697320736574746C65642EAB616E7377657265645F6174B4323032362D31312D31325431393A30353A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30353A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92439663865376436632D356234612D343339322D383137302D306631653264336334623561A9617574686F72697479A753746577617264AA726571756573745F6964D92430303030303030332D303030302D343030302D383030302D303030303030303030303033" + }, + { + "name": "a_claim_exactly_five_minutes_ahead_is_not_flagged", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:07:05Z", + "request_id": "0000000c-0000-4000-8000-00000000000c" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:07:05Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "0000000c-0000-4000-8000-00000000000c" + }, + "card": null, + "counted_answer_id": "n1" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:07:05Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "0000000c-0000-4000-8000-00000000000c" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964A26E31A7616E737765727391DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30373A30355AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030632D303030302D343030302D383030302D303030303030303030303063" + }, + { + "name": "a_claim_five_minutes_and_half_a_second_ahead_is_flagged_from_the_stored_received_at", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:07:05.500Z", + "request_id": "0000000d-0000-4000-8000-00000000000d" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:07:05.500Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": true, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "0000000d-0000-4000-8000-00000000000d" + }, + "card": null, + "counted_answer_id": "n1" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:07:05.500Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": true, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "0000000d-0000-4000-8000-00000000000d" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964A26E31A7616E737765727391DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B8323032362D31312D31325431393A30373A30352E3530305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C3A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030642D303030302D343030302D383030302D303030303030303030303064" + }, + { + "name": "a_claim_more_than_five_minutes_ahead_is_accepted_and_flagged", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:09:00Z", + "request_id": "0000000e-0000-4000-8000-00000000000e" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:09:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": true, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "0000000e-0000-4000-8000-00000000000e" + }, + "card": null, + "counted_answer_id": "n1" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:09:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": true, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "0000000e-0000-4000-8000-00000000000e" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964A26E31A7616E737765727391DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30393A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C3A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030652D303030302D343030302D383030302D303030303030303030303065" + }, + { + "name": "V6_a_matrix_edit_redaction_or_reaction_changes_nothing", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": "n1" + } + }, + { + "kind": "matrix_event", + "body": null, + "server": null, + "response": null, + "matrix_kind": "m.replace", + "choice": "yes" + }, + { + "kind": "matrix_event", + "body": null, + "server": null, + "response": null, + "matrix_kind": "m.redaction", + "choice": null + }, + { + "kind": "matrix_event", + "body": null, + "server": null, + "response": null, + "matrix_kind": "m.reaction", + "choice": "yes" + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964A26E31A7616E737765727391DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031" + }, + { + "name": "V7_a_choice_that_is_not_an_option_is_Refused_choice_invalid", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "maybe", + "note": null + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "0000000f-0000-4000-8000-00000000000f" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": null, + "code": "choice_invalid", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "maybe", + "note": null, + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "0000000f-0000-4000-8000-00000000000f" + }, + "card": null, + "counted_answer_id": null + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": null, + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": null, + "code": "choice_invalid", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "maybe", + "note": null, + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "0000000f-0000-4000-8000-00000000000f" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964C0A7616E737765727391DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A752656675736564A26F66C0A87265747261637473C0A4636F6465AE63686F6963655F696E76616C6964AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A56D61796265A46E6F7465C0AB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030662D303030302D343030302D383030302D303030303030303030303066" + }, + { + "name": "a_changed_card_stops_the_old_answers_counting_and_a_new_answer_is_counted", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": "n1" + } + }, + { + "kind": "card", + "body": null, + "server": null, + "response": null, + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent (revised)", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "e7696ae1d487f78b5dc99fb8ccbd684d6e6f1ea7d7e190c21f3c6b4d8c42aaf7" + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "e7696ae1d487f78b5dc99fb8ccbd684d6e6f1ea7d7e190c21f3c6b4d8c42aaf7", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000010-0000-4000-8000-000000000010" + }, + "server": { + "answer_id": "n2", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:06:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n2", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "e7696ae1d487f78b5dc99fb8ccbd684d6e6f1ea7d7e190c21f3c6b4d8c42aaf7", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:06:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000010-0000-4000-8000-000000000010" + }, + "card": null, + "counted_answer_id": "n2" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "e7696ae1d487f78b5dc99fb8ccbd684d6e6f1ea7d7e190c21f3c6b4d8c42aaf7", + "counted_answer_id": "n2", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n2", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "e7696ae1d487f78b5dc99fb8ccbd684d6e6f1ea7d7e190c21f3c6b4d8c42aaf7", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:06:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000010-0000-4000-8000-000000000010" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94065373639366165316434383766373862356463393966623863636264363834643665366631656137643765313930633231663363366234643863343261616637B1636F756E7465645F616E737765725F6964A26E32A7616E737765727392DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E32A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94065373639366165316434383766373862356463393966623863636264363834643665366631656137643765313930633231663363366234643863343261616637A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30363A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303031302D303030302D343030302D383030302D303030303030303030303130" + }, + { + "name": "a_changed_card_leaves_no_counted_answer_until_one_arrives", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": "n1" + } + }, + { + "kind": "card", + "body": null, + "server": null, + "response": null, + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent (revised)", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "e7696ae1d487f78b5dc99fb8ccbd684d6e6f1ea7d7e190c21f3c6b4d8c42aaf7" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "e7696ae1d487f78b5dc99fb8ccbd684d6e6f1ea7d7e190c21f3c6b4d8c42aaf7", + "counted_answer_id": null, + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94065373639366165316434383766373862356463393966623863636264363834643665366631656137643765313930633231663363366234643863343261616637B1636F756E7465645F616E737765725F6964C0A7616E737765727391DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031" + }, + { + "name": "V8_retraction_with_an_answer_is_bad", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": "n1" + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": null + }, + "retracted": true, + "retracts": "n1", + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000011-0000-4000-8000-000000000011" + }, + "server": { + "answer_id": "n9", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:09:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n9", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": "n1", + "code": "bad_retraction", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": null, + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:09:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000011-0000-4000-8000-000000000011" + }, + "card": null, + "counted_answer_id": "n1" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n9", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": "n1", + "code": "bad_retraction", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": null, + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:09:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000011-0000-4000-8000-000000000011" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964A26E31A7616E737765727392DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E39A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A752656675736564A26F66C0A87265747261637473A26E31A4636F6465AE6261645F72657472616374696F6EAC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465C0AB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30393A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303031312D303030302D343030302D383030302D303030303030303030303131" + }, + { + "name": "V8_retraction_without_retracts_is_bad", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": "n1" + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": null, + "retracted": true, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000012-0000-4000-8000-000000000012" + }, + "server": { + "answer_id": "n9", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:09:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n9", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": null, + "code": "bad_retraction", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": null, + "note": null, + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:09:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000012-0000-4000-8000-000000000012" + }, + "card": null, + "counted_answer_id": "n1" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n9", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": null, + "code": "bad_retraction", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": null, + "note": null, + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:09:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000012-0000-4000-8000-000000000012" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964A26E31A7616E737765727392DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E39A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A752656675736564A26F66C0A87265747261637473C0A4636F6465AE6261645F72657472616374696F6EAC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365C0A46E6F7465C0AB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30393A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303031322D303030302D343030302D383030302D303030303030303030303132" + }, + { + "name": "V8_retracts_on_a_plain_answer_is_bad", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": "n1" + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "yes", + "note": "On reflection, share it." + }, + "retracted": false, + "retracts": "n1", + "answered_at": "2026-11-12T19:06:00Z", + "request_id": "00000002-0000-4000-8000-000000000002" + }, + "server": { + "answer_id": "n9", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:09:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n9", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": "n1", + "code": "bad_retraction", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "yes", + "note": "On reflection, share it.", + "answered_at": "2026-11-12T19:06:00Z", + "received_at": "2026-11-12T19:09:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000002-0000-4000-8000-000000000002" + }, + "card": null, + "counted_answer_id": "n1" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n9", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": "n1", + "code": "bad_retraction", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "yes", + "note": "On reflection, share it.", + "answered_at": "2026-11-12T19:06:00Z", + "received_at": "2026-11-12T19:09:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000002-0000-4000-8000-000000000002" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964A26E31A7616E737765727392DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E39A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A752656675736564A26F66C0A87265747261637473A26E31A4636F6465AE6261645F72657472616374696F6EAC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A3796573A46E6F7465B84F6E207265666C656374696F6E2C2073686172652069742EAB616E7377657265645F6174B4323032362D31312D31325431393A30363A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30393A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030322D303030302D343030302D383030302D303030303030303030303032" + }, + { + "name": "V8_retraction_naming_an_unknown_answer_is_bad", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": "n1" + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": null, + "retracted": true, + "retracts": "n99", + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000013-0000-4000-8000-000000000013" + }, + "server": { + "answer_id": "n9", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:09:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n9", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": "n99", + "code": "bad_retraction", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": null, + "note": null, + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:09:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000013-0000-4000-8000-000000000013" + }, + "card": null, + "counted_answer_id": "n1" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n9", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": "n99", + "code": "bad_retraction", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": null, + "note": null, + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:09:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000013-0000-4000-8000-000000000013" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964A26E31A7616E737765727392DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E39A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A752656675736564A26F66C0A87265747261637473A36E3939A4636F6465AE6261645F72657472616374696F6EAC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365C0A46E6F7465C0AB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30393A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303031332D303030302D343030302D383030302D303030303030303030303133" + }, + { + "name": "V8_retraction_naming_a_Refused_answer_is_bad", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "14b8ffbdb61106dbc06f69f3fc8e2072af7a205d766e4cc040e89733013d54dc", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000014-0000-4000-8000-000000000014" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": null, + "code": "stale_fork", + "options_hash": "14b8ffbdb61106dbc06f69f3fc8e2072af7a205d766e4cc040e89733013d54dc", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000014-0000-4000-8000-000000000014" + }, + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "counted_answer_id": null + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": null, + "retracted": true, + "retracts": "n1", + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000015-0000-4000-8000-000000000015" + }, + "server": { + "answer_id": "n9", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:09:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n9", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": "n1", + "code": "bad_retraction", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": null, + "note": null, + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:09:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000015-0000-4000-8000-000000000015" + }, + "card": null, + "counted_answer_id": null + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": null, + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": null, + "code": "stale_fork", + "options_hash": "14b8ffbdb61106dbc06f69f3fc8e2072af7a205d766e4cc040e89733013d54dc", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000014-0000-4000-8000-000000000014" + }, + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n9", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": "n1", + "code": "bad_retraction", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": null, + "note": null, + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:09:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000015-0000-4000-8000-000000000015" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964C0A7616E737765727392DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A752656675736564A26F66C0A87265747261637473C0A4636F6465AA7374616C655F666F726BAC6F7074696F6E735F68617368D94031346238666662646236313130366462633036663639663366633865323037326166376132303564373636653463633034306538393733333031336435346463A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303031342D303030302D343030302D383030302D303030303030303030303134DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E39A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A752656675736564A26F66C0A87265747261637473A26E31A4636F6465AE6261645F72657472616374696F6EAC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365C0A46E6F7465C0AB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30393A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303031352D303030302D343030302D383030302D303030303030303030303135" + }, + { + "name": "V8_retracting_twice_is_already_retracted", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": "n1" + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": null, + "retracted": true, + "retracts": "n1", + "answered_at": "2026-11-12T19:09:00Z", + "request_id": "00000016-0000-4000-8000-000000000016" + }, + "server": { + "answer_id": "n8", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:09:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n8", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Retraction", + "of": null, + "retracts": "n1", + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": null, + "note": null, + "answered_at": "2026-11-12T19:09:00Z", + "received_at": "2026-11-12T19:09:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000016-0000-4000-8000-000000000016" + }, + "card": null, + "counted_answer_id": null + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": null, + "retracted": true, + "retracts": "n1", + "answered_at": "2026-11-12T19:09:00Z", + "request_id": "00000017-0000-4000-8000-000000000017" + }, + "server": { + "answer_id": "n9", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:10:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n9", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": "n1", + "code": "already_retracted", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": null, + "note": null, + "answered_at": "2026-11-12T19:09:00Z", + "received_at": "2026-11-12T19:10:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000017-0000-4000-8000-000000000017" + }, + "card": null, + "counted_answer_id": null + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": null, + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n8", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Retraction", + "of": null, + "retracts": "n1", + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": null, + "note": null, + "answered_at": "2026-11-12T19:09:00Z", + "received_at": "2026-11-12T19:09:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000016-0000-4000-8000-000000000016" + }, + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n9", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": "n1", + "code": "already_retracted", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": null, + "note": null, + "answered_at": "2026-11-12T19:09:00Z", + "received_at": "2026-11-12T19:10:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000017-0000-4000-8000-000000000017" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964C0A7616E737765727393DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E38A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65AA52657472616374696F6EA26F66C0A87265747261637473A26E31A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365C0A46E6F7465C0AB616E7377657265645F6174B4323032362D31312D31325431393A30393A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30393A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303031362D303030302D343030302D383030302D303030303030303030303136DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E39A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A752656675736564A26F66C0A87265747261637473A26E31A4636F6465B1616C72656164795F726574726163746564AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365C0A46E6F7465C0AB616E7377657265645F6174B4323032362D31312D31325431393A30393A30305AAB72656365697665645F6174B4323032362D31312D31325431393A31303A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303031372D303030302D343030302D383030302D303030303030303030303137" + }, + { + "name": "V8_retraction_under_a_stale_hash_is_stale_fork", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": "n1" + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "14b8ffbdb61106dbc06f69f3fc8e2072af7a205d766e4cc040e89733013d54dc", + "answer": null, + "retracted": true, + "retracts": "n1", + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000018-0000-4000-8000-000000000018" + }, + "server": { + "answer_id": "n9", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:09:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n9", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": "n1", + "code": "stale_fork", + "options_hash": "14b8ffbdb61106dbc06f69f3fc8e2072af7a205d766e4cc040e89733013d54dc", + "choice": null, + "note": null, + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:09:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000018-0000-4000-8000-000000000018" + }, + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "counted_answer_id": "n1" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n9", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": "n1", + "code": "stale_fork", + "options_hash": "14b8ffbdb61106dbc06f69f3fc8e2072af7a205d766e4cc040e89733013d54dc", + "choice": null, + "note": null, + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:09:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000018-0000-4000-8000-000000000018" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964A26E31A7616E737765727392DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E39A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A752656675736564A26F66C0A87265747261637473A26E31A4636F6465AA7374616C655F666F726BAC6F7074696F6E735F68617368D94031346238666662646236313130366462633036663639663366633865323037326166376132303564373636653463633034306538393733333031336435346463A663686F696365C0A46E6F7465C0AB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30393A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303031382D303030302D343030302D383030302D303030303030303030303138" + }, + { + "name": "V9_another_decider_retracts_and_the_answer_stops_counting", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "9f8e7d6c-5b4a-4392-8170-0f1e2d3c4b5a" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": "n1" + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": null, + "retracted": true, + "retracts": "n1", + "answered_at": "2026-11-12T19:09:00Z", + "request_id": "00000019-0000-4000-8000-000000000019" + }, + "server": { + "answer_id": "n7", + "answerer": "9f8e7d6c-5b4a-4392-8170-0f1e2d3c4b5a", + "authority": "Steward", + "received_at": "2026-11-12T19:09:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n7", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Retraction", + "of": null, + "retracts": "n1", + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": null, + "note": null, + "answered_at": "2026-11-12T19:09:00Z", + "received_at": "2026-11-12T19:09:05Z", + "claim_in_future": false, + "answerer": "9f8e7d6c-5b4a-4392-8170-0f1e2d3c4b5a", + "authority": "Steward", + "request_id": "00000019-0000-4000-8000-000000000019" + }, + "card": null, + "counted_answer_id": null + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": null, + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n7", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Retraction", + "of": null, + "retracts": "n1", + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": null, + "note": null, + "answered_at": "2026-11-12T19:09:00Z", + "received_at": "2026-11-12T19:09:05Z", + "claim_in_future": false, + "answerer": "9f8e7d6c-5b4a-4392-8170-0f1e2d3c4b5a", + "authority": "Steward", + "request_id": "00000019-0000-4000-8000-000000000019" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964C0A7616E737765727392DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E37A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65AA52657472616374696F6EA26F66C0A87265747261637473A26E31A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365C0A46E6F7465C0AB616E7377657265645F6174B4323032362D31312D31325431393A30393A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30393A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92439663865376436632D356234612D343339322D383137302D306631653264336334623561A9617574686F72697479A753746577617264AA726571756573745F6964D92430303030303031392D303030302D343030302D383030302D303030303030303030303139" + }, + { + "name": "V9_an_account_off_the_list_cannot_retract", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": "n1" + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": null, + "retracted": true, + "retracts": "n1", + "answered_at": "2026-11-12T19:09:00Z", + "request_id": "0000001a-0000-4000-8000-00000000001a" + }, + "server": { + "answer_id": "n6", + "answerer": "11111111-2222-4333-8444-555555555555", + "authority": "Member", + "received_at": "2026-11-12T19:09:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n6", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": "n1", + "code": "not_a_decider", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": null, + "note": null, + "answered_at": "2026-11-12T19:09:00Z", + "received_at": "2026-11-12T19:09:05Z", + "claim_in_future": false, + "answerer": "11111111-2222-4333-8444-555555555555", + "authority": "Member", + "request_id": "0000001a-0000-4000-8000-00000000001a" + }, + "card": null, + "counted_answer_id": "n1" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n6", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": "n1", + "code": "not_a_decider", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": null, + "note": null, + "answered_at": "2026-11-12T19:09:00Z", + "received_at": "2026-11-12T19:09:05Z", + "claim_in_future": false, + "answerer": "11111111-2222-4333-8444-555555555555", + "authority": "Member", + "request_id": "0000001a-0000-4000-8000-00000000001a" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964A26E31A7616E737765727392DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E36A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A752656675736564A26F66C0A87265747261637473A26E31A4636F6465AD6E6F745F615F64656369646572AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365C0A46E6F7465C0AB616E7377657265645F6174B4323032362D31312D31325431393A30393A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30393A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92431313131313131312D323232322D343333332D383434342D353535353535353535353535A9617574686F72697479A64D656D626572AA726571756573745F6964D92430303030303031612D303030302D343030302D383030302D303030303030303030303161" + }, + { + "name": "V10a_a_ruling_that_closed_the_fork_makes_a_retraction_fork_closed", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": "n1" + } + }, + { + "kind": "card", + "body": null, + "server": null, + "response": null, + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "closed", + "ruling": "cultlib-gaps:resolution:question.slot-mark-consent.n1", + "ruled_from": [ + "n1" + ], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": null, + "retracted": true, + "retracts": "n1", + "answered_at": "2026-11-12T19:09:00Z", + "request_id": "0000001b-0000-4000-8000-00000000001b" + }, + "server": { + "answer_id": "n9", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:09:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n9", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": "n1", + "code": "fork_closed", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": null, + "note": null, + "answered_at": "2026-11-12T19:09:00Z", + "received_at": "2026-11-12T19:09:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "0000001b-0000-4000-8000-00000000001b" + }, + "card": null, + "counted_answer_id": "n1" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n9", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": "n1", + "code": "fork_closed", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": null, + "note": null, + "answered_at": "2026-11-12T19:09:00Z", + "received_at": "2026-11-12T19:09:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "0000001b-0000-4000-8000-00000000001b" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964A26E31A7616E737765727392DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E39A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A752656675736564A26F66C0A87265747261637473A26E31A4636F6465AB666F726B5F636C6F736564AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365C0A46E6F7465C0AB616E7377657265645F6174B4323032362D31312D31325431393A30393A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30393A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303031622D303030302D343030302D383030302D303030303030303030303162" + }, + { + "name": "V11_retracting_the_counted_answer_makes_the_next_decider_answer_counted", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "9f8e7d6c-5b4a-4392-8170-0f1e2d3c4b5a" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": "n1" + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "yes", + "note": "Share it, the audience is settled." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:05:00Z", + "request_id": "00000003-0000-4000-8000-000000000003" + }, + "server": { + "answer_id": "n3", + "answerer": "9f8e7d6c-5b4a-4392-8170-0f1e2d3c4b5a", + "authority": "Steward", + "received_at": "2026-11-12T19:05:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n3", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Revision", + "of": "n1", + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "yes", + "note": "Share it, the audience is settled.", + "answered_at": "2026-11-12T19:05:00Z", + "received_at": "2026-11-12T19:05:05Z", + "claim_in_future": false, + "answerer": "9f8e7d6c-5b4a-4392-8170-0f1e2d3c4b5a", + "authority": "Steward", + "request_id": "00000003-0000-4000-8000-000000000003" + }, + "card": null, + "counted_answer_id": "n1" + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": null, + "retracted": true, + "retracts": "n1", + "answered_at": "2026-11-12T19:09:00Z", + "request_id": "0000001c-0000-4000-8000-00000000001c" + }, + "server": { + "answer_id": "n8", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:09:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n8", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Retraction", + "of": null, + "retracts": "n1", + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": null, + "note": null, + "answered_at": "2026-11-12T19:09:00Z", + "received_at": "2026-11-12T19:09:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "0000001c-0000-4000-8000-00000000001c" + }, + "card": null, + "counted_answer_id": "n3" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": "n3", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n3", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Revision", + "of": "n1", + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "yes", + "note": "Share it, the audience is settled.", + "answered_at": "2026-11-12T19:05:00Z", + "received_at": "2026-11-12T19:05:05Z", + "claim_in_future": false, + "answerer": "9f8e7d6c-5b4a-4392-8170-0f1e2d3c4b5a", + "authority": "Steward", + "request_id": "00000003-0000-4000-8000-000000000003" + }, + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n8", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Retraction", + "of": null, + "retracts": "n1", + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": null, + "note": null, + "answered_at": "2026-11-12T19:09:00Z", + "received_at": "2026-11-12T19:09:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "0000001c-0000-4000-8000-00000000001c" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964A26E33A7616E737765727393DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E33A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A85265766973696F6EA26F66A26E31A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A3796573A46E6F7465D92253686172652069742C207468652061756469656E636520697320736574746C65642EAB616E7377657265645F6174B4323032362D31312D31325431393A30353A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30353A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92439663865376436632D356234612D343339322D383137302D306631653264336334623561A9617574686F72697479A753746577617264AA726571756573745F6964D92430303030303030332D303030302D343030302D383030302D303030303030303030303033DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E38A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65AA52657472616374696F6EA26F66C0A87265747261637473A26E31A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365C0A46E6F7465C0AB616E7377657265645F6174B4323032362D31312D31325431393A30393A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30393A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303031632D303030302D343030302D383030302D303030303030303030303163" + }, + { + "name": "V11_retracting_an_answer_that_was_not_counted_changes_nothing", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "9f8e7d6c-5b4a-4392-8170-0f1e2d3c4b5a" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": "n1" + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "yes", + "note": "Share it, the audience is settled." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:05:00Z", + "request_id": "00000003-0000-4000-8000-000000000003" + }, + "server": { + "answer_id": "n3", + "answerer": "9f8e7d6c-5b4a-4392-8170-0f1e2d3c4b5a", + "authority": "Steward", + "received_at": "2026-11-12T19:05:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n3", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Revision", + "of": "n1", + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "yes", + "note": "Share it, the audience is settled.", + "answered_at": "2026-11-12T19:05:00Z", + "received_at": "2026-11-12T19:05:05Z", + "claim_in_future": false, + "answerer": "9f8e7d6c-5b4a-4392-8170-0f1e2d3c4b5a", + "authority": "Steward", + "request_id": "00000003-0000-4000-8000-000000000003" + }, + "card": null, + "counted_answer_id": "n1" + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": null, + "retracted": true, + "retracts": "n3", + "answered_at": "2026-11-12T19:09:00Z", + "request_id": "0000001d-0000-4000-8000-00000000001d" + }, + "server": { + "answer_id": "n8", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:09:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n8", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Retraction", + "of": null, + "retracts": "n3", + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": null, + "note": null, + "answered_at": "2026-11-12T19:09:00Z", + "received_at": "2026-11-12T19:09:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "0000001d-0000-4000-8000-00000000001d" + }, + "card": null, + "counted_answer_id": "n1" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n3", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Revision", + "of": "n1", + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "yes", + "note": "Share it, the audience is settled.", + "answered_at": "2026-11-12T19:05:00Z", + "received_at": "2026-11-12T19:05:05Z", + "claim_in_future": false, + "answerer": "9f8e7d6c-5b4a-4392-8170-0f1e2d3c4b5a", + "authority": "Steward", + "request_id": "00000003-0000-4000-8000-000000000003" + }, + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n8", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Retraction", + "of": null, + "retracts": "n3", + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": null, + "note": null, + "answered_at": "2026-11-12T19:09:00Z", + "received_at": "2026-11-12T19:09:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "0000001d-0000-4000-8000-00000000001d" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964A26E31A7616E737765727393DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E33A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A85265766973696F6EA26F66A26E31A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A3796573A46E6F7465D92253686172652069742C207468652061756469656E636520697320736574746C65642EAB616E7377657265645F6174B4323032362D31312D31325431393A30353A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30353A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92439663865376436632D356234612D343339322D383137302D306631653264336334623561A9617574686F72697479A753746577617264AA726571756573745F6964D92430303030303030332D303030302D343030302D383030302D303030303030303030303033DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E38A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65AA52657472616374696F6EA26F66C0A87265747261637473A26E33A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365C0A46E6F7465C0AB616E7377657265645F6174B4323032362D31312D31325431393A30393A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30393A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303031642D303030302D343030302D383030302D303030303030303030303164" + }, + { + "name": "after_every_answer_is_retracted_a_fresh_answer_is_Counted_again", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": "n1" + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": null, + "retracted": true, + "retracts": "n1", + "answered_at": "2026-11-12T19:09:00Z", + "request_id": "0000001e-0000-4000-8000-00000000001e" + }, + "server": { + "answer_id": "n8", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:09:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n8", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Retraction", + "of": null, + "retracts": "n1", + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": null, + "note": null, + "answered_at": "2026-11-12T19:09:00Z", + "received_at": "2026-11-12T19:09:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "0000001e-0000-4000-8000-00000000001e" + }, + "card": null, + "counted_answer_id": null + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "yes", + "note": "On reflection, share it." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:06:00Z", + "request_id": "00000002-0000-4000-8000-000000000002" + }, + "server": { + "answer_id": "n2", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:06:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n2", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "yes", + "note": "On reflection, share it.", + "answered_at": "2026-11-12T19:06:00Z", + "received_at": "2026-11-12T19:06:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000002-0000-4000-8000-000000000002" + }, + "card": null, + "counted_answer_id": "n2" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": "n2", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n8", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Retraction", + "of": null, + "retracts": "n1", + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": null, + "note": null, + "answered_at": "2026-11-12T19:09:00Z", + "received_at": "2026-11-12T19:09:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "0000001e-0000-4000-8000-00000000001e" + }, + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n2", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "yes", + "note": "On reflection, share it.", + "answered_at": "2026-11-12T19:06:00Z", + "received_at": "2026-11-12T19:06:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000002-0000-4000-8000-000000000002" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964A26E32A7616E737765727393DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E38A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65AA52657472616374696F6EA26F66C0A87265747261637473A26E31A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365C0A46E6F7465C0AB616E7377657265645F6174B4323032362D31312D31325431393A30393A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30393A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303031652D303030302D343030302D383030302D303030303030303030303165DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E32A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A3796573A46E6F7465B84F6E207265666C656374696F6E2C2073686172652069742EAB616E7377657265645F6174B4323032362D31312D31325431393A30363A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30363A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030322D303030302D343030302D383030302D303030303030303030303032" + }, + { + "name": "R1_a_retraction_with_an_answer_is_a_stored_bad_retraction_and_a_retry_replays_it", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": "n1" + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": null + }, + "retracted": true, + "retracts": "n1", + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "0000a001-0000-4000-8000-00000000a001" + }, + "server": { + "answer_id": "n2", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:09:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n2", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": "n1", + "code": "bad_retraction", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": null, + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:09:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "0000a001-0000-4000-8000-00000000a001" + }, + "card": null, + "counted_answer_id": "n1" + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": null + }, + "retracted": true, + "retracts": "n1", + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "0000a001-0000-4000-8000-00000000a001" + }, + "server": { + "answer_id": "n9", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:09:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n2", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": "n1", + "code": "bad_retraction", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": null, + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:09:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "0000a001-0000-4000-8000-00000000a001" + }, + "card": null, + "counted_answer_id": "n1" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n2", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": "n1", + "code": "bad_retraction", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": null, + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:09:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "0000a001-0000-4000-8000-00000000a001" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964A26E31A7616E737765727392DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E32A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A752656675736564A26F66C0A87265747261637473A26E31A4636F6465AE6261645F72657472616374696F6EAC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465C0AB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30393A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030613030312D303030302D343030302D383030302D303030303030303061303031" + }, + { + "name": "R1_a_retraction_with_no_target_is_a_stored_bad_retraction_and_a_retry_replays_it", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": "n1" + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": null, + "retracted": true, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "0000b001-0000-4000-8000-00000000b001" + }, + "server": { + "answer_id": "n2", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:09:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n2", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": null, + "code": "bad_retraction", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": null, + "note": null, + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:09:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "0000b001-0000-4000-8000-00000000b001" + }, + "card": null, + "counted_answer_id": "n1" + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": null, + "retracted": true, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "0000b001-0000-4000-8000-00000000b001" + }, + "server": { + "answer_id": "n9", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:09:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n2", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": null, + "code": "bad_retraction", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": null, + "note": null, + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:09:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "0000b001-0000-4000-8000-00000000b001" + }, + "card": null, + "counted_answer_id": "n1" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n2", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": null, + "code": "bad_retraction", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": null, + "note": null, + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:09:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "0000b001-0000-4000-8000-00000000b001" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964A26E31A7616E737765727392DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E32A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A752656675736564A26F66C0A87265747261637473C0A4636F6465AE6261645F72657472616374696F6EAC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365C0A46E6F7465C0AB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30393A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030623030312D303030302D343030302D383030302D303030303030303062303031" + }, + { + "name": "R1_a_plain_answer_with_a_target_is_a_stored_bad_retraction_and_a_retry_replays_it", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": "n1" + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": null + }, + "retracted": false, + "retracts": "n1", + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "0000a001-0000-4000-8000-00000000a001" + }, + "server": { + "answer_id": "n2", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:09:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n2", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": "n1", + "code": "bad_retraction", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": null, + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:09:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "0000a001-0000-4000-8000-00000000a001" + }, + "card": null, + "counted_answer_id": "n1" + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": null + }, + "retracted": false, + "retracts": "n1", + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "0000a001-0000-4000-8000-00000000a001" + }, + "server": { + "answer_id": "n9", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:09:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n2", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": "n1", + "code": "bad_retraction", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": null, + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:09:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "0000a001-0000-4000-8000-00000000a001" + }, + "card": null, + "counted_answer_id": "n1" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n2", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": "n1", + "code": "bad_retraction", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": null, + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:09:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "0000a001-0000-4000-8000-00000000a001" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964A26E31A7616E737765727392DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E32A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A752656675736564A26F66C0A87265747261637473A26E31A4636F6465AE6261645F72657472616374696F6EAC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465C0AB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30393A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030613030312D303030302D343030302D383030302D303030303030303061303031" + }, + { + "name": "a_replay_returns_the_counted_answer_id_current_at_the_retry_not_the_one_stored", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": "n1" + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": null, + "retracted": true, + "retracts": "n1", + "answered_at": "2026-11-12T19:09:00Z", + "request_id": "0000001f-0000-4000-8000-00000000001f" + }, + "server": { + "answer_id": "n8", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:09:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n8", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Retraction", + "of": null, + "retracts": "n1", + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": null, + "note": null, + "answered_at": "2026-11-12T19:09:00Z", + "received_at": "2026-11-12T19:09:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "0000001f-0000-4000-8000-00000000001f" + }, + "card": null, + "counted_answer_id": null + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n9", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": null + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": null, + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n8", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Retraction", + "of": null, + "retracts": "n1", + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": null, + "note": null, + "answered_at": "2026-11-12T19:09:00Z", + "received_at": "2026-11-12T19:09:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "0000001f-0000-4000-8000-00000000001f" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964C0A7616E737765727392DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E38A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65AA52657472616374696F6EA26F66C0A87265747261637473A26E31A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365C0A46E6F7465C0AB616E7377657265645F6174B4323032362D31312D31325431393A30393A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30393A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303031662D303030302D343030302D383030302D303030303030303030303166" + }, + { + "name": "R1_retracted_true_with_an_answer_and_no_target_is_a_stored_bad_retraction", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": "n1" + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": null + }, + "retracted": true, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "0000a001-0000-4000-8000-00000000a001" + }, + "server": { + "answer_id": "n2", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:09:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n2", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": null, + "code": "bad_retraction", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": null, + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:09:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "0000a001-0000-4000-8000-00000000a001" + }, + "card": null, + "counted_answer_id": "n1" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n2", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": null, + "code": "bad_retraction", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": null, + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:09:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "0000a001-0000-4000-8000-00000000a001" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964A26E31A7616E737765727392DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E32A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A752656675736564A26F66C0A87265747261637473C0A4636F6465AE6261645F72657472616374696F6EAC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465C0AB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30393A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030613030312D303030302D343030302D383030302D303030303030303061303031" + }, + { + "name": "R1_retracted_true_then_false_under_one_request_id_is_request_id_conflict", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": "n1" + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": null + }, + "retracted": true, + "retracts": "n1", + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "0000a001-0000-4000-8000-00000000a001" + }, + "server": { + "answer_id": "n2", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:09:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n2", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": "n1", + "code": "bad_retraction", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": null, + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:09:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "0000a001-0000-4000-8000-00000000a001" + }, + "card": null, + "counted_answer_id": "n1" + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": null + }, + "retracted": false, + "retracts": "n1", + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "0000a001-0000-4000-8000-00000000a001" + }, + "server": { + "answer_id": "n9", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:09:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.refusal.v0", + "code": "request_id_conflict", + "error": "request_id_conflict: this request_id was used for a different body" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n2", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": "n1", + "code": "bad_retraction", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": null, + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:09:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "0000a001-0000-4000-8000-00000000a001" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964A26E31A7616E737765727392DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E32A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A752656675736564A26F66C0A87265747261637473A26E31A4636F6465AE6261645F72657472616374696F6EAC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465C0AB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30393A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030613030312D303030302D343030302D383030302D303030303030303061303031" + }, + { + "name": "R1_retracted_false_then_true_under_one_request_id_is_request_id_conflict", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": "n1" + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": null + }, + "retracted": false, + "retracts": "n1", + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "0000a001-0000-4000-8000-00000000a001" + }, + "server": { + "answer_id": "n2", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:09:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n2", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": "n1", + "code": "bad_retraction", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": null, + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:09:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "0000a001-0000-4000-8000-00000000a001" + }, + "card": null, + "counted_answer_id": "n1" + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": null + }, + "retracted": true, + "retracts": "n1", + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "0000a001-0000-4000-8000-00000000a001" + }, + "server": { + "answer_id": "n9", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:09:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.refusal.v0", + "code": "request_id_conflict", + "error": "request_id_conflict: this request_id was used for a different body" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n2", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": "n1", + "code": "bad_retraction", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": null, + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:09:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "0000a001-0000-4000-8000-00000000a001" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964A26E31A7616E737765727392DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E32A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A752656675736564A26F66C0A87265747261637473A26E31A4636F6465AE6261645F72657472616374696F6EAC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465C0AB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30393A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030613030312D303030302D343030302D383030302D303030303030303061303031" + }, + { + "name": "R1_a_target_less_retraction_with_retracted_flipped_is_body_invalid_before_any_request_id_lookup", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "title": "Slot mark consent", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48" + }, + "optionsHash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "steps": [ + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": { + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "server": { + "answer_id": "n1", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:02:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + "card": null, + "counted_answer_id": "n1" + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": null, + "retracted": true, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "0000b001-0000-4000-8000-00000000b001" + }, + "server": { + "answer_id": "n2", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:09:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.answer_result.v0", + "answer": { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n2", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": null, + "code": "bad_retraction", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": null, + "note": null, + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:09:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "0000b001-0000-4000-8000-00000000b001" + }, + "card": null, + "counted_answer_id": "n1" + } + }, + { + "kind": "answer", + "body": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "answer": null, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "0000b001-0000-4000-8000-00000000b001" + }, + "server": { + "answer_id": "n9", + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "received_at": "2026-11-12T19:09:05Z" + }, + "response": { + "schemaVersion": "gamecult.fork.refusal.v0", + "code": "body_invalid", + "error": "body_invalid: the body does not satisfy gamecult.fork.answer_request.v0" + } + } + ], + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000001-0000-4000-8000-000000000001" + }, + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n2", + "fork_id": "gamecult-chat:question:slot-mark-consent", + "outcome": "Refused", + "of": null, + "retracts": null, + "code": "bad_retraction", + "options_hash": "7d95fc87d532420611fe415c1def27a3447617bb68235f46e3af130aea361c48", + "choice": null, + "note": null, + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:09:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "0000b001-0000-4000-8000-00000000b001" + } + ] + }, + "recordHex": "85AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438B1636F756E7465645F616E737765725F6964A26E31A7616E737765727392DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303030312D303030302D343030302D383030302D303030303030303030303031DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E32A7666F726B5F6964D92867616D6563756C742D636861743A7175657374696F6E3A736C6F742D6D61726B2D636F6E73656E74A76F7574636F6D65A752656675736564A26F66C0A87265747261637473C0A4636F6465AE6261645F72657472616374696F6EAC6F7074696F6E735F68617368D94037643935666338376435333234323036313166653431356331646566323761333434373631376262363832333566343665336166313330616561333631633438A663686F696365C0A46E6F7465C0AB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30393A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030623030312D303030302D343030302D383030302D303030303030303062303031" + } + ] +} diff --git a/tests/Bifrost.Web.Tests/HostTrustTests.cs b/tests/Bifrost.Web.Tests/HostTrustTests.cs index 54c0128..48902ee 100644 --- a/tests/Bifrost.Web.Tests/HostTrustTests.cs +++ b/tests/Bifrost.Web.Tests/HostTrustTests.cs @@ -362,7 +362,8 @@ public sealed class HostTrustTests : IClassFixture MintPermits = mintPermits, MintWindow = mintWindow ?? TimeSpan.FromHours(1), MaxTrackedKeys = maxTrackedKeys - })).Validate(); + }), + Options.Create(new DeskOptions())).Validate(); } private sealed class StubHostEnvironment(string environmentName) : IHostEnvironment diff --git a/tests/Bifrost.Web.Tests/Support/DeskReplay.cs b/tests/Bifrost.Web.Tests/Support/DeskReplay.cs new file mode 100644 index 0000000..92eba39 --- /dev/null +++ b/tests/Bifrost.Web.Tests/Support/DeskReplay.cs @@ -0,0 +1,75 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using Bifrost.Web.Features.Desk; + +namespace Bifrost.Web.Tests.Support; + +// Shared plumbing for replaying CultLib's transport vectors through HTTP. +public static class DeskReplay +{ + public const string ChatFork = "gamecult-chat:question:slot-mark-consent"; + + private static JsonNode BaseCard => DeskVectors.Root["scenarios"]![0]!["card"]!; + + public static ForkCard Card(string forkId, string title = "Slot mark consent") + { + var card = JsonNode.Parse(BaseCard.ToJsonString())!.AsObject(); + card["fork_id"] = forkId; + card["title"] = title; + return card.Deserialize(DeskJson.Options)!; + } + + public static ForkEntry Entry(ForkCard card) => new( + "gamecult.fork.entry.v0", + card, + new ForkRecord("gamecult.fork.record.v0", card.ForkId, card.OptionsHash, null, [])); + + // Every fork the vectors say each Mind holds, on that Mind's socket. + public static void HoldVectorForks(FakeDeskDoor door) + { + var forks = DeskVectors.Root["transport"]!["mindRoutes"]!["forks"]!.AsObject(); + foreach (var (mind, ids) in forks) + { + foreach (var id in ids!.AsArray()) + { + door.Forks[(DeskWebApplicationFactory.SocketOf(mind), (string)id!)] = Entry(Card((string)id!)); + } + } + } + + public static byte[] AnswerBody(string forkId, string requestId = "00000001-0000-4000-8000-000000000001") + { + var body = JsonNode.Parse(DeskVectors.Root["scenarios"]![0]!["steps"]![0]!["body"]!.ToJsonString())!.AsObject(); + body["fork_id"] = forkId; + body["request_id"] = requestId; + return DeskVectors.Bytes(body); + } + + // The raw segment as it would travel in one path segment: a literal '/' cannot, so it is sent as %2F. + public static string SegmentOnWire(string rawMind) => rawMind.Replace("/", "%2F", StringComparison.Ordinal); + + public static string Target(string route, string rawMind, string? forkId) => route switch + { + "answer" => $"/api/desk/{SegmentOnWire(rawMind)}/answers", + "list" => $"/api/desk/{SegmentOnWire(rawMind)}/forks", + "read" => $"/api/desk/{SegmentOnWire(rawMind)}/forks/{Uri.EscapeDataString(forkId!)}", + _ => throw new ArgumentOutOfRangeException(nameof(route)), + }; + + // Seeds the account the vector token stands for and returns the header value (null for no token). + public static async Task TokenAsync(DeskWebApplicationFactory factory, JsonNode? token) + { + if (token is null) + { + return null; + } + + var active = (bool)token["active"]!; + var subject = active ? DeskWebApplicationFactory.Alice : DeskWebApplicationFactory.Member; + await factory.EnsureAccountAsync(subject, active); + return factory.Token(subject, (string)token["audience"]!, token["scopes"]!.AsArray().Select(s => (string)s!).ToArray()); + } + + public static bool IsEdgeRefusal(string code) => + code is "unauthenticated" or "insufficient_scope" or "forbidden" or "body_forbidden_field" or "body_unknown_field" or "body_invalid"; +} diff --git a/tests/Bifrost.Web.Tests/Support/DeskVectors.cs b/tests/Bifrost.Web.Tests/Support/DeskVectors.cs new file mode 100644 index 0000000..ce450d4 --- /dev/null +++ b/tests/Bifrost.Web.Tests/Support/DeskVectors.cs @@ -0,0 +1,50 @@ +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace Bifrost.Web.Tests.Support; + +// CultLib's fork-card vectors, vendored at commit 89755b08b5dd4bf72ed8e5907b5ff4717e51da12. +public static class DeskVectors +{ + public const string SourceCommit = "89755b08b5dd4bf72ed8e5907b5ff4717e51da12"; + + public const string FileSha256 = "3387debd16059621202b706761519231e7ad52d0d4bd071a32b80c0d916e42fa"; + + public static string Path => System.IO.Path.Combine(AppContext.BaseDirectory, "Fixtures", "fork-card-vectors.json"); + + public static JsonNode Root { get; } = JsonNode.Parse(File.ReadAllBytes(Path))!; + + public static JsonArray Array(params string[] path) + { + JsonNode node = Root; + foreach (var step in path) + { + node = node[step]!; + } + + return node.AsArray(); + } + + public static IEnumerable Rows(params string[] path) => Array(path).Select(row => row!.AsObject()); + + // True when both are the same JSON with the same keys in the same order. + public static bool SameShape(JsonNode? left, JsonNode? right) + { + switch (left, right) + { + case (null, null): + return true; + case (JsonObject a, JsonObject b): + return a.Select(p => p.Key).SequenceEqual(b.Select(p => p.Key)) && + a.All(p => SameShape(p.Value, b[p.Key])); + case (JsonArray a, JsonArray b): + return a.Count == b.Count && a.Zip(b).All(pair => SameShape(pair.First, pair.Second)); + case (JsonValue, JsonValue): + return JsonNode.DeepEquals(left, right); + default: + return false; + } + } + + public static byte[] Bytes(JsonNode node) => System.Text.Encoding.UTF8.GetBytes(node.ToJsonString()); +} diff --git a/tests/Bifrost.Web.Tests/Support/DeskWebApplicationFactory.cs b/tests/Bifrost.Web.Tests/Support/DeskWebApplicationFactory.cs new file mode 100644 index 0000000..450be51 --- /dev/null +++ b/tests/Bifrost.Web.Tests/Support/DeskWebApplicationFactory.cs @@ -0,0 +1,182 @@ +using System.Text; +using Bifrost.Web.Data; +using Bifrost.Web.Domain; +using Bifrost.Web.Features.Desk; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; + +namespace Bifrost.Web.Tests.Support; + +public sealed record DeskReply(int Status, string ContentType, byte[] Body, IHeaderDictionary Headers) +{ + public string Text => Encoding.UTF8.GetString(Body); +} + +// The app with the Desk section configured and the two ports faked. `Configuration` is the Desk section as +// key/value pairs; null leaves the Desk section out, `RegisterPorts` false leaves the ports unregistered. +public sealed class DeskWebApplicationFactory : TestWebApplicationFactory +{ + public const string Alice = "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f"; + public const string Steward = "9f8e7d6c-5b4a-4392-8170-0f1e2d3c4b5a"; + public const string Member = "11111111-2222-4333-8444-555555555555"; + public const string Origin = "https://chat.gamecult.org"; + + private readonly Dictionary? configuration; + private readonly bool registerPorts; + + public DeskWebApplicationFactory(Dictionary? configuration = null, bool configured = true, bool registerPorts = true) + { + this.configuration = configured ? configuration ?? DefaultConfiguration() : null; + this.registerPorts = registerPorts; + } + + public FakeDeskDoor Door { get; } = new(); + + public FakeDeskCredential Credential { get; } = new(); + + + public static Dictionary DefaultConfiguration() + { + var values = new Dictionary + { + ["Desk:AllowedOrigin"] = Origin, + ["Desk:RetryAfterSeconds"] = "30", + ["Desk:Attribution:Operator"] = Alice, + ["Desk:Attribution:Stewards:0"] = Steward, + }; + var i = 0; + foreach (var mind in DeskVectors.Root["transport"]!["mindRoutes"]!["minds"]!.AsObject()) + { + values[$"Desk:Minds:{i}:Id"] = mind.Key; + values[$"Desk:Minds:{i}:Socket"] = SocketOf(mind.Key); + var deciders = DecidersOf(mind.Key); + for (var d = 0; d < deciders.Length; d++) + { + values[$"Desk:Minds:{i}:Deciders:{d}"] = deciders[d]; + } + + i++; + } + + return values; + } + + // A distinct list per Mind (different members, order and length), so a door context that carries another + // Mind's list cannot pass for the route Mind's. + public static string[] DecidersOf(string mindId) => + mindId == "nibu" ? [Steward, Alice, Member] : [Alice, Steward]; + + public static string SocketOf(string mindId) => + "/run/huginn/" + (string)DeskVectors.Root["transport"]!["mindRoutes"]!["minds"]![mindId]!; + + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + base.ConfigureWebHost(builder); + if (configuration is not null) + { + foreach (var (key, value) in configuration) + { + builder.UseSetting(key, value); + } + } + + if (registerPorts) + { + builder.ConfigureServices(services => + { + services.AddSingleton(Credential); + services.AddSingleton(Door); + }); + } + } + + // Upserts the account with an Active (or Suspended) membership. + public async Task EnsureAccountAsync(string id, bool active) + { + using var scope = Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var guid = Guid.Parse(id); + var account = await db.UserAccounts.FindAsync(guid); + if (account is null) + { + account = new UserAccount { Id = guid, GitHubLogin = "desk-" + id[..8], NormalizedGitHubLogin = "desk-" + id[..8], BifrostIdentity = "desk-" + id, NormalizedBifrostIdentity = "desk-" + id }; + db.UserAccounts.Add(account); + db.Memberships.Add(new Domain.Membership { UserAccountId = guid, Status = active ? MembershipStatus.Active : MembershipStatus.Suspended }); + } + else + { + var membership = db.Memberships.Single(m => m.UserAccountId == guid); + membership.Status = active ? MembershipStatus.Active : MembershipStatus.Suspended; + } + + await db.SaveChangesAsync(); + } + + public string Token(string? subject, string? audience, params string[] scopes) => + FakeDeskCredential.Encode(subject, audience, scopes); + + // What routing would see for a raw path. Kestrel refuses a NUL or a malformed escape before routing; the + // placeholder keeps the request inside /api/desk so the handler's own judgment is what the test observes. + private static PathString RoutedPath(string rawPath) + { + var schemeEnd = rawPath.IndexOf("://", StringComparison.Ordinal); + if (schemeEnd > 0) + { + var pathStart = rawPath.IndexOf('/', schemeEnd + 3); + rawPath = pathStart < 0 ? "/" : rawPath[pathStart..]; + } + + try + { + return PathString.FromUriComponent(rawPath); + } + catch (Exception) + { + return new PathString("/api/desk/_"); + } + } + + // Sends one request with the RAW target set exactly as given (TestServer's HttpClient sets none). `path` is + // what routing sees; production Kestrel would hand over the decoded path the same way. + public async Task SendAsync( + string method, string rawTarget, byte[]? body = null, string? token = null, string? origin = null, + Action? configure = null) + { + var context = await Server.SendAsync(http => + { + http.Request.Method = method; + http.Request.Scheme = "https"; + http.Request.Host = new HostString("localhost"); + var query = rawTarget.IndexOf('?'); + var rawPath = query >= 0 ? rawTarget[..query] : rawTarget; + http.Request.Path = RoutedPath(rawPath); + http.Request.QueryString = query >= 0 ? new QueryString(rawTarget[query..]) : QueryString.Empty; + http.Features.GetRequiredFeature().RawTarget = rawTarget; + if (token is not null) + { + http.Request.Headers[FakeDeskCredential.Header] = token; + } + + if (origin is not null) + { + http.Request.Headers.Origin = origin; + } + + if (body is not null) + { + http.Request.Body = new MemoryStream(body); + http.Request.ContentLength = body.Length; + http.Request.ContentType = "application/json"; + } + + configure?.Invoke(http); + }); + + using var buffer = new MemoryStream(); + await context.Response.Body.CopyToAsync(buffer); + return new DeskReply(context.Response.StatusCode, context.Response.ContentType ?? string.Empty, buffer.ToArray(), context.Response.Headers); + } +} diff --git a/tests/Bifrost.Web.Tests/Support/FakeDeskCredential.cs b/tests/Bifrost.Web.Tests/Support/FakeDeskCredential.cs new file mode 100644 index 0000000..77910fc --- /dev/null +++ b/tests/Bifrost.Web.Tests/Support/FakeDeskCredential.cs @@ -0,0 +1,42 @@ +using Microsoft.AspNetCore.Http; +using System.Text.Json; +using Bifrost.Web.Features.Desk; + +namespace Bifrost.Web.Tests.Support; + +// A header-driven stand-in for the bearer validation port. X-Test-Desk-Token carries {sub, aud, scopes}; no header +// means no valid token. It validates nothing: the desk's own judgments (audience, scope, membership) are the subject. +public sealed class FakeDeskCredential : IDeskCredential +{ + public const string Header = "X-Test-Desk-Token"; + + public static string Encode(string? subject, string? audience, IEnumerable scopes) => + JsonSerializer.Serialize(new { sub = subject, aud = audience, scopes }); + + // The port fails (its key source is down), instead of answering. + public bool Throws { get; set; } + + // Runs when a request reaches the port, after the host's own middleware: a test breaks what comes next. + public Action? OnAuthenticate { get; set; } + + public ValueTask AuthenticateAsync(HttpContext context, CancellationToken cancellationToken) + { + if (Throws) + { + throw new InvalidOperationException("the credential port is down"); + } + + OnAuthenticate?.Invoke(context); + if (!context.Request.Headers.TryGetValue(Header, out var raw)) + { + return ValueTask.FromResult(null); + } + + using var document = JsonDocument.Parse(raw.ToString()); + var root = document.RootElement; + return ValueTask.FromResult(new DeskToken( + root.GetProperty("sub").GetString(), + root.GetProperty("aud").GetString(), + root.GetProperty("scopes").EnumerateArray().Select(s => s.GetString()!).ToArray())); + } +} diff --git a/tests/Bifrost.Web.Tests/Support/FakeDeskDoor.cs b/tests/Bifrost.Web.Tests/Support/FakeDeskDoor.cs new file mode 100644 index 0000000..3211adf --- /dev/null +++ b/tests/Bifrost.Web.Tests/Support/FakeDeskDoor.cs @@ -0,0 +1,125 @@ +using System.Text.Json.Nodes; +using Bifrost.Web.Features.Desk; + +namespace Bifrost.Web.Tests.Support; + +public sealed record DeskDoorCall(string Kind, string Socket, DeskDoorContext Context, byte[]? Body, string? ForkId, DeskListRequest? List = null); + +// The door as a test double: it records every call, holds the forks each socket's Mind holds, and by default stores +// an answer to a held fork as Counted. A test may script the outcome or make the door fail before or after storing. +public sealed class FakeDeskDoor : IDeskDoor +{ + private readonly object gate = new(); + + public List Calls { get; } = []; + + public List StoredIn { get; } = []; + + public Dictionary<(string Socket, string ForkId), ForkEntry> Forks { get; } = []; + + public Func? AnswerScript { get; set; } + + // Every call throws (Huginn is down). + public bool Down { get; set; } + + // The answer is stored, then the door throws (a timeout after send). + public bool ThrowAfterStore { get; set; } + + public bool ThrowOnRead { get; set; } + + public Func? ReadScript { get; set; } + + public Func? ListScript { get; set; } + + public Task ListAsync( + string socket, DeskDoorContext context, DeskListRequest request, CancellationToken cancellationToken) + { + lock (gate) + { + Calls.Add(new DeskDoorCall("list", socket, context, null, null, request)); + } + + if (Down) + { + throw new IOException("huginn is down"); + } + + return Task.FromResult(ListScript?.Invoke(socket, request) + ?? new DeskListOutcome.Listed(new ForkList("gamecult.fork.list.v0", request.Mind, "s1", 0, 0, [], null))); + } + + public Task AnswerAsync( + string socket, DeskDoorContext context, ReadOnlyMemory body, CancellationToken cancellationToken) + { + var bytes = body.ToArray(); + lock (gate) + { + Calls.Add(new DeskDoorCall("answer", socket, context, bytes, null)); + } + + if (Down) + { + throw new IOException("huginn is down"); + } + + var outcome = AnswerScript?.Invoke(socket, context, bytes) ?? HeldForkOutcome(socket, context, bytes); + if (outcome is DeskAnswerOutcome.Answered) + { + lock (gate) + { + StoredIn.Add(socket); + } + } + + if (ThrowAfterStore) + { + throw new TimeoutException("the door timed out after send"); + } + + return Task.FromResult(outcome); + } + + public Task ReadAsync( + string socket, DeskDoorContext context, string forkId, CancellationToken cancellationToken) + { + lock (gate) + { + Calls.Add(new DeskDoorCall("read", socket, context, null, forkId)); + } + + if (Down || ThrowOnRead) + { + throw new IOException("huginn is down"); + } + + if (ReadScript is not null) + { + return Task.FromResult(ReadScript(socket, forkId)); + } + + return Task.FromResult(Forks.TryGetValue((socket, forkId), out var entry) + ? new DeskReadOutcome.Found(entry) + : new DeskReadOutcome.NotFound(DeskRefusals.ForkUnknown)); + } + + private DeskAnswerOutcome HeldForkOutcome(string socket, DeskDoorContext context, byte[] body) + { + var request = JsonNode.Parse(body)!.AsObject(); + var forkId = (string)request["fork_id"]!; + if (!Forks.ContainsKey((socket, forkId))) + { + return new DeskAnswerOutcome.NotStored(DeskRefusals.ForkUnknown); + } + + var answer = request["answer"]?.AsObject(); + return new DeskAnswerOutcome.Answered(new AnswerResult( + DeskSchemas.AnswerResult, + new DeskAnswerDocument( + DeskSchemas.DeskAnswer, "n1", forkId, "Counted", null, null, null, + (string)request["options_hash"]!, (string?)answer?["choice"], (string?)answer?["note"], + (string)request["answered_at"]!, "2026-11-12T19:02:05Z", false, + context.Subject, context.Authority, (string)request["request_id"]!), + null, + "n1")); + } +} From 8385eca00f0c8e218ae87bfe4f6f3c57a07201ce Mon Sep 17 00:00:00 2001 From: Heimdall Date: Wed, 11 Nov 2026 01:35:07 +0000 Subject: [PATCH 06/26] Desk: a read of an oversize fork is 422 fork_too_large (#42) --- .../Features/Desk/DeskDocuments.cs | 6 +++++- .../Features/Desk/DeskEndpoints.cs | 4 ++++ src/Bifrost.Web/Features/Desk/DeskPorts.cs | 5 ++++- tests/Bifrost.Web.Tests/DeskReadTests.cs | 20 ++++++++++++++++++- 4 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/Bifrost.Web/Features/Desk/DeskDocuments.cs b/src/Bifrost.Web/Features/Desk/DeskDocuments.cs index aaa652f..61237b7 100644 --- a/src/Bifrost.Web/Features/Desk/DeskDocuments.cs +++ b/src/Bifrost.Web/Features/Desk/DeskDocuments.cs @@ -151,7 +151,7 @@ public static class DeskRefusals [RequestIdConflict] = (409, "request_id_conflict: this request_id was used for a different body"), }; - // fork_too_large (Libby's ruling) is list-only and names its fork, so it is built here, not read from the table. + // fork_too_large (Libby's ruling) names its fork, so it is built here, not read from the table. // Not a literal const: the pinned vectors' doorRefusals do not name it yet, and the schema test pins literals to them. public static string ForkTooLarge => "fork_too_large"; @@ -164,6 +164,10 @@ public static class DeskRefusals ? $"fork_too_large: fork {forkId} (order {order}) is too large for a page; continue with cursor c{order}" : $"fork_too_large: fork {forkId} (order {order}) is too large for a page; the list has reached its end"); + // A read of an oversize fork: the same permanent refusal, naming the fork and nothing to page. + public static DeskRefusal ForkTooLargeDocument(string forkId) => new( + DeskSchemas.Refusal, ForkTooLarge, $"fork_too_large: fork {forkId} is too large to return"); + public static bool IsKnown(string code) => Table.ContainsKey(code); public static int StatusOf(string code) => Table[code].Status; diff --git a/src/Bifrost.Web/Features/Desk/DeskEndpoints.cs b/src/Bifrost.Web/Features/Desk/DeskEndpoints.cs index 7f5cfff..f97a949 100644 --- a/src/Bifrost.Web/Features/Desk/DeskEndpoints.cs +++ b/src/Bifrost.Web/Features/Desk/DeskEndpoints.cs @@ -189,6 +189,10 @@ public static class DeskEndpoints { await WriteJsonAsync(http, 200, IsDecider(context) ? entry : DeskNotes.Strip(entry)); } + else if (found is DeskReadOutcome.ForkTooLarge(var largeFork)) + { + await WriteJsonAsync(http, DeskRefusals.ForkTooLargeStatus, DeskRefusals.ForkTooLargeDocument(largeFork)); + } else if (found is DeskReadOutcome.NotFound(var readCode) && DoorMayRefuse(readCode) && readCode != DeskRefusals.RequestIdConflict) { await WriteRefusalAsync(http, ReadListCode(readCode)); diff --git a/src/Bifrost.Web/Features/Desk/DeskPorts.cs b/src/Bifrost.Web/Features/Desk/DeskPorts.cs index 84ddba4..385f1bb 100644 --- a/src/Bifrost.Web/Features/Desk/DeskPorts.cs +++ b/src/Bifrost.Web/Features/Desk/DeskPorts.cs @@ -35,6 +35,9 @@ public abstract record DeskReadOutcome public sealed record Found(ForkEntry Entry) : DeskReadOutcome; public sealed record NotFound(string Code) : DeskReadOutcome; + + // The fork is too large to return: permanent, and nothing to page, so no order or cursor. + public sealed record ForkTooLarge(string ForkId) : DeskReadOutcome; } // The list request the desk's query string stands for. Mind is the route's, resolved against the configured list. @@ -47,7 +50,7 @@ public abstract record DeskListOutcome public sealed record NotListed(string Code) : DeskListOutcome; // The next fork is too large for any page: the page holds nothing past the cursor. The client continues with - // cursor c; at Order 0 it has reached the end. List only: no other call can be refused this way. + // cursor c; at Order 0 it has reached the end. A read of such a fork has its own outcome (no order). public sealed record ForkTooLarge(string ForkId, int Order) : DeskListOutcome; } diff --git a/tests/Bifrost.Web.Tests/DeskReadTests.cs b/tests/Bifrost.Web.Tests/DeskReadTests.cs index a713433..3587c3d 100644 --- a/tests/Bifrost.Web.Tests/DeskReadTests.cs +++ b/tests/Bifrost.Web.Tests/DeskReadTests.cs @@ -150,7 +150,7 @@ public sealed class DeskReadTests } [Fact] - public async Task fork_too_large_is_list_only_elsewhere_the_door_saying_it_is_unavailable() + public async Task fork_too_large_as_a_bare_door_code_is_unavailable_only_the_typed_outcomes_are_the_refusal() { using var factory = new DeskWebApplicationFactory(); await factory.EnsureAccountAsync(DeskWebApplicationFactory.Alice, true); @@ -164,6 +164,24 @@ public sealed class DeskReadTests } } + [Fact] + public async Task A_read_of_an_oversize_fork_is_the_same_422_naming_the_fork_with_no_order_or_cursor() + { + using var factory = new DeskWebApplicationFactory(); + await factory.EnsureAccountAsync(DeskWebApplicationFactory.Alice, true); + factory.Door.ReadScript = (_, id) => new DeskReadOutcome.ForkTooLarge(id); + + var reply = await factory.SendAsync("GET", DeskReplay.Target("read", "nibu", "nibu:question:huge"), null, ReadToken(factory)); + + var doc = JsonNode.Parse(reply.Body)!; + Assert.Equal(422, reply.Status); + Assert.Equal("fork_too_large", (string?)doc["code"]); + var error = (string)doc["error"]!; + Assert.Contains("nibu:question:huge", error); + Assert.DoesNotContain("order", error); + Assert.DoesNotContain("cursor", error); + } + [Fact] public async Task A_short_page_with_a_cursor_below_its_last_entry_passes_through_unchanged() { From 815304568ff3b8933f991d31566972f22015e98f Mon Sep 17 00:00:00 2001 From: Heimdall Date: Wed, 11 Nov 2026 01:49:37 +0000 Subject: [PATCH 07/26] Desk: pin vectors to CultLib f6c9bf5f (#43) --- .../Features/Desk/DeskDocuments.cs | 20 +- tests/Bifrost.Web.Tests/DeskAnswerTests.cs | 16 + tests/Bifrost.Web.Tests/DeskDocumentTests.cs | 10 +- tests/Bifrost.Web.Tests/DeskReadTests.cs | 108 +- tests/Bifrost.Web.Tests/DeskVectorPinTests.cs | 2 +- .../Fixtures/fork-card-vectors.json | 1919 +++++++++++++++++ .../Bifrost.Web.Tests/Support/DeskVectors.cs | 22 +- 7 files changed, 2075 insertions(+), 22 deletions(-) diff --git a/src/Bifrost.Web/Features/Desk/DeskDocuments.cs b/src/Bifrost.Web/Features/Desk/DeskDocuments.cs index 61237b7..d41c9cd 100644 --- a/src/Bifrost.Web/Features/Desk/DeskDocuments.cs +++ b/src/Bifrost.Web/Features/Desk/DeskDocuments.cs @@ -4,7 +4,7 @@ using System.Text.Json.Serialization; namespace Bifrost.Web.Features.Desk; // The fork-desk documents Bifrost reads or writes, spelled as CultLib's contracts/cultnet/governance/fork-card.md -// (commit 89755b08) spells them: snake_case keys, every key written (nil where absent), declared in the +// (commit f6c9bf5f) spells them: snake_case keys, every key written (nil where absent), declared in the // contract's key order. Bifrost authors only DeskRefusal and DeskUnavailable. AnswerRequest is the client's body; // ForkCard, DeskAnswerDocument and AnswerResult are Huginn's documents, typed here so they pass through unchanged. @@ -151,22 +151,20 @@ public static class DeskRefusals [RequestIdConflict] = (409, "request_id_conflict: this request_id was used for a different body"), }; - // fork_too_large (Libby's ruling) names its fork, so it is built here, not read from the table. - // Not a literal const: the pinned vectors' doorRefusals do not name it yet, and the schema test pins literals to them. - public static string ForkTooLarge => "fork_too_large"; + // fork_too_large names its fork, so it is built here, not read from the table; a bare door code of that name is + // never a refusal. The texts are the pinned vectors' (alone is past the response budget). + public const string ForkTooLarge = "fork_too_large"; public static int ForkTooLargeStatus => 422; + // A list page whose next fork alone is past the budget: the client continues with cursor c; at order 0 + // it has reached the end. public static DeskRefusal ForkTooLargeDocument(string forkId, int order) => new( - DeskSchemas.Refusal, - ForkTooLarge, - order > 0 - ? $"fork_too_large: fork {forkId} (order {order}) is too large for a page; continue with cursor c{order}" - : $"fork_too_large: fork {forkId} (order {order}) is too large for a page; the list has reached its end"); + DeskSchemas.Refusal, ForkTooLarge, $"fork_too_large: fork {forkId} (order {order}) alone is past the response budget"); - // A read of an oversize fork: the same permanent refusal, naming the fork and nothing to page. + // A read of such a fork: nothing to page, so no order. public static DeskRefusal ForkTooLargeDocument(string forkId) => new( - DeskSchemas.Refusal, ForkTooLarge, $"fork_too_large: fork {forkId} is too large to return"); + DeskSchemas.Refusal, ForkTooLarge, $"fork_too_large: fork {forkId} alone is past the response budget"); public static bool IsKnown(string code) => Table.ContainsKey(code); diff --git a/tests/Bifrost.Web.Tests/DeskAnswerTests.cs b/tests/Bifrost.Web.Tests/DeskAnswerTests.cs index 9cf0048..c772c25 100644 --- a/tests/Bifrost.Web.Tests/DeskAnswerTests.cs +++ b/tests/Bifrost.Web.Tests/DeskAnswerTests.cs @@ -65,6 +65,22 @@ public sealed class DeskAnswerTests } } + [Fact] + public async Task A_stale_cards_read_that_is_fork_too_large_stays_unavailable_until_the_contract_says_otherwise() + { + var (factory, token) = await ArrangeAsync(); + using (factory) + { + factory.Door.AnswerScript = (_, _, _) => new DeskAnswerOutcome.Answered(Stale(DeskReplay.ChatFork)); + factory.Door.ReadScript = (_, id) => new DeskReadOutcome.ForkTooLarge(id); + + var reply = await factory.SendAsync("POST", DeskReplay.Target("answer", "gamecult-chat", null), DeskReplay.AnswerBody(DeskReplay.ChatFork), token); + + Assert.Equal(503, reply.Status); + Assert.Equal("unavailable", (string?)JsonNode.Parse(reply.Body)!["code"]); + } + } + [Fact] public async Task Card_read_failure_is_unavailable() { diff --git a/tests/Bifrost.Web.Tests/DeskDocumentTests.cs b/tests/Bifrost.Web.Tests/DeskDocumentTests.cs index bce61c6..f97e3f6 100644 --- a/tests/Bifrost.Web.Tests/DeskDocumentTests.cs +++ b/tests/Bifrost.Web.Tests/DeskDocumentTests.cs @@ -13,10 +13,18 @@ public sealed class DeskDocumentTests public void Refusal_and_unavailable_documents_match_vectors() { var rows = DeskVectors.Rows("doorRefusals").ToList(); - Assert.Equal(9, rows.Count); + Assert.Equal(10, rows.Count); foreach (var row in rows) { var code = (string)row["message"]!["code"]!; + if (code == DeskRefusals.ForkTooLarge) + { + // It names its fork, so it is built, not looked up; the row is the list form at order 3. + Assert.True(DeskVectors.SameRefusal(Write(DeskRefusals.ForkTooLargeDocument("gamecult-chat:question:q3", 3)), row["message"], "gamecult-chat:question:q3", 3), $"refusal {code}"); + Assert.Equal((int)row["httpStatus"]!, DeskRefusals.ForkTooLargeStatus); + continue; + } + Assert.True(DeskVectors.SameShape(row["message"], Write(DeskRefusals.Document(code))), $"refusal {code}"); Assert.Equal((int)row["httpStatus"]!, DeskRefusals.StatusOf(code)); } diff --git a/tests/Bifrost.Web.Tests/DeskReadTests.cs b/tests/Bifrost.Web.Tests/DeskReadTests.cs index 3587c3d..14e4af9 100644 --- a/tests/Bifrost.Web.Tests/DeskReadTests.cs +++ b/tests/Bifrost.Web.Tests/DeskReadTests.cs @@ -102,7 +102,7 @@ public sealed class DeskReadTests } } - Assert.Equal(7, steps); + Assert.Equal(9, steps); } [Fact] @@ -123,7 +123,7 @@ public sealed class DeskReadTests } [Fact] - public async Task A_fork_too_large_for_a_page_is_a_422_that_names_the_fork_and_the_cursor_to_continue_with() + public async Task A_fork_too_large_for_a_page_is_a_422_that_names_the_fork_and_its_order() { using var factory = new DeskWebApplicationFactory(); await factory.EnsureAccountAsync(DeskWebApplicationFactory.Alice, true); @@ -136,17 +136,14 @@ public sealed class DeskReadTests var error = (string)doc["error"]!; Assert.Contains("nibu:question:huge", error); Assert.Contains("order 7", error); - Assert.Contains("c7", error); - // At order 0 there is no cursor to continue with: the list has reached its end. + // At order 0 the text still names the fork and its order: the client reads "below 0" as the end. factory.Door.ListScript = (_, _) => new DeskListOutcome.ForkTooLarge("nibu:question:first", 0); var end = await factory.SendAsync("GET", "/api/desk/nibu/forks", null, ReadToken(factory)); var endError = (string)JsonNode.Parse(end.Body)!["error"]!; Assert.Equal(422, end.Status); Assert.Contains("nibu:question:first", endError); Assert.Contains("order 0", endError); - Assert.DoesNotContain("cursor c0", endError); - Assert.Contains("end", endError); } [Fact] @@ -182,6 +179,105 @@ public sealed class DeskReadTests Assert.DoesNotContain("cursor", error); } + [Fact] + public async Task List_short_pages_and_oversize_forks_follow_the_vectors() + { + var steps = 0; + foreach (var scenario in DeskVectors.Rows("transport", "listShortPages")) + { + using var factory = new DeskWebApplicationFactory(); + await factory.EnsureAccountAsync(DeskWebApplicationFactory.Alice, true); + foreach (var step in scenario["steps"]!.AsArray().Select(s => s!.AsObject())) + { + var request = step["request"]!; + var name = (string)scenario["name"]!; + if (step["refusal"] is { } refusal) + { + factory.Door.ListScript = (_, _) => new DeskListOutcome.ForkTooLarge((string)step["fork_id"]!, (int)step["order"]!); + } + else + { + var list = step["response"]!.Deserialize(DeskJson.Options)!; + factory.Door.ListScript = (_, _) => new DeskListOutcome.Listed(list); + } + + var query = $"?status={(string?)request["status"]}&cursor={(string?)request["cursor"]}&limit={(int)request["limit"]!}"; + var reply = await factory.SendAsync("GET", $"/api/desk/{(string)request["mind"]!}/forks" + query, null, ReadToken(factory)); + + Assert.Equal(step["refusal"] is null ? 200 : (int)step["httpStatus"]!, reply.Status); + Assert.True( + step["refusal"] is null + ? DeskVectors.SameShape(JsonNode.Parse(reply.Body), step["response"]) + : DeskVectors.SameRefusal(JsonNode.Parse(reply.Body), step["refusal"], (string)step["fork_id"]!, (int)step["order"]!), + name); + steps++; + } + } + + Assert.Equal(6, steps); + } + + [Fact] + public async Task A_read_past_the_cap_follows_the_vectors() + { + using var factory = new DeskWebApplicationFactory(); + await factory.EnsureAccountAsync(DeskWebApplicationFactory.Alice, true); + + var rows = DeskVectors.Rows("transport", "readTooLarge").ToList(); + Assert.Single(rows); + foreach (var row in rows) + { + var forkId = (string)row["fork_id"]!; + factory.Door.ReadScript = (_, id) => new DeskReadOutcome.ForkTooLarge(id); + var reply = await factory.SendAsync("GET", DeskReplay.Target("read", (string)row["request"]!["mind"]!, forkId), null, ReadToken(factory)); + + Assert.Equal((int)row["httpStatus"]!, reply.Status); + Assert.True(DeskVectors.SameRefusal(JsonNode.Parse(reply.Body), row["refusal"], forkId, null), (string)row["name"]!); + } + } + + [Fact] + public async Task Door_faults_follow_the_vectors_per_route() + { + using var factory = new DeskWebApplicationFactory(); + await factory.EnsureAccountAsync(DeskWebApplicationFactory.Alice, true); + + var faults = DeskVectors.Rows("transport", "doorFaults").ToList(); + Assert.Equal(3, faults.Count); + foreach (var row in faults) + { + var code = (string)row["doorCode"]!; + string method = "GET", target; + string? bearer = null; + byte[]? body = null; + switch ((string)row["route"]!) + { + case "read": + factory.Door.ReadScript = (_, _) => new DeskReadOutcome.NotFound(code); + target = DeskReplay.Target("read", "nibu", "nibu:question:glow-order"); + bearer = ReadToken(factory); + break; + case "list": + factory.Door.ListScript = (_, _) => new DeskListOutcome.NotListed(code); + target = "/api/desk/nibu/forks"; + bearer = ReadToken(factory); + break; + default: + method = "POST"; + factory.Door.AnswerScript = (_, _, _) => new DeskAnswerOutcome.NotStored(code); + target = DeskReplay.Target("answer", "nibu", null); + body = DeskReplay.AnswerBody("nibu:question:glow-order"); + bearer = factory.Token(DeskWebApplicationFactory.Alice, "bifrost-desk", "desk.answer"); + break; + } + + var reply = await factory.SendAsync(method, target, body, bearer); + + Assert.Equal((int)row["httpStatus"]!, reply.Status); + Assert.True(DeskVectors.SameShape(JsonNode.Parse(reply.Body), row["response"]), (string)row["name"]!); + } + } + [Fact] public async Task A_short_page_with_a_cursor_below_its_last_entry_passes_through_unchanged() { diff --git a/tests/Bifrost.Web.Tests/DeskVectorPinTests.cs b/tests/Bifrost.Web.Tests/DeskVectorPinTests.cs index 161f7cb..e14a79f 100644 --- a/tests/Bifrost.Web.Tests/DeskVectorPinTests.cs +++ b/tests/Bifrost.Web.Tests/DeskVectorPinTests.cs @@ -6,7 +6,7 @@ namespace Bifrost.Web.Tests; public sealed class DeskVectorPinTests { [Fact] - public void Vectors_are_cultlib_89755b08() + public void Vectors_are_cultlib_f6c9bf5f() { var sha = Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(DeskVectors.Path))).ToLowerInvariant(); diff --git a/tests/Bifrost.Web.Tests/Fixtures/fork-card-vectors.json b/tests/Bifrost.Web.Tests/Fixtures/fork-card-vectors.json index ea738e8..5f5e7c2 100644 --- a/tests/Bifrost.Web.Tests/Fixtures/fork-card-vectors.json +++ b/tests/Bifrost.Web.Tests/Fixtures/fork-card-vectors.json @@ -2692,6 +2692,16 @@ "error": "request_invalid: the request does not satisfy its document" }, "messagePackHex": "83AD736368656D6156657273696F6EB867616D6563756C742E666F726B2E7265667573616C2E7630A4636F6465AF726571756573745F696E76616C6964A56572726F72D93A726571756573745F696E76616C69643A20746865207265717565737420646F6573206E6F7420736174697366792069747320646F63756D656E74" + }, + { + "name": "fork_too_large", + "httpStatus": 422, + "message": { + "schemaVersion": "gamecult.fork.refusal.v0", + "code": "fork_too_large", + "error": "fork_too_large: fork gamecult-chat:question:q3 (order 3) alone is past the response budget" + }, + "messagePackHex": "83AD736368656D6156657273696F6EB867616D6563756C742E666F726B2E7265667573616C2E7630A4636F6465AE666F726B5F746F6F5F6C61726765A56572726F72D95A666F726B5F746F6F5F6C617267653A20666F726B2067616D6563756C742D636861743A7175657374696F6E3A713320286F7264657220332920616C6F6E6520697320706173742074686520726573706F6E736520627564676574" } ], "unavailable": [ @@ -3974,6 +3984,344 @@ "next_cursor": "c4" }, "messagePackHex": "87AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E6C6973742E7630A46D696E64AD67616D6563756C742D63686174A57374616D70A473343132AA6F70656E5F636F756E74FFAE7265636F726465645F636F756E7402A5666F726B739283AD736368656D6156657273696F6EB667616D6563756C742E666F726B2E656E7472792E7630A463617264DE0010AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E636172642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7135A57469746C65A6466F726B2035A7636F6E74657874BD53686F756C64206120736C6F74206D61726B206265207368617265643FA76F7074696F6E739282A26964A3796573A474657874A953686172652069742E82A26964A26E6FA474657874AE4B656570206974206C6F63616C2EAE7265636F6D6D656E646174696F6EA26E6FA7646570656E647391D92967616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365A56C65616E73C0A4666C6167C0A97261697365645F696E82A46B696E64A743757453706563A26964D92B63756C746C69622D676170733A6375745F737065633A6375742D736C6F742D746F6C6572616E63652E7234A8646563696465727391D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A6737461747573A46F70656EA672756C696E67C0AA72756C65645F66726F6D90A56F7264657205AC6F7074696F6E735F68617368D94031366432306136646334346236636330306166643337386539663063386235316466653562303765383639653161326161393538386465316465353837666162A67265636F726485AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7135AC6F7074696F6E735F68617368D94031366432306136646334346236636330306166643337386539663063386235316466653562303765383639653161326161393538386465316465353837666162B1636F756E7465645F616E737765725F6964A26E31A7616E737765727391DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7135A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94031366432306136646334346236636330306166643337386539663063386235316466653562303765383639653161326161393538386465316465353837666162A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303032302D303030302D343030302D383030302D30303030303030303030323083AD736368656D6156657273696F6EB667616D6563756C742E666F726B2E656E7472792E7630A463617264DE0010AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E636172642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7134A57469746C65A6466F726B2034A7636F6E74657874BD53686F756C64206120736C6F74206D61726B206265207368617265643FA76F7074696F6E739282A26964A3796573A474657874A953686172652069742E82A26964A26E6FA474657874AE4B656570206974206C6F63616C2EAE7265636F6D6D656E646174696F6EA26E6FA7646570656E647391D92967616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365A56C65616E73C0A4666C6167C0A97261697365645F696E82A46B696E64A743757453706563A26964D92B63756C746C69622D676170733A6375745F737065633A6375742D736C6F742D746F6C6572616E63652E7234A8646563696465727391D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A6737461747573A46F70656EA672756C696E67C0AA72756C65645F66726F6D90A56F7264657204AC6F7074696F6E735F68617368D94061363436303432333766393036623666656339636232393238363336383564393636616266383034386632373661666132346364346631653163346337346161A67265636F726485AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7134AC6F7074696F6E735F68617368D94061363436303432333766393036623666656339636232393238363336383564393636616266383034386632373661666132346364346631653163346337346161B1636F756E7465645F616E737765725F6964C0A7616E737765727390AB6E6578745F637572736F72A26334" + }, + { + "name": "two_cards_share_one_order", + "why": "rule: order is a unique rank per Mind; the refusal names both fork ids (not expressible in the schema)", + "message": { + "schemaVersion": "gamecult.fork.list.v0", + "mind": "gamecult-chat", + "stamp": "s412", + "open_count": 4, + "recorded_count": 2, + "forks": [ + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q5", + "title": "Fork 5", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 5, + "options_hash": "16d20a6dc44b6cc00afd378e9f0c8b51dfe5b07e869e1a2aa9588de1de587fab" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q5", + "options_hash": "16d20a6dc44b6cc00afd378e9f0c8b51dfe5b07e869e1a2aa9588de1de587fab", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:q5", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "16d20a6dc44b6cc00afd378e9f0c8b51dfe5b07e869e1a2aa9588de1de587fab", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000020-0000-4000-8000-000000000020" + } + ] + } + }, + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q4", + "title": "Fork 4", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 4, + "options_hash": "a64604237f906b6fec9cb292863685d966abf8048f276afa24cd4f1e1c4c74aa" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q4", + "options_hash": "a64604237f906b6fec9cb292863685d966abf8048f276afa24cd4f1e1c4c74aa", + "counted_answer_id": null, + "answers": [] + } + }, + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q4b", + "title": "Fork 4", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 4, + "options_hash": "a64604237f906b6fec9cb292863685d966abf8048f276afa24cd4f1e1c4c74aa" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q4b", + "options_hash": "a64604237f906b6fec9cb292863685d966abf8048f276afa24cd4f1e1c4c74aa", + "counted_answer_id": null, + "answers": [] + } + }, + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q3", + "title": "Fork 3", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "closed", + "ruling": "cultlib-gaps:resolution:question.q3.n1", + "ruled_from": [ + "n1" + ], + "order": 3, + "options_hash": "ced892eb663c22e3d7ece602708043ee545f56b9d4e4421d7f5bfaa86c418ce2" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q3", + "options_hash": "ced892eb663c22e3d7ece602708043ee545f56b9d4e4421d7f5bfaa86c418ce2", + "counted_answer_id": "n1", + "answers": [ + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n1", + "fork_id": "gamecult-chat:question:q3", + "outcome": "Counted", + "of": null, + "retracts": null, + "code": null, + "options_hash": "ced892eb663c22e3d7ece602708043ee545f56b9d4e4421d7f5bfaa86c418ce2", + "choice": "yes", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:02:05Z", + "claim_in_future": false, + "answerer": "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f", + "authority": "Operator", + "request_id": "00000021-0000-4000-8000-000000000021" + }, + { + "schemaVersion": "gamecult.desk_answer.v0", + "answer_id": "n2", + "fork_id": "gamecult-chat:question:q3", + "outcome": "Refused", + "of": null, + "retracts": null, + "code": "fork_closed", + "options_hash": "ced892eb663c22e3d7ece602708043ee545f56b9d4e4421d7f5bfaa86c418ce2", + "choice": "no", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act.", + "answered_at": "2026-11-12T19:02:00Z", + "received_at": "2026-11-12T19:04:05Z", + "claim_in_future": false, + "answerer": "9f8e7d6c-5b4a-4392-8170-0f1e2d3c4b5a", + "authority": "Steward", + "request_id": "00000022-0000-4000-8000-000000000022" + } + ] + } + }, + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q2", + "title": "Fork 2", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 2, + "options_hash": "5c7c2945e8e8689c6c928b11d7e0a033640b0c66afb7fcffb9e740c019d63e79" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q2", + "options_hash": "5c7c2945e8e8689c6c928b11d7e0a033640b0c66afb7fcffb9e740c019d63e79", + "counted_answer_id": null, + "answers": [] + } + }, + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q1", + "title": "Fork 1", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "closed", + "ruling": "cultlib-gaps:resolution:question.q1.n1", + "ruled_from": [], + "order": 1, + "options_hash": "61959964bd214ea1b961554c43ec46a9faf156dd547cc50c8c16bbdcd55f223a" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q1", + "options_hash": "61959964bd214ea1b961554c43ec46a9faf156dd547cc50c8c16bbdcd55f223a", + "counted_answer_id": null, + "answers": [] + } + } + ], + "next_cursor": null + }, + "messagePackHex": "87AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E6C6973742E7630A46D696E64AD67616D6563756C742D63686174A57374616D70A473343132AA6F70656E5F636F756E7404AE7265636F726465645F636F756E7402A5666F726B739683AD736368656D6156657273696F6EB667616D6563756C742E666F726B2E656E7472792E7630A463617264DE0010AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E636172642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7135A57469746C65A6466F726B2035A7636F6E74657874BD53686F756C64206120736C6F74206D61726B206265207368617265643FA76F7074696F6E739282A26964A3796573A474657874A953686172652069742E82A26964A26E6FA474657874AE4B656570206974206C6F63616C2EAE7265636F6D6D656E646174696F6EA26E6FA7646570656E647391D92967616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365A56C65616E73C0A4666C6167C0A97261697365645F696E82A46B696E64A743757453706563A26964D92B63756C746C69622D676170733A6375745F737065633A6375742D736C6F742D746F6C6572616E63652E7234A8646563696465727391D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A6737461747573A46F70656EA672756C696E67C0AA72756C65645F66726F6D90A56F7264657205AC6F7074696F6E735F68617368D94031366432306136646334346236636330306166643337386539663063386235316466653562303765383639653161326161393538386465316465353837666162A67265636F726485AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7135AC6F7074696F6E735F68617368D94031366432306136646334346236636330306166643337386539663063386235316466653562303765383639653161326161393538386465316465353837666162B1636F756E7465645F616E737765725F6964A26E31A7616E737765727391DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7135A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94031366432306136646334346236636330306166643337386539663063386235316466653562303765383639653161326161393538386465316465353837666162A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303032302D303030302D343030302D383030302D30303030303030303030323083AD736368656D6156657273696F6EB667616D6563756C742E666F726B2E656E7472792E7630A463617264DE0010AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E636172642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7134A57469746C65A6466F726B2034A7636F6E74657874BD53686F756C64206120736C6F74206D61726B206265207368617265643FA76F7074696F6E739282A26964A3796573A474657874A953686172652069742E82A26964A26E6FA474657874AE4B656570206974206C6F63616C2EAE7265636F6D6D656E646174696F6EA26E6FA7646570656E647391D92967616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365A56C65616E73C0A4666C6167C0A97261697365645F696E82A46B696E64A743757453706563A26964D92B63756C746C69622D676170733A6375745F737065633A6375742D736C6F742D746F6C6572616E63652E7234A8646563696465727391D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A6737461747573A46F70656EA672756C696E67C0AA72756C65645F66726F6D90A56F7264657204AC6F7074696F6E735F68617368D94061363436303432333766393036623666656339636232393238363336383564393636616266383034386632373661666132346364346631653163346337346161A67265636F726485AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7134AC6F7074696F6E735F68617368D94061363436303432333766393036623666656339636232393238363336383564393636616266383034386632373661666132346364346631653163346337346161B1636F756E7465645F616E737765725F6964C0A7616E73776572739083AD736368656D6156657273696F6EB667616D6563756C742E666F726B2E656E7472792E7630A463617264DE0010AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E636172642E7630A7666F726B5F6964BA67616D6563756C742D636861743A7175657374696F6E3A713462A57469746C65A6466F726B2034A7636F6E74657874BD53686F756C64206120736C6F74206D61726B206265207368617265643FA76F7074696F6E739282A26964A3796573A474657874A953686172652069742E82A26964A26E6FA474657874AE4B656570206974206C6F63616C2EAE7265636F6D6D656E646174696F6EA26E6FA7646570656E647391D92967616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365A56C65616E73C0A4666C6167C0A97261697365645F696E82A46B696E64A743757453706563A26964D92B63756C746C69622D676170733A6375745F737065633A6375742D736C6F742D746F6C6572616E63652E7234A8646563696465727391D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A6737461747573A46F70656EA672756C696E67C0AA72756C65645F66726F6D90A56F7264657204AC6F7074696F6E735F68617368D94061363436303432333766393036623666656339636232393238363336383564393636616266383034386632373661666132346364346631653163346337346161A67265636F726485AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964BA67616D6563756C742D636861743A7175657374696F6E3A713462AC6F7074696F6E735F68617368D94061363436303432333766393036623666656339636232393238363336383564393636616266383034386632373661666132346364346631653163346337346161B1636F756E7465645F616E737765725F6964C0A7616E73776572739083AD736368656D6156657273696F6EB667616D6563756C742E666F726B2E656E7472792E7630A463617264DE0010AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E636172642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7133A57469746C65A6466F726B2033A7636F6E74657874BD53686F756C64206120736C6F74206D61726B206265207368617265643FA76F7074696F6E739282A26964A3796573A474657874A953686172652069742E82A26964A26E6FA474657874AE4B656570206974206C6F63616C2EAE7265636F6D6D656E646174696F6EA26E6FA7646570656E647391D92967616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365A56C65616E73C0A4666C6167C0A97261697365645F696E82A46B696E64A743757453706563A26964D92B63756C746C69622D676170733A6375745F737065633A6375742D736C6F742D746F6C6572616E63652E7234A8646563696465727391D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A6737461747573A6636C6F736564A672756C696E67D92663756C746C69622D676170733A7265736F6C7574696F6E3A7175657374696F6E2E71332E6E31AA72756C65645F66726F6D91A26E31A56F7264657203AC6F7074696F6E735F68617368D94063656438393265623636336332326533643765636536303237303830343365653534356635366239643465343432316437663562666161383663343138636532A67265636F726485AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7133AC6F7074696F6E735F68617368D94063656438393265623636336332326533643765636536303237303830343365653534356635366239643465343432316437663562666161383663343138636532B1636F756E7465645F616E737765725F6964A26E31A7616E737765727392DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E31A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7133A76F7574636F6D65A7436F756E746564A26F66C0A87265747261637473C0A4636F6465C0AC6F7074696F6E735F68617368D94063656438393265623636336332326533643765636536303237303830343365653534356635366239643465343432316437663562666161383663343138636532A663686F696365A3796573A46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30323A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A9617574686F72697479A84F70657261746F72AA726571756573745F6964D92430303030303032312D303030302D343030302D383030302D303030303030303030303231DE0010AD736368656D6156657273696F6EB767616D6563756C742E6465736B5F616E737765722E7630A9616E737765725F6964A26E32A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7133A76F7574636F6D65A752656675736564A26F66C0A87265747261637473C0A4636F6465AB666F726B5F636C6F736564AC6F7074696F6E735F68617368D94063656438393265623636336332326533643765636536303237303830343365653534356635366239643465343432316437663562666161383663343138636532A663686F696365A26E6FA46E6F7465D9524B656570206974206C6F63616C3A206120736C6F74206D61726B2069732074686520617574686F7227732063616C6C2C20616E642073686172696E672069742069732061207365706172617465206163742EAB616E7377657265645F6174B4323032362D31312D31325431393A30323A30305AAB72656365697665645F6174B4323032362D31312D31325431393A30343A30355AAF636C61696D5F696E5F667574757265C2A8616E737765726572D92439663865376436632D356234612D343339322D383137302D306631653264336334623561A9617574686F72697479A753746577617264AA726571756573745F6964D92430303030303032322D303030302D343030302D383030302D30303030303030303030323283AD736368656D6156657273696F6EB667616D6563756C742E666F726B2E656E7472792E7630A463617264DE0010AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E636172642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7132A57469746C65A6466F726B2032A7636F6E74657874BD53686F756C64206120736C6F74206D61726B206265207368617265643FA76F7074696F6E739282A26964A3796573A474657874A953686172652069742E82A26964A26E6FA474657874AE4B656570206974206C6F63616C2EAE7265636F6D6D656E646174696F6EA26E6FA7646570656E647391D92967616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365A56C65616E73C0A4666C6167C0A97261697365645F696E82A46B696E64A743757453706563A26964D92B63756C746C69622D676170733A6375745F737065633A6375742D736C6F742D746F6C6572616E63652E7234A8646563696465727391D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A6737461747573A46F70656EA672756C696E67C0AA72756C65645F66726F6D90A56F7264657202AC6F7074696F6E735F68617368D94035633763323934356538653836383963366339323862313164376530613033333634306230633636616662376663666662396537343063303139643633653739A67265636F726485AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7132AC6F7074696F6E735F68617368D94035633763323934356538653836383963366339323862313164376530613033333634306230633636616662376663666662396537343063303139643633653739B1636F756E7465645F616E737765725F6964C0A7616E73776572739083AD736368656D6156657273696F6EB667616D6563756C742E666F726B2E656E7472792E7630A463617264DE0010AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E636172642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7131A57469746C65A6466F726B2031A7636F6E74657874BD53686F756C64206120736C6F74206D61726B206265207368617265643FA76F7074696F6E739282A26964A3796573A474657874A953686172652069742E82A26964A26E6FA474657874AE4B656570206974206C6F63616C2EAE7265636F6D6D656E646174696F6EA26E6FA7646570656E647391D92967616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365A56C65616E73C0A4666C6167C0A97261697365645F696E82A46B696E64A743757453706563A26964D92B63756C746C69622D676170733A6375745F737065633A6375742D736C6F742D746F6C6572616E63652E7234A8646563696465727391D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A6737461747573A6636C6F736564A672756C696E67D92663756C746C69622D676170733A7265736F6C7574696F6E3A7175657374696F6E2E71312E6E31AA72756C65645F66726F6D90A56F7264657201AC6F7074696F6E735F68617368D94036313935393936346264323134656131623936313535346334336563343661396661663135366464353437636335306338633136626264636435356632323361A67265636F726485AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7131AC6F7074696F6E735F68617368D94036313935393936346264323134656131623936313535346334336563343661396661663135366464353437636335306338633136626264636435356632323361B1636F756E7465645F616E737765725F6964C0A7616E737765727390AB6E6578745F637572736F72C0", + "refusal": "order 4 is shared by gamecult-chat:question:q4 and gamecult-chat:question:q4b" } ], "listScenarios": [ @@ -5796,6 +6144,406 @@ } ] }, + { + "name": "order_zero_is_legal_but_no_cursor_names_it", + "ordinal": 9, + "entries": [ + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q3", + "title": "Fork 3", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "ced892eb663c22e3d7ece602708043ee545f56b9d4e4421d7f5bfaa86c418ce2" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q3", + "options_hash": "ced892eb663c22e3d7ece602708043ee545f56b9d4e4421d7f5bfaa86c418ce2", + "counted_answer_id": null, + "answers": [] + } + }, + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q2", + "title": "Fork 2", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 2, + "options_hash": "5c7c2945e8e8689c6c928b11d7e0a033640b0c66afb7fcffb9e740c019d63e79" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q2", + "options_hash": "5c7c2945e8e8689c6c928b11d7e0a033640b0c66afb7fcffb9e740c019d63e79", + "counted_answer_id": null, + "answers": [] + } + }, + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q1", + "title": "Fork 1", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 1, + "options_hash": "61959964bd214ea1b961554c43ec46a9faf156dd547cc50c8c16bbdcd55f223a" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q1", + "options_hash": "61959964bd214ea1b961554c43ec46a9faf156dd547cc50c8c16bbdcd55f223a", + "counted_answer_id": null, + "answers": [] + } + }, + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q0", + "title": "Fork 0", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 0, + "options_hash": "1777bd06ffafdf6e81bed5979f37193c4b4e7c3d2504f904e9131012f0767a9d" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q0", + "options_hash": "1777bd06ffafdf6e81bed5979f37193c4b4e7c3d2504f904e9131012f0767a9d", + "counted_answer_id": null, + "answers": [] + } + } + ], + "steps": [ + { + "request": { + "schemaVersion": "gamecult.fork.list_request.v0", + "mind": "gamecult-chat", + "status": null, + "cursor": null, + "limit": 3 + }, + "response": { + "schemaVersion": "gamecult.fork.list.v0", + "mind": "gamecult-chat", + "stamp": "s9", + "open_count": 4, + "recorded_count": 0, + "forks": [ + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q3", + "title": "Fork 3", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "ced892eb663c22e3d7ece602708043ee545f56b9d4e4421d7f5bfaa86c418ce2" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q3", + "options_hash": "ced892eb663c22e3d7ece602708043ee545f56b9d4e4421d7f5bfaa86c418ce2", + "counted_answer_id": null, + "answers": [] + } + }, + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q2", + "title": "Fork 2", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 2, + "options_hash": "5c7c2945e8e8689c6c928b11d7e0a033640b0c66afb7fcffb9e740c019d63e79" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q2", + "options_hash": "5c7c2945e8e8689c6c928b11d7e0a033640b0c66afb7fcffb9e740c019d63e79", + "counted_answer_id": null, + "answers": [] + } + }, + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q1", + "title": "Fork 1", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 1, + "options_hash": "61959964bd214ea1b961554c43ec46a9faf156dd547cc50c8c16bbdcd55f223a" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q1", + "options_hash": "61959964bd214ea1b961554c43ec46a9faf156dd547cc50c8c16bbdcd55f223a", + "counted_answer_id": null, + "answers": [] + } + } + ], + "next_cursor": "c1" + }, + "responseHex": "87AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E6C6973742E7630A46D696E64AD67616D6563756C742D63686174A57374616D70A27339AA6F70656E5F636F756E7404AE7265636F726465645F636F756E7400A5666F726B739383AD736368656D6156657273696F6EB667616D6563756C742E666F726B2E656E7472792E7630A463617264DE0010AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E636172642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7133A57469746C65A6466F726B2033A7636F6E74657874BD53686F756C64206120736C6F74206D61726B206265207368617265643FA76F7074696F6E739282A26964A3796573A474657874A953686172652069742E82A26964A26E6FA474657874AE4B656570206974206C6F63616C2EAE7265636F6D6D656E646174696F6EA26E6FA7646570656E647391D92967616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365A56C65616E73C0A4666C6167C0A97261697365645F696E82A46B696E64A743757453706563A26964D92B63756C746C69622D676170733A6375745F737065633A6375742D736C6F742D746F6C6572616E63652E7234A8646563696465727391D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A6737461747573A46F70656EA672756C696E67C0AA72756C65645F66726F6D90A56F7264657203AC6F7074696F6E735F68617368D94063656438393265623636336332326533643765636536303237303830343365653534356635366239643465343432316437663562666161383663343138636532A67265636F726485AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7133AC6F7074696F6E735F68617368D94063656438393265623636336332326533643765636536303237303830343365653534356635366239643465343432316437663562666161383663343138636532B1636F756E7465645F616E737765725F6964C0A7616E73776572739083AD736368656D6156657273696F6EB667616D6563756C742E666F726B2E656E7472792E7630A463617264DE0010AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E636172642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7132A57469746C65A6466F726B2032A7636F6E74657874BD53686F756C64206120736C6F74206D61726B206265207368617265643FA76F7074696F6E739282A26964A3796573A474657874A953686172652069742E82A26964A26E6FA474657874AE4B656570206974206C6F63616C2EAE7265636F6D6D656E646174696F6EA26E6FA7646570656E647391D92967616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365A56C65616E73C0A4666C6167C0A97261697365645F696E82A46B696E64A743757453706563A26964D92B63756C746C69622D676170733A6375745F737065633A6375742D736C6F742D746F6C6572616E63652E7234A8646563696465727391D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A6737461747573A46F70656EA672756C696E67C0AA72756C65645F66726F6D90A56F7264657202AC6F7074696F6E735F68617368D94035633763323934356538653836383963366339323862313164376530613033333634306230633636616662376663666662396537343063303139643633653739A67265636F726485AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7132AC6F7074696F6E735F68617368D94035633763323934356538653836383963366339323862313164376530613033333634306230633636616662376663666662396537343063303139643633653739B1636F756E7465645F616E737765725F6964C0A7616E73776572739083AD736368656D6156657273696F6EB667616D6563756C742E666F726B2E656E7472792E7630A463617264DE0010AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E636172642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7131A57469746C65A6466F726B2031A7636F6E74657874BD53686F756C64206120736C6F74206D61726B206265207368617265643FA76F7074696F6E739282A26964A3796573A474657874A953686172652069742E82A26964A26E6FA474657874AE4B656570206974206C6F63616C2EAE7265636F6D6D656E646174696F6EA26E6FA7646570656E647391D92967616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365A56C65616E73C0A4666C6167C0A97261697365645F696E82A46B696E64A743757453706563A26964D92B63756C746C69622D676170733A6375745F737065633A6375742D736C6F742D746F6C6572616E63652E7234A8646563696465727391D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A6737461747573A46F70656EA672756C696E67C0AA72756C65645F66726F6D90A56F7264657201AC6F7074696F6E735F68617368D94036313935393936346264323134656131623936313535346334336563343661396661663135366464353437636335306338633136626264636435356632323361A67265636F726485AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7131AC6F7074696F6E735F68617368D94036313935393936346264323134656131623936313535346334336563343661396661663135366464353437636335306338633136626264636435356632323361B1636F756E7465645F616E737765725F6964C0A7616E737765727390AB6E6578745F637572736F72A26331" + }, + { + "request": { + "schemaVersion": "gamecult.fork.list_request.v0", + "mind": "gamecult-chat", + "status": null, + "cursor": "c1", + "limit": 3 + }, + "response": { + "schemaVersion": "gamecult.fork.list.v0", + "mind": "gamecult-chat", + "stamp": "s9", + "open_count": 4, + "recorded_count": 0, + "forks": [ + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q0", + "title": "Fork 0", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 0, + "options_hash": "1777bd06ffafdf6e81bed5979f37193c4b4e7c3d2504f904e9131012f0767a9d" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q0", + "options_hash": "1777bd06ffafdf6e81bed5979f37193c4b4e7c3d2504f904e9131012f0767a9d", + "counted_answer_id": null, + "answers": [] + } + } + ], + "next_cursor": null + }, + "responseHex": "87AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E6C6973742E7630A46D696E64AD67616D6563756C742D63686174A57374616D70A27339AA6F70656E5F636F756E7404AE7265636F726465645F636F756E7400A5666F726B739183AD736368656D6156657273696F6EB667616D6563756C742E666F726B2E656E7472792E7630A463617264DE0010AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E636172642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7130A57469746C65A6466F726B2030A7636F6E74657874BD53686F756C64206120736C6F74206D61726B206265207368617265643FA76F7074696F6E739282A26964A3796573A474657874A953686172652069742E82A26964A26E6FA474657874AE4B656570206974206C6F63616C2EAE7265636F6D6D656E646174696F6EA26E6FA7646570656E647391D92967616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365A56C65616E73C0A4666C6167C0A97261697365645F696E82A46B696E64A743757453706563A26964D92B63756C746C69622D676170733A6375745F737065633A6375742D736C6F742D746F6C6572616E63652E7234A8646563696465727391D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A6737461747573A46F70656EA672756C696E67C0AA72756C65645F66726F6D90A56F7264657200AC6F7074696F6E735F68617368D94031373737626430366666616664663665383162656435393739663337313933633462346537633364323530346639303465393133313031326630373637613964A67265636F726485AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7130AC6F7074696F6E735F68617368D94031373737626430366666616664663665383162656435393739663337313933633462346537633364323530346639303465393133313031326630373637613964B1636F756E7465645F616E737765725F6964C0A7616E737765727390AB6E6578745F637572736F72C0" + } + ] + }, { "name": "a_mind_with_no_forks", "ordinal": 7, @@ -5823,6 +6571,1177 @@ ] } ], + "listShortPages": [ + { + "name": "a_short_page_resumes_below_its_last_entry", + "ordinal": 9, + "entries": [ + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q3", + "title": "Fork 3", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "ced892eb663c22e3d7ece602708043ee545f56b9d4e4421d7f5bfaa86c418ce2" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q3", + "options_hash": "ced892eb663c22e3d7ece602708043ee545f56b9d4e4421d7f5bfaa86c418ce2", + "counted_answer_id": null, + "answers": [] + } + }, + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q2", + "title": "Fork 2", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 2, + "options_hash": "5c7c2945e8e8689c6c928b11d7e0a033640b0c66afb7fcffb9e740c019d63e79" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q2", + "options_hash": "5c7c2945e8e8689c6c928b11d7e0a033640b0c66afb7fcffb9e740c019d63e79", + "counted_answer_id": null, + "answers": [] + } + }, + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q1", + "title": "Fork 1", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 1, + "options_hash": "61959964bd214ea1b961554c43ec46a9faf156dd547cc50c8c16bbdcd55f223a" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q1", + "options_hash": "61959964bd214ea1b961554c43ec46a9faf156dd547cc50c8c16bbdcd55f223a", + "counted_answer_id": null, + "answers": [] + } + }, + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q0", + "title": "Fork 0", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 0, + "options_hash": "1777bd06ffafdf6e81bed5979f37193c4b4e7c3d2504f904e9131012f0767a9d" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q0", + "options_hash": "1777bd06ffafdf6e81bed5979f37193c4b4e7c3d2504f904e9131012f0767a9d", + "counted_answer_id": null, + "answers": [] + } + } + ], + "steps": [ + { + "request": { + "schemaVersion": "gamecult.fork.list_request.v0", + "mind": "gamecult-chat", + "status": null, + "cursor": null, + "limit": 3 + }, + "response": { + "schemaVersion": "gamecult.fork.list.v0", + "mind": "gamecult-chat", + "stamp": "s9", + "open_count": 4, + "recorded_count": 0, + "forks": [ + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q3", + "title": "Fork 3", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "ced892eb663c22e3d7ece602708043ee545f56b9d4e4421d7f5bfaa86c418ce2" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q3", + "options_hash": "ced892eb663c22e3d7ece602708043ee545f56b9d4e4421d7f5bfaa86c418ce2", + "counted_answer_id": null, + "answers": [] + } + }, + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q2", + "title": "Fork 2", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 2, + "options_hash": "5c7c2945e8e8689c6c928b11d7e0a033640b0c66afb7fcffb9e740c019d63e79" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q2", + "options_hash": "5c7c2945e8e8689c6c928b11d7e0a033640b0c66afb7fcffb9e740c019d63e79", + "counted_answer_id": null, + "answers": [] + } + } + ], + "next_cursor": "c2" + }, + "responseHex": "87AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E6C6973742E7630A46D696E64AD67616D6563756C742D63686174A57374616D70A27339AA6F70656E5F636F756E7404AE7265636F726465645F636F756E7400A5666F726B739283AD736368656D6156657273696F6EB667616D6563756C742E666F726B2E656E7472792E7630A463617264DE0010AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E636172642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7133A57469746C65A6466F726B2033A7636F6E74657874BD53686F756C64206120736C6F74206D61726B206265207368617265643FA76F7074696F6E739282A26964A3796573A474657874A953686172652069742E82A26964A26E6FA474657874AE4B656570206974206C6F63616C2EAE7265636F6D6D656E646174696F6EA26E6FA7646570656E647391D92967616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365A56C65616E73C0A4666C6167C0A97261697365645F696E82A46B696E64A743757453706563A26964D92B63756C746C69622D676170733A6375745F737065633A6375742D736C6F742D746F6C6572616E63652E7234A8646563696465727391D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A6737461747573A46F70656EA672756C696E67C0AA72756C65645F66726F6D90A56F7264657203AC6F7074696F6E735F68617368D94063656438393265623636336332326533643765636536303237303830343365653534356635366239643465343432316437663562666161383663343138636532A67265636F726485AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7133AC6F7074696F6E735F68617368D94063656438393265623636336332326533643765636536303237303830343365653534356635366239643465343432316437663562666161383663343138636532B1636F756E7465645F616E737765725F6964C0A7616E73776572739083AD736368656D6156657273696F6EB667616D6563756C742E666F726B2E656E7472792E7630A463617264DE0010AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E636172642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7132A57469746C65A6466F726B2032A7636F6E74657874BD53686F756C64206120736C6F74206D61726B206265207368617265643FA76F7074696F6E739282A26964A3796573A474657874A953686172652069742E82A26964A26E6FA474657874AE4B656570206974206C6F63616C2EAE7265636F6D6D656E646174696F6EA26E6FA7646570656E647391D92967616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365A56C65616E73C0A4666C6167C0A97261697365645F696E82A46B696E64A743757453706563A26964D92B63756C746C69622D676170733A6375745F737065633A6375742D736C6F742D746F6C6572616E63652E7234A8646563696465727391D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A6737461747573A46F70656EA672756C696E67C0AA72756C65645F66726F6D90A56F7264657202AC6F7074696F6E735F68617368D94035633763323934356538653836383963366339323862313164376530613033333634306230633636616662376663666662396537343063303139643633653739A67265636F726485AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7132AC6F7074696F6E735F68617368D94035633763323934356538653836383963366339323862313164376530613033333634306230633636616662376663666662396537343063303139643633653739B1636F756E7465645F616E737765725F6964C0A7616E737765727390AB6E6578745F637572736F72A26332" + }, + { + "request": { + "schemaVersion": "gamecult.fork.list_request.v0", + "mind": "gamecult-chat", + "status": null, + "cursor": "c2", + "limit": 3 + }, + "response": { + "schemaVersion": "gamecult.fork.list.v0", + "mind": "gamecult-chat", + "stamp": "s9", + "open_count": 4, + "recorded_count": 0, + "forks": [ + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q1", + "title": "Fork 1", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 1, + "options_hash": "61959964bd214ea1b961554c43ec46a9faf156dd547cc50c8c16bbdcd55f223a" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q1", + "options_hash": "61959964bd214ea1b961554c43ec46a9faf156dd547cc50c8c16bbdcd55f223a", + "counted_answer_id": null, + "answers": [] + } + }, + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q0", + "title": "Fork 0", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 0, + "options_hash": "1777bd06ffafdf6e81bed5979f37193c4b4e7c3d2504f904e9131012f0767a9d" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q0", + "options_hash": "1777bd06ffafdf6e81bed5979f37193c4b4e7c3d2504f904e9131012f0767a9d", + "counted_answer_id": null, + "answers": [] + } + } + ], + "next_cursor": null + }, + "responseHex": "87AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E6C6973742E7630A46D696E64AD67616D6563756C742D63686174A57374616D70A27339AA6F70656E5F636F756E7404AE7265636F726465645F636F756E7400A5666F726B739283AD736368656D6156657273696F6EB667616D6563756C742E666F726B2E656E7472792E7630A463617264DE0010AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E636172642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7131A57469746C65A6466F726B2031A7636F6E74657874BD53686F756C64206120736C6F74206D61726B206265207368617265643FA76F7074696F6E739282A26964A3796573A474657874A953686172652069742E82A26964A26E6FA474657874AE4B656570206974206C6F63616C2EAE7265636F6D6D656E646174696F6EA26E6FA7646570656E647391D92967616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365A56C65616E73C0A4666C6167C0A97261697365645F696E82A46B696E64A743757453706563A26964D92B63756C746C69622D676170733A6375745F737065633A6375742D736C6F742D746F6C6572616E63652E7234A8646563696465727391D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A6737461747573A46F70656EA672756C696E67C0AA72756C65645F66726F6D90A56F7264657201AC6F7074696F6E735F68617368D94036313935393936346264323134656131623936313535346334336563343661396661663135366464353437636335306338633136626264636435356632323361A67265636F726485AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7131AC6F7074696F6E735F68617368D94036313935393936346264323134656131623936313535346334336563343661396661663135366464353437636335306338633136626264636435356632323361B1636F756E7465645F616E737765725F6964C0A7616E73776572739083AD736368656D6156657273696F6EB667616D6563756C742E666F726B2E656E7472792E7630A463617264DE0010AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E636172642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7130A57469746C65A6466F726B2030A7636F6E74657874BD53686F756C64206120736C6F74206D61726B206265207368617265643FA76F7074696F6E739282A26964A3796573A474657874A953686172652069742E82A26964A26E6FA474657874AE4B656570206974206C6F63616C2EAE7265636F6D6D656E646174696F6EA26E6FA7646570656E647391D92967616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365A56C65616E73C0A4666C6167C0A97261697365645F696E82A46B696E64A743757453706563A26964D92B63756C746C69622D676170733A6375745F737065633A6375742D736C6F742D746F6C6572616E63652E7234A8646563696465727391D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A6737461747573A46F70656EA672756C696E67C0AA72756C65645F66726F6D90A56F7264657200AC6F7074696F6E735F68617368D94031373737626430366666616664663665383162656435393739663337313933633462346537633364323530346639303465393133313031326630373637613964A67265636F726485AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7130AC6F7074696F6E735F68617368D94031373737626430366666616664663665383162656435393739663337313933633462346537633364323530346639303465393133313031326630373637613964B1636F756E7465645F616E737765725F6964C0A7616E737765727390AB6E6578745F637572736F72C0" + } + ] + }, + { + "name": "a_first_entry_past_the_budget_is_fork_too_large_and_the_client_continues_below_its_order", + "ordinal": 9, + "entries": [ + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q3", + "title": "Fork 3", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "ced892eb663c22e3d7ece602708043ee545f56b9d4e4421d7f5bfaa86c418ce2" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q3", + "options_hash": "ced892eb663c22e3d7ece602708043ee545f56b9d4e4421d7f5bfaa86c418ce2", + "counted_answer_id": null, + "answers": [] + } + }, + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q2", + "title": "Fork 2", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 2, + "options_hash": "5c7c2945e8e8689c6c928b11d7e0a033640b0c66afb7fcffb9e740c019d63e79" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q2", + "options_hash": "5c7c2945e8e8689c6c928b11d7e0a033640b0c66afb7fcffb9e740c019d63e79", + "counted_answer_id": null, + "answers": [] + } + }, + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q1", + "title": "Fork 1", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 1, + "options_hash": "61959964bd214ea1b961554c43ec46a9faf156dd547cc50c8c16bbdcd55f223a" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q1", + "options_hash": "61959964bd214ea1b961554c43ec46a9faf156dd547cc50c8c16bbdcd55f223a", + "counted_answer_id": null, + "answers": [] + } + }, + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q0", + "title": "Fork 0", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 0, + "options_hash": "1777bd06ffafdf6e81bed5979f37193c4b4e7c3d2504f904e9131012f0767a9d" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q0", + "options_hash": "1777bd06ffafdf6e81bed5979f37193c4b4e7c3d2504f904e9131012f0767a9d", + "counted_answer_id": null, + "answers": [] + } + } + ], + "steps": [ + { + "request": { + "schemaVersion": "gamecult.fork.list_request.v0", + "mind": "gamecult-chat", + "status": null, + "cursor": null, + "limit": 3 + }, + "refusal": { + "schemaVersion": "gamecult.fork.refusal.v0", + "code": "fork_too_large", + "error": "fork_too_large: fork gamecult-chat:question:q3 (order 3) alone is past the response budget" + }, + "refusalHex": "83AD736368656D6156657273696F6EB867616D6563756C742E666F726B2E7265667573616C2E7630A4636F6465AE666F726B5F746F6F5F6C61726765A56572726F72D95A666F726B5F746F6F5F6C617267653A20666F726B2067616D6563756C742D636861743A7175657374696F6E3A713320286F7264657220332920616C6F6E6520697320706173742074686520726573706F6E736520627564676574", + "httpStatus": 422, + "fork_id": "gamecult-chat:question:q3", + "order": 3 + }, + { + "request": { + "schemaVersion": "gamecult.fork.list_request.v0", + "mind": "gamecult-chat", + "status": null, + "cursor": "c3", + "limit": 3 + }, + "response": { + "schemaVersion": "gamecult.fork.list.v0", + "mind": "gamecult-chat", + "stamp": "s9", + "open_count": 4, + "recorded_count": 0, + "forks": [ + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q2", + "title": "Fork 2", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 2, + "options_hash": "5c7c2945e8e8689c6c928b11d7e0a033640b0c66afb7fcffb9e740c019d63e79" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q2", + "options_hash": "5c7c2945e8e8689c6c928b11d7e0a033640b0c66afb7fcffb9e740c019d63e79", + "counted_answer_id": null, + "answers": [] + } + }, + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q1", + "title": "Fork 1", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 1, + "options_hash": "61959964bd214ea1b961554c43ec46a9faf156dd547cc50c8c16bbdcd55f223a" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q1", + "options_hash": "61959964bd214ea1b961554c43ec46a9faf156dd547cc50c8c16bbdcd55f223a", + "counted_answer_id": null, + "answers": [] + } + }, + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q0", + "title": "Fork 0", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 0, + "options_hash": "1777bd06ffafdf6e81bed5979f37193c4b4e7c3d2504f904e9131012f0767a9d" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q0", + "options_hash": "1777bd06ffafdf6e81bed5979f37193c4b4e7c3d2504f904e9131012f0767a9d", + "counted_answer_id": null, + "answers": [] + } + } + ], + "next_cursor": null + }, + "responseHex": "87AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E6C6973742E7630A46D696E64AD67616D6563756C742D63686174A57374616D70A27339AA6F70656E5F636F756E7404AE7265636F726465645F636F756E7400A5666F726B739383AD736368656D6156657273696F6EB667616D6563756C742E666F726B2E656E7472792E7630A463617264DE0010AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E636172642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7132A57469746C65A6466F726B2032A7636F6E74657874BD53686F756C64206120736C6F74206D61726B206265207368617265643FA76F7074696F6E739282A26964A3796573A474657874A953686172652069742E82A26964A26E6FA474657874AE4B656570206974206C6F63616C2EAE7265636F6D6D656E646174696F6EA26E6FA7646570656E647391D92967616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365A56C65616E73C0A4666C6167C0A97261697365645F696E82A46B696E64A743757453706563A26964D92B63756C746C69622D676170733A6375745F737065633A6375742D736C6F742D746F6C6572616E63652E7234A8646563696465727391D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A6737461747573A46F70656EA672756C696E67C0AA72756C65645F66726F6D90A56F7264657202AC6F7074696F6E735F68617368D94035633763323934356538653836383963366339323862313164376530613033333634306230633636616662376663666662396537343063303139643633653739A67265636F726485AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7132AC6F7074696F6E735F68617368D94035633763323934356538653836383963366339323862313164376530613033333634306230633636616662376663666662396537343063303139643633653739B1636F756E7465645F616E737765725F6964C0A7616E73776572739083AD736368656D6156657273696F6EB667616D6563756C742E666F726B2E656E7472792E7630A463617264DE0010AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E636172642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7131A57469746C65A6466F726B2031A7636F6E74657874BD53686F756C64206120736C6F74206D61726B206265207368617265643FA76F7074696F6E739282A26964A3796573A474657874A953686172652069742E82A26964A26E6FA474657874AE4B656570206974206C6F63616C2EAE7265636F6D6D656E646174696F6EA26E6FA7646570656E647391D92967616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365A56C65616E73C0A4666C6167C0A97261697365645F696E82A46B696E64A743757453706563A26964D92B63756C746C69622D676170733A6375745F737065633A6375742D736C6F742D746F6C6572616E63652E7234A8646563696465727391D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A6737461747573A46F70656EA672756C696E67C0AA72756C65645F66726F6D90A56F7264657201AC6F7074696F6E735F68617368D94036313935393936346264323134656131623936313535346334336563343661396661663135366464353437636335306338633136626264636435356632323361A67265636F726485AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7131AC6F7074696F6E735F68617368D94036313935393936346264323134656131623936313535346334336563343661396661663135366464353437636335306338633136626264636435356632323361B1636F756E7465645F616E737765725F6964C0A7616E73776572739083AD736368656D6156657273696F6EB667616D6563756C742E666F726B2E656E7472792E7630A463617264DE0010AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E636172642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7130A57469746C65A6466F726B2030A7636F6E74657874BD53686F756C64206120736C6F74206D61726B206265207368617265643FA76F7074696F6E739282A26964A3796573A474657874A953686172652069742E82A26964A26E6FA474657874AE4B656570206974206C6F63616C2EAE7265636F6D6D656E646174696F6EA26E6FA7646570656E647391D92967616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365A56C65616E73C0A4666C6167C0A97261697365645F696E82A46B696E64A743757453706563A26964D92B63756C746C69622D676170733A6375745F737065633A6375742D736C6F742D746F6C6572616E63652E7234A8646563696465727391D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A6737461747573A46F70656EA672756C696E67C0AA72756C65645F66726F6D90A56F7264657200AC6F7074696F6E735F68617368D94031373737626430366666616664663665383162656435393739663337313933633462346537633364323530346639303465393133313031326630373637613964A67265636F726485AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7130AC6F7074696F6E735F68617368D94031373737626430366666616664663665383162656435393739663337313933633462346537633364323530346639303465393133313031326630373637613964B1636F756E7465645F616E737765725F6964C0A7616E737765727390AB6E6578745F637572736F72C0" + } + ] + }, + { + "name": "fork_too_large_on_order_zero_leaves_nothing_below", + "ordinal": 9, + "entries": [ + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q1", + "title": "Fork 1", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 1, + "options_hash": "61959964bd214ea1b961554c43ec46a9faf156dd547cc50c8c16bbdcd55f223a" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q1", + "options_hash": "61959964bd214ea1b961554c43ec46a9faf156dd547cc50c8c16bbdcd55f223a", + "counted_answer_id": null, + "answers": [] + } + }, + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q0", + "title": "Fork 0", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 0, + "options_hash": "1777bd06ffafdf6e81bed5979f37193c4b4e7c3d2504f904e9131012f0767a9d" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q0", + "options_hash": "1777bd06ffafdf6e81bed5979f37193c4b4e7c3d2504f904e9131012f0767a9d", + "counted_answer_id": null, + "answers": [] + } + } + ], + "steps": [ + { + "request": { + "schemaVersion": "gamecult.fork.list_request.v0", + "mind": "gamecult-chat", + "status": null, + "cursor": null, + "limit": 5 + }, + "response": { + "schemaVersion": "gamecult.fork.list.v0", + "mind": "gamecult-chat", + "stamp": "s9", + "open_count": 2, + "recorded_count": 0, + "forks": [ + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q1", + "title": "Fork 1", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 1, + "options_hash": "61959964bd214ea1b961554c43ec46a9faf156dd547cc50c8c16bbdcd55f223a" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q1", + "options_hash": "61959964bd214ea1b961554c43ec46a9faf156dd547cc50c8c16bbdcd55f223a", + "counted_answer_id": null, + "answers": [] + } + } + ], + "next_cursor": "c1" + }, + "responseHex": "87AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E6C6973742E7630A46D696E64AD67616D6563756C742D63686174A57374616D70A27339AA6F70656E5F636F756E7402AE7265636F726465645F636F756E7400A5666F726B739183AD736368656D6156657273696F6EB667616D6563756C742E666F726B2E656E7472792E7630A463617264DE0010AD736368656D6156657273696F6EB567616D6563756C742E666F726B2E636172642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7131A57469746C65A6466F726B2031A7636F6E74657874BD53686F756C64206120736C6F74206D61726B206265207368617265643FA76F7074696F6E739282A26964A3796573A474657874A953686172652069742E82A26964A26E6FA474657874AE4B656570206974206C6F63616C2EAE7265636F6D6D656E646174696F6EA26E6FA7646570656E647391D92967616D6563756C742D636861743A7175657374696F6E3A66696C652D7A6F6E652D61756469656E6365A56C65616E73C0A4666C6167C0A97261697365645F696E82A46B696E64A743757453706563A26964D92B63756C746C69622D676170733A6375745F737065633A6375742D736C6F742D746F6C6572616E63652E7234A8646563696465727391D92465306431633262332D613439352D343638372D383837392D366135623463336432653166A6737461747573A46F70656EA672756C696E67C0AA72756C65645F66726F6D90A56F7264657201AC6F7074696F6E735F68617368D94036313935393936346264323134656131623936313535346334336563343661396661663135366464353437636335306338633136626264636435356632323361A67265636F726485AD736368656D6156657273696F6EB767616D6563756C742E666F726B2E7265636F72642E7630A7666F726B5F6964B967616D6563756C742D636861743A7175657374696F6E3A7131AC6F7074696F6E735F68617368D94036313935393936346264323134656131623936313535346334336563343661396661663135366464353437636335306338633136626264636435356632323361B1636F756E7465645F616E737765725F6964C0A7616E737765727390AB6E6578745F637572736F72A26331" + }, + { + "request": { + "schemaVersion": "gamecult.fork.list_request.v0", + "mind": "gamecult-chat", + "status": null, + "cursor": "c1", + "limit": 5 + }, + "refusal": { + "schemaVersion": "gamecult.fork.refusal.v0", + "code": "fork_too_large", + "error": "fork_too_large: fork gamecult-chat:question:q0 (order 0) alone is past the response budget" + }, + "refusalHex": "83AD736368656D6156657273696F6EB867616D6563756C742E666F726B2E7265667573616C2E7630A4636F6465AE666F726B5F746F6F5F6C61726765A56572726F72D95A666F726B5F746F6F5F6C617267653A20666F726B2067616D6563756C742D636861743A7175657374696F6E3A713020286F7264657220302920616C6F6E6520697320706173742074686520726573706F6E736520627564676574", + "httpStatus": 422, + "fork_id": "gamecult-chat:question:q0", + "order": 0 + } + ] + } + ], + "readTooLarge": [ + { + "name": "a_read_whose_entry_is_past_the_cap_is_fork_too_large_without_an_order", + "ordinal": 9, + "entries": [ + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q3", + "title": "Fork 3", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "ced892eb663c22e3d7ece602708043ee545f56b9d4e4421d7f5bfaa86c418ce2" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q3", + "options_hash": "ced892eb663c22e3d7ece602708043ee545f56b9d4e4421d7f5bfaa86c418ce2", + "counted_answer_id": null, + "answers": [] + } + }, + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q2", + "title": "Fork 2", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 2, + "options_hash": "5c7c2945e8e8689c6c928b11d7e0a033640b0c66afb7fcffb9e740c019d63e79" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q2", + "options_hash": "5c7c2945e8e8689c6c928b11d7e0a033640b0c66afb7fcffb9e740c019d63e79", + "counted_answer_id": null, + "answers": [] + } + }, + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q1", + "title": "Fork 1", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 1, + "options_hash": "61959964bd214ea1b961554c43ec46a9faf156dd547cc50c8c16bbdcd55f223a" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q1", + "options_hash": "61959964bd214ea1b961554c43ec46a9faf156dd547cc50c8c16bbdcd55f223a", + "counted_answer_id": null, + "answers": [] + } + }, + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q0", + "title": "Fork 0", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 0, + "options_hash": "1777bd06ffafdf6e81bed5979f37193c4b4e7c3d2504f904e9131012f0767a9d" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q0", + "options_hash": "1777bd06ffafdf6e81bed5979f37193c4b4e7c3d2504f904e9131012f0767a9d", + "counted_answer_id": null, + "answers": [] + } + } + ], + "request": { + "schemaVersion": "gamecult.fork.read_request.v0", + "mind": "gamecult-chat", + "fork_id": "gamecult-chat:question:q3" + }, + "refusal": { + "schemaVersion": "gamecult.fork.refusal.v0", + "code": "fork_too_large", + "error": "fork_too_large: fork gamecult-chat:question:q3 alone is past the response budget" + }, + "refusalHex": "83AD736368656D6156657273696F6EB867616D6563756C742E666F726B2E7265667573616C2E7630A4636F6465AE666F726B5F746F6F5F6C61726765A56572726F72D950666F726B5F746F6F5F6C617267653A20666F726B2067616D6563756C742D636861743A7175657374696F6E3A713320616C6F6E6520697320706173742074686520726573706F6E736520627564676574", + "httpStatus": 422, + "fork_id": "gamecult-chat:question:q3" + } + ], + "doorFaults": [ + { + "name": "read_door_answers_request_id_conflict_the_client_sees_unavailable", + "route": "read", + "doorCode": "request_id_conflict", + "httpStatus": 503, + "response": { + "schemaVersion": "gamecult.fork.unavailable.v0", + "code": "unavailable", + "error": "unavailable: the outcome is not confirmed, try again with the same request_id", + "retryAfterSeconds": 30 + }, + "responseHex": "84AD736368656D6156657273696F6EBC67616D6563756C742E666F726B2E756E617661696C61626C652E7630A4636F6465AB756E617661696C61626C65A56572726F72D94D756E617661696C61626C653A20746865206F7574636F6D65206973206E6F7420636F6E6669726D65642C2074727920616761696E2077697468207468652073616D6520726571756573745F6964B1726574727941667465725365636F6E64731E" + }, + { + "name": "list_door_answers_request_id_conflict_the_client_sees_unavailable", + "route": "list", + "doorCode": "request_id_conflict", + "httpStatus": 503, + "response": { + "schemaVersion": "gamecult.fork.unavailable.v0", + "code": "unavailable", + "error": "unavailable: the outcome is not confirmed, try again with the same request_id", + "retryAfterSeconds": 30 + }, + "responseHex": "84AD736368656D6156657273696F6EBC67616D6563756C742E666F726B2E756E617661696C61626C652E7630A4636F6465AB756E617661696C61626C65A56572726F72D94D756E617661696C61626C653A20746865206F7574636F6D65206973206E6F7420636F6E6669726D65642C2074727920616761696E2077697468207468652073616D6520726571756573745F6964B1726574727941667465725365636F6E64731E" + }, + { + "name": "answer_door_answers_request_id_conflict_the_client_sees_it", + "route": "answer", + "doorCode": "request_id_conflict", + "httpStatus": 409, + "response": { + "schemaVersion": "gamecult.fork.refusal.v0", + "code": "request_id_conflict", + "error": "request_id_conflict: this request_id was used for a different body" + }, + "responseHex": "83AD736368656D6156657273696F6EB867616D6563756C742E666F726B2E7265667573616C2E7630A4636F6465B3726571756573745F69645F636F6E666C696374A56572726F72D942726571756573745F69645F636F6E666C6963743A207468697320726571756573745F696420776173207573656420666F72206120646966666572656E7420626F6479" + } + ], "mindRoutes": { "minds": { "gamecult-chat": "huginn-gamecult-chat.sock", diff --git a/tests/Bifrost.Web.Tests/Support/DeskVectors.cs b/tests/Bifrost.Web.Tests/Support/DeskVectors.cs index ce450d4..03ab7db 100644 --- a/tests/Bifrost.Web.Tests/Support/DeskVectors.cs +++ b/tests/Bifrost.Web.Tests/Support/DeskVectors.cs @@ -3,12 +3,12 @@ using System.Text.Json.Nodes; namespace Bifrost.Web.Tests.Support; -// CultLib's fork-card vectors, vendored at commit 89755b08b5dd4bf72ed8e5907b5ff4717e51da12. +// CultLib's fork-card vectors, vendored at commit f6c9bf5f975177d2033578e9b51b4d21930b728a. public static class DeskVectors { - public const string SourceCommit = "89755b08b5dd4bf72ed8e5907b5ff4717e51da12"; + public const string SourceCommit = "f6c9bf5f975177d2033578e9b51b4d21930b728a"; - public const string FileSha256 = "3387debd16059621202b706761519231e7ad52d0d4bd071a32b80c0d916e42fa"; + public const string FileSha256 = "cc15fc9e36e9c1f7d77b42279dc0f9a72c392adb251cc6ae3a59b79b7a004619"; public static string Path => System.IO.Path.Combine(AppContext.BaseDirectory, "Fixtures", "fork-card-vectors.json"); @@ -46,5 +46,21 @@ public static class DeskVectors } } + // The vectors' refusal `error` text is illustrative (Libby): the contract pins the code, the key set, the status + // (asserted by the caller) and that the text names the fork id, and its order on a list. + public static bool SameRefusal(JsonNode? actual, JsonNode? vector, string forkId, int? order) + { + if (actual is not JsonObject a || vector is not JsonObject v || + !a.Select(p => p.Key).SequenceEqual(v.Select(p => p.Key)) || + (string?)a["schemaVersion"] != (string?)v["schemaVersion"] || (string?)a["code"] != (string?)v["code"]) + { + return false; + } + + var error = (string?)a["error"] ?? string.Empty; + return error.Contains(forkId, StringComparison.Ordinal) && + (order is null || error.Contains($"order {order}", StringComparison.Ordinal)); + } + public static byte[] Bytes(JsonNode node) => System.Text.Encoding.UTF8.GetBytes(node.ToJsonString()); } From 05dcceb2034073ef0e800a5504438d44d0846ad7 Mon Sep 17 00:00:00 2001 From: Heimdall Date: Wed, 11 Nov 2026 01:54:04 +0000 Subject: [PATCH 08/26] Desk: a stale answer on an oversize fork is 422 fork_too_large (#44) --- .../Features/Desk/DeskEndpoints.cs | 7 ++++ tests/Bifrost.Web.Tests/DeskAnswerTests.cs | 34 +++++++++++++++++-- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/Bifrost.Web/Features/Desk/DeskEndpoints.cs b/src/Bifrost.Web/Features/Desk/DeskEndpoints.cs index f97a949..83cd6c1 100644 --- a/src/Bifrost.Web/Features/Desk/DeskEndpoints.cs +++ b/src/Bifrost.Web/Features/Desk/DeskEndpoints.cs @@ -229,6 +229,13 @@ public static class DeskEndpoints if (string.Equals(result.Answer.Code, DeskStatus.StaleFork, StringComparison.Ordinal)) { var current = await door.ReadAsync(mind.Socket, context, result.Answer.ForkId, cancellationToken); + if (current is DeskReadOutcome.ForkTooLarge(var largeFork)) + { + // Nothing is recorded for a stale answer, and the card it would carry cannot be returned: permanent. + await WriteJsonAsync(http, DeskRefusals.ForkTooLargeStatus, DeskRefusals.ForkTooLargeDocument(largeFork)); + return; + } + if (current is not DeskReadOutcome.Found(var entry)) { await WriteUnavailableAsync(http, desk); diff --git a/tests/Bifrost.Web.Tests/DeskAnswerTests.cs b/tests/Bifrost.Web.Tests/DeskAnswerTests.cs index c772c25..709cd31 100644 --- a/tests/Bifrost.Web.Tests/DeskAnswerTests.cs +++ b/tests/Bifrost.Web.Tests/DeskAnswerTests.cs @@ -66,7 +66,7 @@ public sealed class DeskAnswerTests } [Fact] - public async Task A_stale_cards_read_that_is_fork_too_large_stays_unavailable_until_the_contract_says_otherwise() + public async Task A_stale_answers_card_read_of_an_oversize_fork_is_422_fork_too_large_naming_the_fork() { var (factory, token) = await ArrangeAsync(); using (factory) @@ -76,8 +76,36 @@ public sealed class DeskAnswerTests var reply = await factory.SendAsync("POST", DeskReplay.Target("answer", "gamecult-chat", null), DeskReplay.AnswerBody(DeskReplay.ChatFork), token); - Assert.Equal(503, reply.Status); - Assert.Equal("unavailable", (string?)JsonNode.Parse(reply.Body)!["code"]); + var doc = JsonNode.Parse(reply.Body)!; + Assert.Equal(422, reply.Status); + Assert.Equal("fork_too_large", (string?)doc["code"]); + Assert.Contains(DeskReplay.ChatFork, (string)doc["error"]!); + Assert.Null(doc["card"]); + } + } + + [Fact] + public async Task A_replayed_request_id_that_is_stale_again_takes_the_same_reread_and_gets_the_same_422() + { + var (factory, token) = await ArrangeAsync(); + using (factory) + { + // The door's stored Refused(stale_fork) for this request_id, returned on every replay while the fork stays oversize. + factory.Door.AnswerScript = (_, _, _) => new DeskAnswerOutcome.Answered(Stale(DeskReplay.ChatFork)); + factory.Door.ReadScript = (_, id) => new DeskReadOutcome.ForkTooLarge(id); + var body = DeskReplay.AnswerBody(DeskReplay.ChatFork); + var target = DeskReplay.Target("answer", "gamecult-chat", null); + + var first = await factory.SendAsync("POST", target, body, token); + var replay = await factory.SendAsync("POST", target, body, token); + + Assert.Equal(422, first.Status); + Assert.Equal(first.Status, replay.Status); + Assert.Equal(first.Body, replay.Body); + Assert.Equal("fork_too_large", (string?)JsonNode.Parse(replay.Body)!["code"]); + Assert.Contains(DeskReplay.ChatFork, (string)JsonNode.Parse(replay.Body)!["error"]!); + Assert.Equal(2, factory.Door.Calls.Count(c => c.Kind == "answer")); + Assert.Equal(2, factory.Door.Calls.Count(c => c.Kind == "read")); } } From 781e6eabf6191303664f152ec0155a0d1bdb7593 Mon Sep 17 00:00:00 2001 From: Heimdall Date: Wed, 11 Nov 2026 02:16:28 +0000 Subject: [PATCH 09/26] Desk: pin vectors to CultLib de8a062a (#45) --- .../Features/Desk/DeskDocuments.cs | 2 +- tests/Bifrost.Web.Tests/DeskAnswerTests.cs | 48 ++++ tests/Bifrost.Web.Tests/DeskVectorPinTests.cs | 2 +- .../Fixtures/fork-card-vectors.json | 207 ++++++++++++++++++ .../Bifrost.Web.Tests/Support/DeskVectors.cs | 6 +- 5 files changed, 260 insertions(+), 5 deletions(-) diff --git a/src/Bifrost.Web/Features/Desk/DeskDocuments.cs b/src/Bifrost.Web/Features/Desk/DeskDocuments.cs index d41c9cd..f8658f4 100644 --- a/src/Bifrost.Web/Features/Desk/DeskDocuments.cs +++ b/src/Bifrost.Web/Features/Desk/DeskDocuments.cs @@ -4,7 +4,7 @@ using System.Text.Json.Serialization; namespace Bifrost.Web.Features.Desk; // The fork-desk documents Bifrost reads or writes, spelled as CultLib's contracts/cultnet/governance/fork-card.md -// (commit f6c9bf5f) spells them: snake_case keys, every key written (nil where absent), declared in the +// (commit de8a062a) spells them: snake_case keys, every key written (nil where absent), declared in the // contract's key order. Bifrost authors only DeskRefusal and DeskUnavailable. AnswerRequest is the client's body; // ForkCard, DeskAnswerDocument and AnswerResult are Huginn's documents, typed here so they pass through unchanged. diff --git a/tests/Bifrost.Web.Tests/DeskAnswerTests.cs b/tests/Bifrost.Web.Tests/DeskAnswerTests.cs index 709cd31..ce1bcdc 100644 --- a/tests/Bifrost.Web.Tests/DeskAnswerTests.cs +++ b/tests/Bifrost.Web.Tests/DeskAnswerTests.cs @@ -109,6 +109,54 @@ public sealed class DeskAnswerTests } } + [Fact] + public async Task A_stale_answer_over_an_oversize_fork_follows_the_vector_fresh_and_replayed() + { + var rows = DeskVectors.Rows("transport", "answerTooLarge").ToList(); + Assert.Single(rows); + foreach (var row in rows) + { + var (factory, token) = await ArrangeAsync(); + using (factory) + { + var request = row["request"]!; + var forkId = (string)row["fork_id"]!; + Assert.Equal(forkId, (string)request["fork_id"]!); + var stale = new AnswerResult( + DeskSchemas.AnswerResult, + new DeskAnswerDocument( + DeskSchemas.DeskAnswer, "n1", forkId, (string)row["outcome"]!, null, null, (string)row["code"]!, + (string)request["options_hash"]!, (string?)request["answer"]!["choice"], (string?)request["answer"]!["note"], + (string)request["answered_at"]!, "2026-11-12T19:02:05Z", false, DeskWebApplicationFactory.Alice, "Operator", + (string)request["request_id"]!), + null, + null); + factory.Door.AnswerScript = (_, _, _) => new DeskAnswerOutcome.Answered(stale); + factory.Door.ReadScript = (_, id) => new DeskReadOutcome.ForkTooLarge(id); + var body = DeskVectors.Bytes(request); + var target = DeskReplay.Target("answer", "gamecult-chat", null); + + var fresh = await factory.SendAsync("POST", target, body, token); + var replay = await factory.SendAsync("POST", target, body, token); + + foreach (var reply in new[] { fresh, replay }) + { + Assert.Equal((int)row["httpStatus"]!, reply.Status); + var doc = JsonNode.Parse(reply.Body); + Assert.True(DeskVectors.SameRefusal(doc, row["refusal"], forkId, null), (string)row["name"]!); + Assert.Null(doc!["card"]); + } + + Assert.Equal(fresh.Body, replay.Body); + // The door got the client's bytes untouched, stored nothing counted, and was asked for the card twice. + Assert.All(factory.Door.Calls.Where(c => c.Kind == "answer"), c => Assert.Equal(body, c.Body)); + Assert.Equal(forkId, factory.Door.Calls.First(c => c.Kind == "read").ForkId); + Assert.False((bool)row["counted"]!); + Assert.NotEqual("Counted", stale.Answer.Outcome); + } + } + } + [Fact] public async Task Card_read_failure_is_unavailable() { diff --git a/tests/Bifrost.Web.Tests/DeskVectorPinTests.cs b/tests/Bifrost.Web.Tests/DeskVectorPinTests.cs index e14a79f..3ed7df6 100644 --- a/tests/Bifrost.Web.Tests/DeskVectorPinTests.cs +++ b/tests/Bifrost.Web.Tests/DeskVectorPinTests.cs @@ -6,7 +6,7 @@ namespace Bifrost.Web.Tests; public sealed class DeskVectorPinTests { [Fact] - public void Vectors_are_cultlib_f6c9bf5f() + public void Vectors_are_cultlib_de8a062a() { var sha = Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(DeskVectors.Path))).ToLowerInvariant(); diff --git a/tests/Bifrost.Web.Tests/Fixtures/fork-card-vectors.json b/tests/Bifrost.Web.Tests/Fixtures/fork-card-vectors.json index 5f5e7c2..38b255d 100644 --- a/tests/Bifrost.Web.Tests/Fixtures/fork-card-vectors.json +++ b/tests/Bifrost.Web.Tests/Fixtures/fork-card-vectors.json @@ -7702,6 +7702,213 @@ "fork_id": "gamecult-chat:question:q3" } ], + "answerTooLarge": [ + { + "name": "a_stale_answer_whose_card_reread_is_past_the_cap_is_fork_too_large_over_a_stored_stale_refusal", + "entries": [ + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q3", + "title": "Fork 3", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 3, + "options_hash": "ced892eb663c22e3d7ece602708043ee545f56b9d4e4421d7f5bfaa86c418ce2" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q3", + "options_hash": "ced892eb663c22e3d7ece602708043ee545f56b9d4e4421d7f5bfaa86c418ce2", + "counted_answer_id": null, + "answers": [] + } + }, + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q2", + "title": "Fork 2", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 2, + "options_hash": "5c7c2945e8e8689c6c928b11d7e0a033640b0c66afb7fcffb9e740c019d63e79" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q2", + "options_hash": "5c7c2945e8e8689c6c928b11d7e0a033640b0c66afb7fcffb9e740c019d63e79", + "counted_answer_id": null, + "answers": [] + } + }, + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q1", + "title": "Fork 1", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 1, + "options_hash": "61959964bd214ea1b961554c43ec46a9faf156dd547cc50c8c16bbdcd55f223a" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q1", + "options_hash": "61959964bd214ea1b961554c43ec46a9faf156dd547cc50c8c16bbdcd55f223a", + "counted_answer_id": null, + "answers": [] + } + }, + { + "schemaVersion": "gamecult.fork.entry.v0", + "card": { + "schemaVersion": "gamecult.fork.card.v0", + "fork_id": "gamecult-chat:question:q0", + "title": "Fork 0", + "context": "Should a slot mark be shared?", + "options": [ + { + "id": "yes", + "text": "Share it." + }, + { + "id": "no", + "text": "Keep it local." + } + ], + "recommendation": "no", + "depends": [ + "gamecult-chat:question:file-zone-audience" + ], + "leans": null, + "flag": null, + "raised_in": { + "kind": "CutSpec", + "id": "cultlib-gaps:cut_spec:cut-slot-tolerance.r4" + }, + "deciders": [ + "e0d1c2b3-a495-4687-8879-6a5b4c3d2e1f" + ], + "status": "open", + "ruling": null, + "ruled_from": [], + "order": 0, + "options_hash": "1777bd06ffafdf6e81bed5979f37193c4b4e7c3d2504f904e9131012f0767a9d" + }, + "record": { + "schemaVersion": "gamecult.fork.record.v0", + "fork_id": "gamecult-chat:question:q0", + "options_hash": "1777bd06ffafdf6e81bed5979f37193c4b4e7c3d2504f904e9131012f0767a9d", + "counted_answer_id": null, + "answers": [] + } + } + ], + "request": { + "schemaVersion": "gamecult.fork.answer_request.v0", + "fork_id": "gamecult-chat:question:q3", + "options_hash": "14b8ffbdb61106dbc06f69f3fc8e2072af7a205d766e4cc040e89733013d54dc", + "answer": { + "choice": "yes", + "note": "Keep it local: a slot mark is the author's call, and sharing it is a separate act." + }, + "retracted": false, + "retracts": null, + "answered_at": "2026-11-12T19:02:00Z", + "request_id": "0000a001-0000-4000-8000-00000000a001" + }, + "refusal": { + "schemaVersion": "gamecult.fork.refusal.v0", + "code": "fork_too_large", + "error": "fork_too_large: fork gamecult-chat:question:q3 alone is past the response budget" + }, + "refusalHex": "83AD736368656D6156657273696F6EB867616D6563756C742E666F726B2E7265667573616C2E7630A4636F6465AE666F726B5F746F6F5F6C61726765A56572726F72D950666F726B5F746F6F5F6C617267653A20666F726B2067616D6563756C742D636861743A7175657374696F6E3A713320616C6F6E6520697320706173742074686520726573706F6E736520627564676574", + "httpStatus": 422, + "outcome": "Refused", + "code": "stale_fork", + "counted": false, + "fork_id": "gamecult-chat:question:q3" + } + ], "doorFaults": [ { "name": "read_door_answers_request_id_conflict_the_client_sees_unavailable", diff --git a/tests/Bifrost.Web.Tests/Support/DeskVectors.cs b/tests/Bifrost.Web.Tests/Support/DeskVectors.cs index 03ab7db..f613b9f 100644 --- a/tests/Bifrost.Web.Tests/Support/DeskVectors.cs +++ b/tests/Bifrost.Web.Tests/Support/DeskVectors.cs @@ -3,12 +3,12 @@ using System.Text.Json.Nodes; namespace Bifrost.Web.Tests.Support; -// CultLib's fork-card vectors, vendored at commit f6c9bf5f975177d2033578e9b51b4d21930b728a. +// CultLib's fork-card vectors, vendored at commit de8a062ab9d8c510c8b36e634fae8f108a570aff. public static class DeskVectors { - public const string SourceCommit = "f6c9bf5f975177d2033578e9b51b4d21930b728a"; + public const string SourceCommit = "de8a062ab9d8c510c8b36e634fae8f108a570aff"; - public const string FileSha256 = "cc15fc9e36e9c1f7d77b42279dc0f9a72c392adb251cc6ae3a59b79b7a004619"; + public const string FileSha256 = "a7129eee00cb38c602481b0de7de0182f83122685b69ff441e3123adc3e0f774"; public static string Path => System.IO.Path.Combine(AppContext.BaseDirectory, "Fixtures", "fork-card-vectors.json"); From 68dee25e3aecdcb7314191edc4fa3034142ca99c Mon Sep 17 00:00:00 2001 From: Heimdall Date: Wed, 11 Nov 2026 04:53:55 +0000 Subject: [PATCH 10/26] Drop indexes: declarations from TS document types (#46) --- tools/agent-transport.mjs | 5 ----- tools/bifrost-crossing-documents.mjs | 5 ----- tools/bifrost-repository-release-authority.mjs | 1 - tools/governance-threads.mjs | 10 ---------- tools/provider-advertisement.mjs | 3 --- 5 files changed, 24 deletions(-) diff --git a/tools/agent-transport.mjs b/tools/agent-transport.mjs index 42c7337..ab124fa 100644 --- a/tools/agent-transport.mjs +++ b/tools/agent-transport.mjs @@ -68,11 +68,6 @@ const updateRequestDefinition = defineDocumentType({ parse: parseUpdateRequest, }, name: "id", - indexes: { - targetRepoName: "targetRepoName", - targetAgentIdentity: "targetAgentIdentity", - status: "status", - }, members: [ { slot: 1, memberName: "id", typeName: "string", isName: true }, { slot: 2, memberName: "targetRepoName", typeName: "string", indexAlias: "targetRepoName" }, diff --git a/tools/bifrost-crossing-documents.mjs b/tools/bifrost-crossing-documents.mjs index b7d60ac..a05c27f 100644 --- a/tools/bifrost-crossing-documents.mjs +++ b/tools/bifrost-crossing-documents.mjs @@ -27,11 +27,6 @@ export function crossingReceiptDefinition(defineDocumentTypeInput) { contentHash: crossingReceiptSchemaId, global: false, name: "receiptId", - indexes: { - commandId: "commandId", - crossingKind: "crossingKind", - status: "status", - }, schema: { parse: parseCrossingReceipt }, members: [ { slot: 1, memberName: "receiptId", typeName: "string", isName: true }, diff --git a/tools/bifrost-repository-release-authority.mjs b/tools/bifrost-repository-release-authority.mjs index f1bf4a2..c8d0c02 100644 --- a/tools/bifrost-repository-release-authority.mjs +++ b/tools/bifrost-repository-release-authority.mjs @@ -24,7 +24,6 @@ export function repositoryReleaseAuthorityDefinition(defineDocumentTypeInput) { contentHash: repositoryReleaseAuthoritySchemaId, global: false, name: "authorityId", - indexes: { repositoryFullName: "repositoryFullName", upstreamRef: "upstreamRef", commitSha: "commitSha", status: "status" }, schema: { parse: parseRepositoryReleaseAuthority }, members: [ { slot: 1, memberName: "authorityId", typeName: "string", isName: true }, diff --git a/tools/governance-threads.mjs b/tools/governance-threads.mjs index 832e5f7..d2eae86 100644 --- a/tools/governance-threads.mjs +++ b/tools/governance-threads.mjs @@ -59,11 +59,6 @@ const topicDefinition = defineDocumentType({ schemaVersion: "bifrost.governance.topic.v0", schema: { parse: parseTopic }, name: "id", - indexes: { - jurisdictionRepoName: "jurisdictionRepoName", - jurisdictionAgentIdentity: "jurisdictionAgentIdentity", - status: "status", - }, members: [ { slot: 1, memberName: "id", typeName: "string", isName: true }, { slot: 2, memberName: "title", typeName: "string" }, @@ -93,11 +88,6 @@ const commentDefinition = defineDocumentType({ schemaVersion: "bifrost.governance.topic_comment.v0", schema: { parse: parseComment }, name: "id", - indexes: { - topicId: "topicId", - authorId: "authorId", - stance: "stance", - }, members: [ { slot: 1, memberName: "id", typeName: "string", isName: true }, { slot: 2, memberName: "topicId", typeName: "string", indexAlias: "topicId" }, diff --git a/tools/provider-advertisement.mjs b/tools/provider-advertisement.mjs index 32563af..6c6a4fb 100644 --- a/tools/provider-advertisement.mjs +++ b/tools/provider-advertisement.mjs @@ -179,9 +179,6 @@ const advertisementDefinition = defineDocumentType({ parse: (input) => parseAdvertisement(input, { requireCurrentContract: false }), }, name: "providerId", - indexes: { - serviceName: "serviceName", - }, members: [ { slot: 1, memberName: "providerId", typeName: "string", isName: true }, { slot: 2, memberName: "serviceName", typeName: "string", indexAlias: "serviceName" }, From 4e86c33cd21414c191164ccbce71c222924d2fa8 Mon Sep 17 00:00:00 2001 From: Heimdall Date: Wed, 11 Nov 2026 05:01:33 +0000 Subject: [PATCH 11/26] Resolve CultLib via VOIDBOT_CULTLIB_ROOT in tests; test formerly indexed stores (#47) --- tests/cultlib-root.mjs | 5 ++ tests/persona-feedback-cli.test.mjs | 3 +- tests/persona-feedback-idunn-health.test.mjs | 3 +- tests/persona-matrix-mouth-runtime.test.mjs | 7 +- tests/shared-value-stores.test.mjs | 86 ++++++++++++++++++++ tests/watchdog-notice.test.mjs | 4 +- tools/agent-transport.mjs | 6 +- tools/governance-threads.mjs | 4 +- 8 files changed, 108 insertions(+), 10 deletions(-) create mode 100644 tests/cultlib-root.mjs create mode 100644 tests/shared-value-stores.test.mjs diff --git a/tests/cultlib-root.mjs b/tests/cultlib-root.mjs new file mode 100644 index 0000000..619c403 --- /dev/null +++ b/tests/cultlib-root.mjs @@ -0,0 +1,5 @@ +import { resolve } from "node:path"; + +// The CultLib checkout the tests load at run time, as the tools resolve it: +// $VOIDBOT_CULTLIB_ROOT, else the sibling ../CultLib. Its dist/ is gitignored; build it there first. +export const cultLibRoot = resolve(process.env.VOIDBOT_CULTLIB_ROOT || resolve(import.meta.dirname, "..", "..", "CultLib")); diff --git a/tests/persona-feedback-cli.test.mjs b/tests/persona-feedback-cli.test.mjs index f4fec1d..704d587 100644 --- a/tests/persona-feedback-cli.test.mjs +++ b/tests/persona-feedback-cli.test.mjs @@ -9,7 +9,8 @@ import { resolve } from "node:path"; import { buildFeedbackDefinitions } from "../tools/persona-feedback-documents.mjs"; import { conversationFromDiscordMessage, discordGatewayReady } from "../tools/bifrost-discord-persona-ingress.mjs"; -const root=resolve(import.meta.dirname,".."), cult=resolve(root,"..","CultLib"); +import { cultLibRoot } from "./cultlib-root.mjs"; +const root=resolve(import.meta.dirname,".."), cult=cultLibRoot; const cr=createRequire(resolve(cult,"packages","cultcache-ts","package.json")); const mr=createRequire(resolve(cult,"packages","cultmesh-ts","package.json")); const {defineDocumentType}=cr("@gamecult/cultcache-ts"), {CultMesh}=mr("@gamecult/cultmesh-ts"), defs=buildFeedbackDefinitions(defineDocumentType); diff --git a/tests/persona-feedback-idunn-health.test.mjs b/tests/persona-feedback-idunn-health.test.mjs index 360a15a..80c3a0f 100644 --- a/tests/persona-feedback-idunn-health.test.mjs +++ b/tests/persona-feedback-idunn-health.test.mjs @@ -8,7 +8,8 @@ import { resolve } from "node:path"; import { enrollBifrostFeedbackIdentity, providerHealthIdentity, signFeedbackAdmission } from "../tools/bifrost-feedback-identity.mjs"; import { createPersonaFeedbackHealthPublisher, createSignedHealthRawPut, createSignedPersonaFeedbackHealth, signedHealthSourceRole } from "../tools/persona-feedback-idunn-health.mjs"; -const root=resolve(import.meta.dirname,".."),cult=resolve(root,"..","CultLib"),require=createRequire(resolve(cult,"packages","cultnet-ts","package.json")),{encode,decode}=require("@msgpack/msgpack"); +import { cultLibRoot } from "./cultlib-root.mjs"; +const root=resolve(import.meta.dirname,".."),cult=cultLibRoot,require=createRequire(resolve(cult,"packages","cultnet-ts","package.json")),{encode,decode}=require("@msgpack/msgpack"); const idDomain=Buffer.from("gamecult.provider-health.identity.v1\0"),signatureDomain=Buffer.from("gamecult.provider-health.signature.v1\0"),purpose=Buffer.from("idunn.signed_daemon_health.v1"); test("Persona-feedback emits the exact canonical signed daemon-health tuple",async()=>{ diff --git a/tests/persona-matrix-mouth-runtime.test.mjs b/tests/persona-matrix-mouth-runtime.test.mjs index 098a422..4d2b0c6 100644 --- a/tests/persona-matrix-mouth-runtime.test.mjs +++ b/tests/persona-matrix-mouth-runtime.test.mjs @@ -1,9 +1,10 @@ // The parts of the Matrix mouth that sit on CultLib: the chat_* wire documents, the // CultCache cursor and brake stores, and the CultNet RUDP endpoint. Needs the built -// CultLib TypeScript packages (CULTLIB_ROOT, else ../CultLib), like the other Bifrost tests. CultLib's dist/ is gitignored: +// CultLib TypeScript packages (VOIDBOT_CULTLIB_ROOT, else ../CultLib), like the other Bifrost tests. CultLib's dist/ is gitignored: // run `npm ci && npm run build:ts` there first. Since CultLib 286a9af the packages are @gamecult/cultcache-ts, // @gamecult/cultnet-ts and @gamecult/cultmesh-ts; the directories under packages/ keep their short names. import test from "node:test"; +import { cultLibRoot } from "./cultlib-root.mjs"; import assert from "node:assert/strict"; import dgram from "node:dgram"; import { createServer } from "node:http"; @@ -15,7 +16,7 @@ import * as docs from "../tools/persona-chat-documents.mjs"; import { createMouth, cultcacheState, connectionIdFor, createBrakeReader, loadRuntime, pageOf, startChatEndpoint } from "../tools/persona-matrix-mouth.mjs"; const root = resolve(import.meta.dirname, ".."); -const runtime = loadRuntime(resolve(process.env.CULTLIB_ROOT ?? resolve(root, "..", "CultLib"))); +const runtime = loadRuntime(cultLibRoot); const { msgpack } = runtime; const fixtureDir = resolve(import.meta.dirname, "fixtures", "persona-chat-v1"); @@ -256,7 +257,7 @@ test("one process serves N Personas from systemd credentials: each token only in probe.close(); const child = spawn(process.execPath, [resolve(root, "tools", "persona-matrix-mouth.mjs")], { env: { - PATH: process.env.PATH, CULTLIB_ROOT: resolve(process.env.CULTLIB_ROOT ?? resolve(root, "..", "CultLib")), PERSONAS: "eureka,nova", CREDENTIALS_DIRECTORY: creds, CONTROL_ROOT: control, + PATH: process.env.PATH, CULTLIB_ROOT: cultLibRoot, PERSONAS: "eureka,nova", CREDENTIALS_DIRECTORY: creds, CONTROL_ROOT: control, STATE_DIRECTORY: stateDir, MATRIX_HOMESERVER: `http://127.0.0.1:${hs.address().port}`, MATRIX_ROOMS: "#swarm:t", MATRIX_SERVER_NAME: "t", CHAT_RUDP_LISTEN: `127.0.0.1:${port}`, }, }); diff --git a/tests/shared-value-stores.test.mjs b/tests/shared-value-stores.test.mjs new file mode 100644 index 0000000..092f15a --- /dev/null +++ b/tests/shared-value-stores.test.mjs @@ -0,0 +1,86 @@ +// Six document types once declared `indexes:` on grouping fields (status, repo, agent, topic...) that many rows share. +// CultLib's `indexes:` means unique, so each store must still write, close and reopen with rows that share those values. +// Real CLIs and store openers against temp files; CultLib comes from tests/cultlib-root.mjs. +import test from "node:test"; +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { resolve } from "node:path"; +import { cultLibRoot } from "./cultlib-root.mjs"; +import { openCrossingReceiptStore } from "../tools/bifrost-crossing-documents.mjs"; +import { buildAuthorizedRelease, openRepositoryReleaseAuthorityStore } from "../tools/bifrost-repository-release-authority.mjs"; + +const root = resolve(import.meta.dirname, ".."); +const dir = mkdtempSync(resolve(tmpdir(), "bifrost-shared-")); +test.after(() => rmSync(dir, { recursive: true, force: true })); + +const env = { + ...process.env, + VOIDBOT_CULTLIB_ROOT: cultLibRoot, + BIFROST_SKIP_LOCAL_ENV: "true", + BIFROST_ROOT_VERSE: "test-verse", + BIFROST_MACHINE_ID: "test-machine", + BIFROST_CROSSING_RECEIPT_STORE: resolve(dir, "receipts.cc"), +}; +for (const name of ["BIFROST_BRIDGE_BASE_URL", "BIFROST_BRIDGE_TOKEN", "BIFROST_ALLOW_UNRECEIPTED_ACTIVITY"]) delete env[name]; + +function cli(tool, args) { + const run = spawnSync(process.execPath, [resolve(root, "tools", tool), ...args], { env, encoding: "utf8", cwd: root }); + assert.equal(run.status, 0, `${tool} ${args[0]}\n${run.stderr}`); + return run.stdout; +} + +test("agent-transport store holds requests that share repo, agent and status", () => { + const store = resolve(dir, "agent-transport.cc"); + for (const id of ["req-1", "req-2", "req-3"]) { + cli("agent-transport.mjs", ["enqueue", "--store", store, "--id", id, "--repo", "Bifrost", "--agent", "heimdall", "--title", id, "--request", "body", "--allow-unmirrored", "true"]); + } + const listed = JSON.parse(cli("agent-transport.mjs", ["list", "--store", store])); + assert.deepEqual(listed.map((request) => request.id).sort(), ["req-1", "req-2", "req-3"]); +}); + +test("governance-threads store holds topics and comments that share their grouping fields", () => { + const store = resolve(dir, "governance.cc"); + for (const id of ["topic-1", "topic-2"]) { + cli("governance-threads.mjs", ["open", "--store", store, "--id", id, "--title", id, "--repo", "Bifrost", "--agent", "heimdall", "--summary", "s", "--allow-unmirrored", "true"]); + } + for (const id of ["c-1", "c-2"]) { + cli("governance-threads.mjs", ["comment", "--store", store, "--topic", "topic-1", "--id", id, "--author", "heimdall", "--stance", "support", "--body", "same", "--allow-unmirrored", "true"]); + } + const topics = JSON.parse(cli("governance-threads.mjs", ["list", "--store", store])); + assert.deepEqual(topics.map((topic) => topic.id).sort(), ["topic-1", "topic-2"]); + const shown = JSON.parse(cli("governance-threads.mjs", ["show", "--store", store, "--topic", "topic-1"])); + assert.equal(JSON.stringify(shown).match(/"id": ?"c-\d"/g)?.length ?? 0, 2); +}); + +test("crossing receipt store reopens with receipts that share status, kind and command", async () => { + const receipts = await openCrossingReceiptStore(env.BIFROST_CROSSING_RECEIPT_STORE); + const before = receipts.getAll().length; + assert.ok(before >= 5, "the CLI runs above leave completed agent_transport.request receipts"); + const reopened = await openCrossingReceiptStore(env.BIFROST_CROSSING_RECEIPT_STORE); + assert.equal(reopened.getAll().length, before); + assert.ok(new Set(reopened.getAll().map((receipt) => `${receipt.status}/${receipt.crossingKind}`)).size < before); +}); + +test("release authority store reopens with authorities that share repository, ref and status", async () => { + const path = resolve(dir, "release-authority.cc"); + const authority = (commitSha) => buildAuthorizedRelease({ + repositoryFullName: "GameCult/Bifrost", upstreamRef: "refs/heads/main", commitSha, + commandId: `cmd-${commitSha[0]}`, crossingReceiptId: `crossing-${commitSha[0]}`, policyDecisionId: "policy", authorityReference: "ref", + actorIdentity: "heimdall", sourceKind: "test", sourceId: "src", epiphanyRunId: "run", epiphanyLaneId: "lane", epiphanyAgentIdentity: "heimdall", + externalReceiptUrl: "https://example.test/r", externalReceiptId: "r", authorizedAt: "2026-11-11T00:00:00.000Z", + }); + const store = await openRepositoryReleaseAuthorityStore(path); + await store.put(authority("a".repeat(40))); + await store.put(authority("b".repeat(40))); + const reopened = await openRepositoryReleaseAuthorityStore(path); + assert.equal(reopened.getAll().length, 2); +}); + +test("provider advertisement store exports and re-exports beside other documents", () => { + const out = resolve(dir, "provider.cc"); + cli("provider-advertisement.mjs", ["export", "--out", out]); + const second = JSON.parse(cli("provider-advertisement.mjs", ["export", "--out", out])); + assert.equal(second.ok, true); +}); diff --git a/tests/watchdog-notice.test.mjs b/tests/watchdog-notice.test.mjs index 21d27aa..954c051 100644 --- a/tests/watchdog-notice.test.mjs +++ b/tests/watchdog-notice.test.mjs @@ -1,3 +1,4 @@ +import { cultLibRoot } from "./cultlib-root.mjs"; import test from "node:test"; import assert from "node:assert/strict"; import { createHash } from "node:crypto"; @@ -26,8 +27,7 @@ const T_OPEN = "2023-11-14T22:13:20.000Z"; const T_CLOSE = "2023-11-14T22:15:00.000Z"; const CLOSED_TEXT = `Idunn incident closed (recovered): continuity-exhausted on \`closed-target\`, opened ${T_OPEN}, closed ${T_CLOSE}`; -// CultLib is the sibling ../CultLib, like the other tests. -const cult = resolve(import.meta.dirname, "..", "..", "CultLib"); +const cult = cultLibRoot; const cache = createRequire(resolve(cult, "packages", "cultcache-ts", "package.json"))("@gamecult/cultcache-ts"); const msgpack = createRequire(resolve(cult, "packages", "cultnet-ts", "package.json"))("@msgpack/msgpack"); diff --git a/tools/agent-transport.mjs b/tools/agent-transport.mjs index ab124fa..b187b81 100644 --- a/tools/agent-transport.mjs +++ b/tools/agent-transport.mjs @@ -21,13 +21,15 @@ const defaultPersonaName = "Bifrost"; const defaultPersonaAvatarUrl = "https://raw.githubusercontent.com/GameCult/Bifrost/main/src/Bifrost.Web/wwwroot/img/bifrost-profile.png"; +const cultLibRoot = resolve(process.env.VOIDBOT_CULTLIB_ROOT || resolve(projectsRoot, "CultLib")); + const cultCachePackagePath = resolveFirstExisting("CultCache TypeScript runtime", [ - resolve(projectsRoot, "CultLib", "packages", "cultcache-ts", "package.json"), + resolve(cultLibRoot, "packages", "cultcache-ts", "package.json"), resolve(projectsRoot, "CultCacheTS", "package.json"), resolve(projectsRoot, "CultCacheTS", "node_modules", "cultcache-ts", "package.json"), ]); const cultNetPackagePath = resolveFirstExisting("CultNet TypeScript runtime", [ - resolve(projectsRoot, "CultLib", "packages", "cultnet-ts", "package.json"), + resolve(cultLibRoot, "packages", "cultnet-ts", "package.json"), resolve(projectsRoot, "CultNetTS", "package.json"), ]); const cultCacheRequire = createRequire(cultCachePackagePath); diff --git a/tools/governance-threads.mjs b/tools/governance-threads.mjs index d2eae86..7359ceb 100644 --- a/tools/governance-threads.mjs +++ b/tools/governance-threads.mjs @@ -23,8 +23,10 @@ const defaultPersonaName = "Bifrost"; const defaultPersonaAvatarUrl = "https://raw.githubusercontent.com/GameCult/Bifrost/main/src/Bifrost.Web/wwwroot/img/bifrost-profile.png"; +const cultLibRoot = resolve(process.env.VOIDBOT_CULTLIB_ROOT || resolve(projectsRoot, "CultLib")); + const cultCachePackagePath = resolveFirstExisting("CultCache TypeScript runtime", [ - resolve(projectsRoot, "CultLib", "packages", "cultcache-ts", "package.json"), + resolve(cultLibRoot, "packages", "cultcache-ts", "package.json"), resolve(projectsRoot, "CultCacheTS", "package.json"), resolve(projectsRoot, "CultCacheTS", "node_modules", "cultcache-ts", "package.json"), ]); From 1d0548313cd141264cd2d539dec9701bce040382 Mon Sep 17 00:00:00 2001 From: Heimdall Date: Wed, 11 Nov 2026 06:26:30 +0000 Subject: [PATCH 12/26] Mouth: addressing facts on deliveries (chat_pending.v3 basis and form) (#48) --- docs/bridge.md | 24 +++ ...mecult.persona.chat_pending.v3.tag.msgpack | Bin 0 -> 197 bytes ...na.chat_pending.v3.unproven-notice.msgpack | Bin 0 -> 153 bytes ...lt.persona.chat_pending_request.v2.msgpack | 1 + tests/persona-matrix-mouth-runtime.test.mjs | 24 +++ tests/persona-matrix-mouth.test.mjs | 174 +++++++++++++++++- tools/persona-chat-documents.mjs | 34 ++++ tools/persona-matrix-mouth.mjs | 58 +++++- 8 files changed, 307 insertions(+), 8 deletions(-) create mode 100644 tests/fixtures/persona-chat-v5/gamecult.persona.chat_pending.v3.tag.msgpack create mode 100644 tests/fixtures/persona-chat-v5/gamecult.persona.chat_pending.v3.unproven-notice.msgpack create mode 100644 tests/fixtures/persona-chat-v5/gamecult.persona.chat_pending_request.v2.msgpack diff --git a/docs/bridge.md b/docs/bridge.md index d3a07c9..4468988 100644 --- a/docs/bridge.md +++ b/docs/bridge.md @@ -198,6 +198,30 @@ Bifrost's native Discord gateway is the conversation observer. A direct mention Persona speech goes out through Matrix, not Discord. `tools/persona-matrix-mouth.mjs` is the one process for every Persona (`PERSONAS`, a list of slugs): it holds each Persona's Matrix access token as its own systemd credential (`matrix-token-`) and nobody else holds any. Per Persona it keeps one `/sync` loop over the configured rooms and the account's direct rooms (a DM invitation from an account on `MATRIX_SERVER_NAME` is accepted while that Persona's brake is released, and one from any other server is declined), one cursor and one delivery sequence. A delivery carries the sender's Matrix id, the event's `m.mentions` user ids and whether the room is a DM. A media event (`m.image`, `m.file`, `m.audio`, `m.video`) is delivered as a typed text line, `[image filename="cat.png" mime=image/png size=2048 url=mxc://server/id]`, followed by its caption when it has one, in `text` and in context. Only `m.image` is also fetched, and only when `IMAGE_STORE_ROOT` is set (unset, the line above is all there is and nothing is downloaded). The fetch is made as the receiving Persona, with its own token, from `/_matrix/client/v1/media/download//` on its homeserver, for a message the Persona is delivered (not its own words, a room it is not joined to, or history before the ear), and only from the event's own `content.url`; a URL in the message text is never followed. An encrypted attachment (`m.room.encrypted`, or `content.file`) is never fetched. The type is decided by the magic bytes alone (png, jpeg, gif, webp; SVG and anything else is refused, the declared mime is ignored), the body is capped at 5 MiB by `Content-Length` and by the bytes actually read, and the pixel count read from the image header (never decoded) is capped at 40 MP. A refused or failed fetch leaves the typed line with a fixed-word reason (`refused=encrypted|no-url|fetch-failed|too-big|wrong-type|bad-header|too-many-pixels|store-failed`) and no image. A stored image is written to `IMAGE_STORE_ROOT///[.png|.jpg|.gif|.webp]`, the name generated by the mouth, never the sender's: files `0640`, room directories `2750` (the Persona directory is setgid to the Persona's group, so the Persona's reader opens them by group; the mouth is the only writer), each written under a temporary name and renamed. It is deleted once it is at least 24 hours old and its delivery has been acked, whichever is later; a room keeps at most 50 MiB, the oldest evicted first. The sweep runs when an image is stored and when an ack arrives. A delivery is the 12-field tuple of `gamecult.persona.chat_pending.v2`: field 11, after `is_direct`, is nil or `[path, mime, size, sha256]`, a reference and never bytes; `path` is the store-relative `/` above, `mime` the sniffed type, `size` the file length (at most 5 MiB) and `sha256` its lowercase hex digest. The reader re-checks all four against the file. A Persona turn that has no reader for it still sees the typed line. Reading wakes nothing and acks nothing: the heartbeat polls with the non-consuming `gamecult.persona.chat_pending_request.v1`, and only `chat_ack` advances a position. An ack posts nothing to Matrix. That Persona's public `m.read` receipt follows `gamecult.persona.chat_read.v1` (`[persona, room_alias, event_id, at_unix_ms]`), which Huginn sends when a delivery has gone into the model's input: the mouth posts `m.read` at that event id only, for an event it delivered to that Persona in that room (to the delivered room id, whatever alias the report named), never behind the last receipt that landed there, one report per room at a time, and only while her brake is released (re-read after the presence write; fire and forget on the endpoint: a failure is logged once and not retried; pending polls and acks post none). Her Matrix presence follows a turn and her brake: `online` while a read report, a send or an ack came within `presenceGraceMs` (5 minutes), `unavailable` after that, `offline` while the brake is held. Each `/sync` carries the state as `set_presence`, and a change is also `PUT /presence//status` at once (a brake placed mid long-poll shows within `syncTimeoutMs`, 30 s, plus one PUT; a stopped mouth reads offline after Synapse's sync timeout). The mouth speaks on a `gamecult.persona.chat_send_request.v1` and joins rooms only while that Persona's brake (`/etc/gamecult/minds//control.cc`) is released. One loopback CultNet RUDP endpoint serves all of them; a request naming an unlisted Persona gets a typed `chat_refusal` (`UnknownPersona`). Document shapes: `tools/persona-chat-documents.mjs`; golden bytes: `tests/fixtures/persona-chat-v1/`, `-v2/` and `-v3/` (the pending answer; Huginn pins the same bytes). The Discord posting actuator, its permit and its RUDP listener are removed. +### Addressing facts (`chat_pending.v3`) + +The mouth wakes no one; the heartbeat and each Persona session decide who is told a delivery (Huginn `address.rs`, ruling `eureka-body:ruling:addressed-delivery-only`). Telling every Persona every message made 71.5% of 4,351 Persona turns on 2026-11-10 do nothing. The rule stays in one place, Huginn's, because two of its inputs are not Matrix facts: the Persona's stewardship records (her projects' chat names) and the sender's role (operator or Mind). The mouth owns the other half: what Matrix itself says, per Persona, for the delivery. Matrix verifies the sender's identity and nothing else: `tag` and `room` are the sender's claim in `m.mentions` (the homeserver does not check the text against it, nor that the sender may use `@room`), and `reply` is checked against the event it targets, which the mouth reads as her account. + +A client asks for the facts by the kind it polls with: `gamecult.persona.chat_pending_request.v2` (the v1 tuple, `[persona, after_seq]`) is answered with `gamecult.persona.chat_pending.v3`; a v1 request still gets the 12-field `chat_pending.v2` answer unchanged. A v3 delivery is the 12 fields of the v2 delivery, then: + +| Field | Type | Meaning | +|---|---|---| +| 12 `basis` | `"direct"`, `"tag"`, `"reply"`, `"room"` or nil | Why the message is for this Persona, by what Matrix says, first that holds: `direct` the room is one of her DMs; `tag` `m.mentions.user_ids` lists her id, or the client sent no `m.mentions` at all and an `` in the HTML body is exactly her `matrix.to` link; `reply` it replies to, or is in a thread under, an event her account wrote (her Self run speaks as her account, so its words count; any member's message in a thread under one of her events is a `reply` too, and the target is read once per room and event, a failure for a minute); `room` an `@room` mention. A `notice` or an `edit` costs no reply lookup. Known limit: lookups run one at a time inside the `/sync` round (10 s each, at most two per reply), so a slow homeserver slows the round; they are not parallelised or capped per round. | +| 13 `form` | `"message"`, `"notice"` or `"edit"` | `m.notice` is an automated line; `m.replace` is an edit of an earlier message. Only a `message` can address anyone. | + +What the mouth cannot know, and so what a nil `basis` means: **unproven, not "not for her"**. Her handle and public name in the text (any casing, with or without `@`, `'s`), the names her stewardships list, a project's repo name, and whether the sender is the operator or a Mind are all matched by Huginn, not here. A reply whose target the homeserver will not give is nil and logged. The mouth also cannot see whether a message was meant for her without naming her. Every delivery is still delivered, claimed tags included: nothing is dropped in the mouth, so the Huginn rule cannot lose a message the mouth had no way to judge. + +Proposed runtime half (follow-up `persona-wakes-on-address-only`): + +1. Poll with `chat_pending_request.v2` and parse `chat_pending.v3`. +2. A delivery is *addressed* to a Persona when `form == "message"` and either `basis` is `direct`, `tag` or `reply`, or `address::addresses` matches her names or her projects' names on the text. The off-beat wake keeps the sender gate of `heartbeat.rs` `wakes` (the operator or another Mind of the roster): a stranger's tag, `@room` or DM is delivered but wakes no one off the beat. `basis == "room"` is the runtime's policy to weigh; the mouth only reports it. +3. A real-time wake, a live session's tell and the stagger apply to addressed deliveries only. A `notice` or an `edit` never wakes, whatever its basis. +4. A delivery that is not addressed is acked in sequence with the rest, so the position moves (the mouth stops reading at 500 unacked deliveries); it is not lost: Matrix is the record, and the next addressed delivery's `context` (up to 20 lines) and a beat's own room read show it. Reading the tail on a scheduled beat is a runtime read of Matrix, a follow-up of the ruling `wake-reads-room-tail`; the mouth adds no endpoint for it. +5. `basis` and `form` are evidence, not authority: a sender's claim is weighed by the runtime's own rule (`address.rs` for names, the sender gate for who may wake her); the mouth reports what the event says and what only Matrix could look up (the author of a reply target). + +Golden bytes: `tests/fixtures/persona-chat-v5/`; Huginn pins the same bytes. + + Bifrost admits an observation only when guild, channel, target runtime, canonical repository, Persona, producer, and producer runtime match a preconfigured `bifrost.persona_feedback.target_binding.v0` document in Bifrost's private provider store. That binding also owns source visibility, data classification, and the delivery-store target. Bifrost never infers public visibility from Discord identifiers. It classifies the author using its own `bifrost.discord.actor_link.v0` state: a match yields `linked_governance_feedback`; absence yields `unlinked_social_feedback`. Neither class grants adoption, work, release, or deployment authority. `tools/persona-feedback.mjs enroll-identity` creates the Bifrost service's Ed25519 seed with exclusive creation and mode `0600`, then exports its public MessagePack trust anchor. The private seed belongs under Bifrost's service-private state (default `/var/lib/gamecult/bifrost/persona-feedback/persona-feedback-ed25519.seed`) and is checked for regular-file type, exact Unix mode, and service-user ownership every time it signs. VoidBot publishes pending observations only; it neither pumps admission nor receives the signing path. diff --git a/tests/fixtures/persona-chat-v5/gamecult.persona.chat_pending.v3.tag.msgpack b/tests/fixtures/persona-chat-v5/gamecult.persona.chat_pending.v3.tag.msgpack new file mode 100644 index 0000000000000000000000000000000000000000..006f6cf6f6bbd6d289589bb4ebd1ec33d3ef4b36 GIT binary patch literal 197 zcmbQVEVZ;KH9K+QJoatM`30#(i6!|(R_TeksmY}|C3^Wq=^GUjlaf(H7OSKhZgR*? zElETXUg8TBS(A~PlasFiG4wnG1LG{+hK&ppCvS0p2%+d$o|;&clbKosRkv1R5>{oK Z9P;wZQ0+Q&U~x%e`tsb=;^M^gQ~ d.eventId), ["$e1"]); assert.deepEqual(pending.deliveries[0], delivery, "sender, mentions and the DM flag cross the wire exactly"); + reply = nextReply(); + put(docs.kinds.pendingRequestV3, "eureka", docs.pendingRequestTuple({ persona: "eureka", afterSeq: 0 })); + document = await reply; + assert.equal(document.schemaId, docs.kinds.pendingV3, "the kind asked by names the answer"); + assert.deepEqual(docs.parsePendingV3(msgpack.decode(document.payload)).deliveries[0], { ...delivery, basis: null, form: "message" }); + put(docs.kinds.ack, "eureka:7", docs.ackTuple({ persona: "eureka", throughSeq: 7 })); reply = nextReply(); put(docs.kinds.sendRequest, "req-2", docs.sendRequestTuple({ requestId: "req-2", persona: "eureka", roomAlias: "#operator:gamecult.org", text: "muted" })); @@ -311,6 +317,24 @@ for (const [name, { tuple, parse }] of Object.entries(v3Samples)) { }); } +// The pending answer v3 (ChatDelivery v4, basis and form): the bytes Huginn pins in its crates/mind-body/tests/fixtures/persona-chat-v5/. +const v5Dir = resolve(root, "tests", "fixtures", "persona-chat-v5"); +const v5Samples = { + "gamecult.persona.chat_pending_request.v2": { tuple: docs.pendingRequestTuple({ persona: "eureka", afterSeq: 3 }), parse: docs.parsePendingRequest }, + "gamecult.persona.chat_pending.v3.tag": { tuple: docs.pendingV3Tuple({ persona: "eureka", deliveries: [{ ...delivery, isDirect: false, basis: "tag", form: "message" }] }), parse: docs.parsePendingV3 }, + "gamecult.persona.chat_pending.v3.unproven-notice": { tuple: docs.pendingV3Tuple({ persona: "eureka", deliveries: [{ ...delivery, isDirect: false, mentions: [], basis: null, form: "notice" }] }), parse: docs.parsePendingV3 }, +}; +for (const [name, { tuple, parse }] of Object.entries(v5Samples)) { + test(`golden fixture: ${name} is byte-stable and parses back`, async () => { + const bytes = Buffer.from(msgpack.encode(tuple)); + const file = resolve(v5Dir, `${name}.msgpack`); + if (process.env.UPDATE_FIXTURES === "1") { await mkdir(v5Dir, { recursive: true }); await writeFile(file, bytes); } + assert.deepEqual(bytes, await readFile(file), "wire bytes changed: Huginn decodes these fixtures"); + assert.deepEqual(parse(msgpack.decode(bytes)), parse(tuple)); + if (name.includes("pending.v3")) assert.equal(msgpack.decode(bytes)[1][0].length, 14); + }); +} + // The send pair v2: the bytes the Huginn organ pins from its side. const v2Dir = resolve(root, "tests", "fixtures", "persona-chat-v2"); const v2Request = basis => docs.sendRequestV2Tuple({ requestId: "req-1", persona: "eureka", roomAlias: "#swarm:gamecult.org", text: "hi", basis }); diff --git a/tests/persona-matrix-mouth.test.mjs b/tests/persona-matrix-mouth.test.mjs index 1e33f64..9458370 100644 --- a/tests/persona-matrix-mouth.test.mjs +++ b/tests/persona-matrix-mouth.test.mjs @@ -10,7 +10,7 @@ import { createImageStore, RETAIN_MS } from "../tools/persona-chat-images.mjs"; import { personaStoreRoot } from "./image-bytes.mjs"; import * as docs from "../tools/persona-chat-documents.mjs"; import { gif, png, svg } from "./image-bytes.mjs"; -import { createMouth, bodyText, createBrakeReader, guarded, redact, roomsFor, projectRoomsVariable } from "../tools/persona-matrix-mouth.mjs"; +import { createMouth, pageOf, bodyText, createBrakeReader, guarded, redact, roomsFor, projectRoomsVariable } from "../tools/persona-matrix-mouth.mjs"; const TOKEN = "syt_SECRET_TOKEN_9f3"; const HS_NAME = "gamecult.test"; @@ -1303,3 +1303,175 @@ test("a read report that throws does not wedge the room: the next report still p assert.deepEqual(unhandled, [], "no rejection escapes the tail's cleanup"); assert.equal(hs.receipts().length, 1); }); + +// Addressing facts (docs/bridge.md, "Addressing facts"): why a delivery is for this Persona, as the heartbeat reads it. +// Each case is one message as a room member sends it, with the basis and form the mouth hands the consumer. +const META = `@meta:${HS_NAME}`; +const withContent = (id, sender, content, ts = 3000) => ({ type: "m.room.message", event_id: id, sender, origin_server_ts: ts, content: { msgtype: "m.text", ...content } }); +async function deliver(events, { persona = {}, preload } = {}) { + const hs = await fakeHomeserver({ + preload, + sync: since => since === null ? { next_batch: "b0" } : { next_batch: "b1", ...joined({ [ROOM]: { timeline: { events } } }) }, + }); + try { + const mouth = mouthFor(hs, persona); + await mouth.syncOnce(); + return { made: await mouth.syncOnce(), hs, mouth }; + } finally { await hs.close(); } +} +const facts = made => made.map(d => [d.eventId, d.basis, d.form]); + +test("addressing facts: the operator speaking untagged proves nothing, tagged proves a tag, and the tag outranks @room", async () => { + const { made } = await deliver([ + withContent("$plain", META, { body: "everyone, status?" }), + withContent("$tag", META, { body: "eureka, status?", "m.mentions": { user_ids: [SELF] } }), + withContent("$both", META, { body: "all hands", "m.mentions": { user_ids: [SELF], room: true } }), + withContent("$room", META, { body: "@room standup", "m.mentions": { room: true } }), + ]); + assert.deepEqual(facts(made), [["$plain", null, "message"], ["$tag", "tag", "message"], ["$both", "tag", "message"], ["$room", "room", "message"]]); +}); + +test("addressing facts: a tag of another Mind is no basis for this one, and is one for that one", async () => { + const hs = await fakeHomeserver({ + users: { [TOKEN]: SELF, [TOKEN_NOVA]: NOVA }, + sync: since => since === null ? { next_batch: "b0", ...joined({ [ROOM]: { timeline: { events: [] } } }) } : { next_batch: "b1", ...joined({ [ROOM]: { timeline: { events: [withContent("$n", META, { body: "nova?", "m.mentions": { user_ids: [NOVA] } })] } } }) }, + }); + try { + const eureka = mouthFor(hs); + const nova = mouthFor(hs, { persona: "nova", token: TOKEN_NOVA }); + for (const m of [eureka, nova]) await m.syncOnce(); + assert.deepEqual([facts(await eureka.syncOnce()), facts(await nova.syncOnce())], [[["$n", null, "message"]], [["$n", "tag", "message"]]]); + } finally { await hs.close(); } +}); + +test("addressing facts: a name in the text is not a basis; the mouth does not know names, so every casing and alias reaches the heartbeat with null", async () => { + const bodies = ["Eureka, look", "EUREKA", "eureka's PR", "@eureka:gamecult.test please", "Bifrost needs a look"]; + const { made } = await deliver(bodies.map((body, i) => withContent(`$n${i}`, META, { body }))); + assert.deepEqual(made.map(d => d.basis), [null, null, null, null, null]); + assert.deepEqual(made.map(d => d.text), bodies, "the text is delivered whole for the heartbeat's own name match"); +}); + +test("addressing facts: a DM is direct whatever it says or carries, a notice in it included", async () => { + const DM = "!dm:gamecult.test"; + const hs = await fakeHomeserver({ + direct: { [META]: [DM] }, + sync: since => since === null ? { next_batch: "b0" } : { next_batch: "b1", ...joined({ [DM]: { timeline: { events: [withContent("$dm", META, { body: "hi" }), withContent("$dmn", META, { msgtype: "m.notice", body: "automated" })] } } }) }, + }); + try { + const mouth = mouthFor(hs); + await mouth.syncOnce(); + assert.deepEqual(facts(await mouth.syncOnce()), [["$dm", "direct", "message"], ["$dmn", "direct", "notice"]]); + } finally { await hs.close(); } +}); + +test("addressing facts: a notice that tags her carries the tag and the notice form, so the consumer can refuse to wake on it", async () => { + const { made } = await deliver([withContent("$w", `@root-watch:${HS_NAME}`, { msgtype: "m.notice", body: "disk 91%", "m.mentions": { user_ids: [SELF] } })]); + assert.deepEqual(facts(made), [["$w", "tag", "notice"]]); +}); + +test("addressing facts: an edit is its own form and is never read as a new message", async () => { + const { made } = await deliver([withContent("$e", META, { body: "* fixed", "m.new_content": { msgtype: "m.text", body: "fixed" }, "m.relates_to": { rel_type: "m.replace", event_id: "$orig" } })]); + assert.deepEqual(facts(made), [["$e", null, "edit"]]); +}); + +test("addressing facts: a reply to her own words is a reply, in a room and in a thread, and her Self run's words are hers", async () => { + const mine = withContent("$mine", SELF, { body: "my earlier say" }, 1000); + const theirs = withContent("$theirs", `@nibu:${HS_NAME}`, { body: "someone else's" }, 1001); + const reply = to => ({ "m.relates_to": { "m.in_reply_to": { event_id: to } } }); + const thread = (root, fallback) => ({ "m.relates_to": { rel_type: "m.thread", event_id: root, "m.in_reply_to": { event_id: fallback }, is_falling_back: true } }); + const { made } = await deliver([ + withContent("$r1", META, { body: "yes", ...reply("$mine") }), + withContent("$r2", META, { body: "no", ...reply("$theirs") }), + withContent("$r3", META, { body: "in thread under mine", ...thread("$mine", "$theirs") }), + withContent("$r4", META, { body: "in thread under theirs", ...thread("$theirs", "$theirs") }), + withContent("$r5", META, { body: "reply, and tagged", ...reply("$mine"), "m.mentions": { user_ids: [SELF] } }), + ], { preload: { [ROOM]: [mine, theirs] } }); + assert.deepEqual(facts(made), [["$r1", "reply", "message"], ["$r2", null, "message"], ["$r3", "reply", "message"], ["$r4", null, "message"], ["$r5", "tag", "message"]]); +}); + +test("addressing facts: a reply target the homeserver will not give is null and logged without the token, never a guess", async () => { + const logs = []; + const { made } = await deliver([withContent("$r", META, { body: "re", "m.relates_to": { "m.in_reply_to": { event_id: "$gone" } } })], { persona: { log: l => logs.push(l) } }); + assert.deepEqual(facts(made), [["$r", null, "message"]]); + assert.ok(logs.some(l => l.includes("reply target $gone not read")) && logs.every(l => !l.includes(TOKEN))); +}); + +test("addressing facts: a client that sends no m.mentions is a tag when its body links her matrix.to pill; one that sends m.mentions without her is not", async () => { + const pill = id => `Eureka: look`; + const { made } = await deliver([ + withContent("$p1", META, { body: "Eureka: look", format: "org.matrix.custom.html", formatted_body: pill(SELF) }), + withContent("$p2", META, { body: "Eureka: look", format: "org.matrix.custom.html", formatted_body: pill(encodeURIComponent(SELF)) }), + withContent("$p3", META, { body: "Nova: look", format: "org.matrix.custom.html", formatted_body: pill(NOVA) }), + withContent("$p4", META, { body: "Eureka: look", format: "org.matrix.custom.html", formatted_body: pill(SELF), "m.mentions": {} }), + ]); + assert.deepEqual(made.map(d => d.basis), ["tag", "tag", null, null]); +}); + +test("addressing facts: her own messages are no delivery at all, so no basis can wake her on her own words", async () => { + const { made } = await deliver([withContent("$self", SELF, { body: "my own", "m.mentions": { user_ids: [SELF] } })]); + assert.deepEqual(made, []); +}); + +test("addressing facts: the v3 answer carries basis and form; the v2 answer a v1 request gets is the unchanged 12 fields", async () => { + const { made } = await deliver([withContent("$t", META, { body: "x", "m.mentions": { user_ids: [SELF] } })]); + const pack = { encode: v => Buffer.from(JSON.stringify(v)) }; + assert.equal(docs.parsePendingV3(pageOf("eureka", made, pack, () => {}, true)).deliveries[0].basis, "tag"); + const v2 = pageOf("eureka", made, pack); + assert.equal(v2[1][0].length, 12); + assert.equal(docs.parsePending(v2).deliveries[0].basis, undefined); + assert.throws(() => docs.parseDeliveryV3([...docs.deliveryTuple(made[0]), "tag", "poem"]), /unknown delivery form/); + assert.throws(() => docs.parseDeliveryV3([...docs.deliveryTuple(made[0]), "everyone", "message"]), /unknown delivery basis/); + assert.throws(() => docs.parseDeliveryV3(docs.deliveryTuple(made[0])), /14-field/); +}); + +test("addressing facts: a hit read for a reply in one room is not lent to a reply in another room, and a target the server places elsewhere is null", async () => { + const mine = { ...withContent("$mine", SELF, { body: "mine" }, 1000), room_id: ROOM }; + const reply = id => withContent(id, META, { body: "re", "m.relates_to": { "m.in_reply_to": { event_id: "$mine" } } }); + const hs = await fakeHomeserver({ + preload: { [ROOM]: [mine], "!operator:gamecult.test": [{ ...mine, room_id: "!elsewhere:gamecult.test" }] }, + sync: since => since === null ? { next_batch: "b0" } : { next_batch: "b1", ...joined({ [ROOM]: { timeline: { events: [reply("$prime")] } }, "!operator:gamecult.test": { timeline: { events: [reply("$spoof")] } } }) }, + }); + try { + const mouth = mouthFor(hs); + await mouth.syncOnce(); + assert.deepEqual(facts(await mouth.syncOnce()), [["$prime", "reply", "message"], ["$spoof", null, "message"]]); + } finally { await hs.close(); } +}); + +test("addressing facts: a pill is her exact matrix.to anchor: a longer id, a non-link attribute and a quoted reply are no tag", async () => { + const html = h => ({ body: "x", format: "org.matrix.custom.html", formatted_body: h }); + const { made } = await deliver([ + withContent("$a", META, html(`x`)), + withContent("$b", META, html(`x`)), + withContent("$c", META, html(`Eureka`)), + withContent("$d", META, html(`
her said
ok`)), + ]); + assert.deepEqual(made.map(d => d.basis), [null, null, "tag", null]); +}); + +test("addressing facts: an edit or a notice replying to her costs no target fetch", async () => { + const to = id => ({ "m.relates_to": { "m.in_reply_to": { event_id: id } } }); + const { made, hs } = await deliver([ + withContent("$n", META, { msgtype: "m.notice", body: "n", ...to("$noticed") }), + withContent("$e", META, { body: "* e", "m.relates_to": { rel_type: "m.replace", event_id: "$orig", "m.in_reply_to": { event_id: "$edited" } } }), + ]); + assert.deepEqual(facts(made), [["$n", null, "notice"], ["$e", null, "edit"]]); + assert.equal(hs.seen.filter(r => r.path.includes("/event/")).length, 0); +}); + +test("addressing facts: a target that failed is not refetched within a minute, and is after it", async () => { + let t = 1_000_000; + const rel = { "m.relates_to": { "m.in_reply_to": { event_id: "$gone" } } }; + const round = id => joined({ [ROOM]: { timeline: { events: [withContent(id, META, { body: id, ...rel })] } } }); + const script = { null: { next_batch: "b0" }, b0: { next_batch: "b1", ...round("$r1") }, b1: { next_batch: "b2", ...round("$r2") }, b2: { next_batch: "b3", ...round("$r3") } }; + const hs = await fakeHomeserver({ sync: since => script[since] ?? { next_batch: since } }); + const fetched = () => hs.seen.filter(r => r.path.includes("/event/")).length; + try { + const mouth = mouthFor(hs, { now: () => t }); + await mouth.syncOnce(); await mouth.syncOnce(); + t += 59_000; await mouth.syncOnce(); + assert.equal(fetched(), 1, "inside the minute: not refetched"); + t += 2_000; await mouth.syncOnce(); + assert.equal(fetched(), 2, "past the minute: read again"); + } finally { await hs.close(); } +}); diff --git a/tools/persona-chat-documents.mjs b/tools/persona-chat-documents.mjs index f5c6e67..4867901 100644 --- a/tools/persona-chat-documents.mjs +++ b/tools/persona-chat-documents.mjs @@ -9,6 +9,8 @@ import { IMAGE_PATH, MAX_BYTES, MIMES, extensionOf } from "./persona-chat-images export const kinds = Object.freeze({ pendingRequest: "gamecult.persona.chat_pending_request.v1", pending: "gamecult.persona.chat_pending.v2", + pendingRequestV3: "gamecult.persona.chat_pending_request.v2", // the same tuple as v1; asking by this kind is asking for the v3 answer + pendingV3: "gamecult.persona.chat_pending.v3", ack: "gamecult.persona.chat_ack.v1", read: "gamecult.persona.chat_read.v1", sendRequest: "gamecult.persona.chat_send_request.v1", @@ -53,6 +55,12 @@ export function parsePendingRequest(t) { return { persona: text(persona, "persona"), afterSeq: seq(afterSeq, "after_seq") }; } +// What the mouth can establish about a delivery from Matrix alone, for the Persona it is delivered to (see docs/bridge.md, "Addressing facts"). +// `basis` is why the message is for her, or null when the mouth has no proof (not that it is not for her: her names, her projects' names and +// the sender's role are not the mouth's to know). `form` is the event's kind: only a "message" can address anyone. +export const bases = Object.freeze(["direct", "tag", "reply", "room"]); +export const forms = Object.freeze(["message", "notice", "edit"]); + export function ackTuple({ persona, throughSeq }) { return [text(persona, "persona"), seq(throughSeq, "through_seq")]; } @@ -190,6 +198,23 @@ function parseImage(t) { imageTuple({ path, mime, size, sha256 }); return { path, mime, size, sha256 }; } +// The v3 delivery: the 12 fields above, then basis and form. +export function deliveryV3Tuple(d) { + return [...deliveryTuple(d), basis(d.basis ?? null), form(d.form ?? "message")]; +} +export function parseDeliveryV3(t) { + const fields = tuple(t, 14, "delivery_v3"); + return { ...parseDelivery(fields.slice(0, 12)), basis: basis(fields[12]), form: form(fields[13]) }; +} +function basis(v) { + if (v !== null && !bases.includes(v)) throw new Error(`unknown delivery basis ${String(v)}`); + return v; +} +function form(v) { + if (!forms.includes(v)) throw new Error(`unknown delivery form ${String(v)}`); + return v; +} + export function parseDelivery(t) { const [s, roomAlias, roomId, eventId, sender, senderDisplay, body, atUnixMs, context, mentions, isDirect, image] = tuple(t, 12, "delivery"); if (!Array.isArray(context)) throw new Error("delivery context must be a list"); @@ -212,6 +237,15 @@ export function parsePending(t) { return { persona: text(persona, "persona"), deliveries: deliveries.map(parseDelivery) }; } +export function pendingV3Tuple({ persona, deliveries }) { + return [text(persona, "persona"), deliveries.map(deliveryV3Tuple)]; +} +export function parsePendingV3(t) { + const [persona, deliveries] = tuple(t, 2, "chat_pending_v3"); + if (!Array.isArray(deliveries)) throw new Error("chat_pending deliveries must be a list"); + return { persona: text(persona, "persona"), deliveries: deliveries.map(parseDeliveryV3) }; +} + // The CultNet document_put_raw envelope every chat_* document travels in. export function envelope({ kind, recordKey, payload, sourceRuntimeId, sourceAgentId, sourceRole }) { return { diff --git a/tools/persona-matrix-mouth.mjs b/tools/persona-matrix-mouth.mjs index 1fca153..439816e 100644 --- a/tools/persona-matrix-mouth.mjs +++ b/tools/persona-matrix-mouth.mjs @@ -5,7 +5,9 @@ // from another sender becomes a delivery kept in memory, carrying its sender id, // its m.mentions user ids and whether the room is a DM. The /sync position // (next_batch) is persisted only when an ack covers it, so a restart before the -// ack re-delivers instead of losing. Reading wakes nothing: the heartbeat polls +// ack re-delivers instead of losing. A v3 delivery also carries what Matrix proves about why it is for her (basis: direct, tag, +// reply, room; form: message, notice, edit); names and roles are the heartbeat's to match, not the mouth's (docs/bridge.md, +// "Addressing facts"). Reading wakes nothing: the heartbeat polls // with a non-consuming chat_pending_request and decides who runs. An ack posts nothing to Matrix. Her public // m.read receipt follows a chat_read document from Huginn, sent when a delivery has gone into the model's // input: the mouth posts m.read at that event id only, for an event it delivered to her, never behind one @@ -63,6 +65,9 @@ const NOT_E2EE = "This room is not end-to-end encrypted: the homeserver can read const dayOf = ms => new Date(ms).toISOString().slice(0, 10); const HEAD_PAGE = 50; // the room head a gated send reads; also the most ids a RoomMoved receipt names +// The kind of an event for a delivery: an edit (m.replace) and an automated notice (m.notice) are not messages anyone is addressed by. +const formOf = content => content?.["m.relates_to"]?.rel_type === "m.replace" ? "edit" : content?.msgtype === "m.notice" ? "notice" : "message"; + // A message line: a text event with a sender, not an edit. const isLine = e => e?.type === "m.room.message" && typeof e.content?.body === "string" && typeof e.sender === "string" && e.sender !== "" && e.content?.["m.relates_to"]?.rel_type !== "m.replace"; // Whether `event` moves the room under a Persona whose account is `selfId`: someone else's line that is @@ -284,6 +289,43 @@ export function createMouth({ return lines; } + // Why a delivered event is for this Persona, from what Matrix says and nothing else: "direct" (a DM), "tag" (m.mentions lists her + // id, or, from a client that sends no m.mentions at all, the body links her matrix.to pill), "reply" (it replies to, or sits in a + // thread under, an event her account wrote, so her Self run's words count as hers), "room" (an @room mention). In that order; null + // when none holds. Her names, her projects' names and the sender's role are the heartbeat's to match (Huginn address.rs), never the + // mouth's. A reply target that cannot be read is null and logged: unknown, not "not hers". + async function basisOf(roomId, event) { + if (directRooms.has(roomId)) return "direct"; + const mentions = event.content["m.mentions"]; + if (Array.isArray(mentions?.user_ids) ? mentions.user_ids.includes(selfId) : mentions === undefined && pillFor(event.content.formatted_body)) return "tag"; + if (formOf(event.content) === "message" && await repliesToMe(roomId, event.content["m.relates_to"])) return "reply"; // an edit or a notice wakes no one: no fetch for it + return mentions?.room === true ? "room" : null; + } + // A pill is an anchor whose href is exactly her matrix.to link; a longer id, an attribute that merely contains one and a quoted reply do not count. + const pillFor = html => typeof html === "string" && [...html.replace(/[\s\S]*?<\/mx-reply>/gi, "").matchAll(/]*?href\s*=\s*(["'])(.*?)\1/gi)].some(([, , href]) => [selfId, encodeURIComponent(selfId)].some(id => href === `https://matrix.to/#/${id}`)); + const authors = new Map(); // `${room id} ${event id}` -> { sender, at }: read results, a failure kept for NEGATIVE_MS, bounded; the key holds the room so another room's reply cannot borrow a hit + const NEGATIVE_MS = 60 * 1000; + async function authorOf(roomId, eventId) { + const key = `${roomId} ${eventId}`; + const known = authors.get(key); + if (known && (known.sender !== null || now() - known.at < NEGATIVE_MS)) return known.sender; + let sender = null; + try { + const target = await matrix("GET", `/rooms/${encodeURIComponent(roomId)}/event/${encodeURIComponent(eventId)}`, undefined, API, AbortSignal.timeout(sendTimeoutMs)); + if (target?.room_id === undefined || target.room_id === roomId) sender = typeof target?.sender === "string" ? target.sender : null; // an id from another room is not an answer about this one + } catch (error) { + say(`reply target ${eventId} not read: ${error.message}`); + } + authors.set(key, { sender, at: now() }); + if (authors.size > DELIVERED_INDEX) authors.delete(authors.keys().next().value); + return sender; + } + async function repliesToMe(roomId, relates) { + const targets = new Set([relates?.["m.in_reply_to"]?.event_id, relates?.rel_type === "m.thread" ? relates.event_id : undefined].filter(id => typeof id === "string" && id !== "")); + for (const id of targets) if (await authorOf(roomId, id) === selfId) return true; + return false; + } + // The image of one delivered m.image event: { image } (a stored reference) or { refused } (a reason word). With no // store nothing is attempted and nothing is said. The url is the event's own content.url; encrypted events and // anything the checks refuse leave only the text line. @@ -351,12 +393,13 @@ export function createMouth({ const { image = null, refused = null } = await imageOf(roomId, event.content); if (refused !== null) say(`image refused: ${refused}`); const mentioned = event.content["m.mentions"]?.user_ids; + const basis = await basisOf(roomId, event); made.push({ seq: ++seqCounter, roomAlias: alias, roomId, eventId: event.event_id, sender: event.sender, senderDisplay: displayNames.get(event.sender) ?? localpart(event.sender), text: bodyText(event.content, docs.limits.textBytes, refused), atUnixMs, context, image, mentions: Array.isArray(mentioned) ? mentioned.filter(id => typeof id === "string" && id !== "" && id.length <= 255).slice(0, docs.limits.mentions) : [], - isDirect: directRooms.has(roomId), + isDirect: directRooms.has(roomId), basis, form: formOf(event.content), }); if (image) images.hold(image.path, seqCounter); } @@ -628,12 +671,12 @@ export function connectionIdFor(connectionKey) { // The reply to a chat_pending_request: deliveries oldest first, each encoded on its own so one that // will not encode is skipped and logged without silencing the rest, up to the reply byte budget. // The caller acks what it has read and asks again. -export function pageOf(persona, deliveries, msgpack, log = () => {}) { +export function pageOf(persona, deliveries, msgpack, log = () => {}, v3 = false) { const page = []; let bytes = 0; for (const delivery of deliveries) { let tuple, size; - try { tuple = docs.deliveryTuple(delivery); size = msgpack.encode(tuple).length; } catch (error) { + try { tuple = (v3 ? docs.deliveryV3Tuple : docs.deliveryTuple)(delivery); size = msgpack.encode(tuple).length; } catch (error) { log(`delivery ${delivery?.seq} skipped: ${error?.message ?? error}`); continue; } @@ -661,7 +704,7 @@ export async function startChatEndpoint({ mouths, runtime, port, connectionKey, const respond = (peer, persona, kind, recordKey, tuple) => server.send(peer, "schema", msgpack.encode(cultnet.encodeCultNetMessageForWire( docs.envelope({ kind, recordKey, payload: msgpack.encode(tuple), sourceRuntimeId: runtimeId, sourceAgentId: persona, sourceRole: "bifrost-persona-matrix-mouth" }), "cultnet.schema.v0"))); const refuseUnknown = (peer, persona) => respond(peer, persona, docs.kinds.refusal, persona, docs.refusalTuple({ persona, refusal: "UnknownPersona" })); - const served = [docs.kinds.pendingRequest, docs.kinds.ack, docs.kinds.read, docs.kinds.sendRequest, docs.kinds.sendRequestV2, docs.kinds.openDirectRequest]; + const served = [docs.kinds.pendingRequest, docs.kinds.pendingRequestV3, docs.kinds.ack, docs.kinds.read, docs.kinds.sendRequest, docs.kinds.sendRequestV2, docs.kinds.openDirectRequest]; const handle = async ({ peer, channelId, payload }) => { try { if (channelId !== "schema") throw new Error("chat request arrived on a foreign channel"); @@ -678,11 +721,12 @@ export async function startChatEndpoint({ mouths, runtime, port, connectionKey, const mouth = mouths.get(request.persona); const receipt = mouth ? await mouth.openDirect(request) : { requestId: request.requestId, outcome: "UnknownPersona", roomId: null, eventId: null }; respond(peer, request.persona, docs.kinds.openDirectReceipt, receipt.requestId, docs.openDirectReceiptTuple(receipt)); - } else if (document.schemaId === docs.kinds.pendingRequest) { + } else if (document.schemaId === docs.kinds.pendingRequest || document.schemaId === docs.kinds.pendingRequestV3) { + const v3 = document.schemaId === docs.kinds.pendingRequestV3; // the kind asked by names the answer: v1 gets the 12-field v2 answer, as before const request = docs.parsePendingRequest(tuple); const mouth = mouths.get(request.persona); if (!mouth) return refuseUnknown(peer, request.persona); - respond(peer, request.persona, docs.kinds.pending, request.persona, pageOf(request.persona, mouth.pendingAfter(request.afterSeq), msgpack, log)); + respond(peer, request.persona, v3 ? docs.kinds.pendingV3 : docs.kinds.pending, request.persona, pageOf(request.persona, mouth.pendingAfter(request.afterSeq), msgpack, log, v3)); } else if (document.schemaId === docs.kinds.ack) { const request = docs.parseAck(tuple); const mouth = mouths.get(request.persona); From be44a09d52a83f7d45d8818f13b85cd9b929206f Mon Sep 17 00:00:00 2001 From: Heimdall Date: Wed, 11 Nov 2026 06:39:35 +0000 Subject: [PATCH 13/26] OIDC: refuse reserved localparts at authorize; gap list in oidc-issuer.md (#49) --- docs/oidc-issuer.md | 20 ++++++++++ .../Features/Oidc/OidcEndpoints.cs | 9 +++++ tests/Bifrost.Web.Tests/OidcIssuerTests.cs | 39 +++++++++++++++++++ .../Bifrost.Web.Tests/Support/OidcTestKit.cs | 27 +++++++++++++ 4 files changed, 95 insertions(+) diff --git a/docs/oidc-issuer.md b/docs/oidc-issuer.md index a7f59a4..beaa6df 100644 --- a/docs/oidc-issuer.md +++ b/docs/oidc-issuer.md @@ -70,6 +70,10 @@ The authorize endpoint reads the cookie session and the same actor facts the `/A `error_description=The account is not an active GameCult member.` - Signed in, `Active`, but `NormalizedBifrostIdentity` empty: `error=access_denied` with `error_description=The account has no Bifrost identity.` +- Signed in, `Active`, but the identity is a reserved localpart (`BifrostIdentityService.IsReserved`: the 13 Synapse + accounts and the system names, the same 28 as MAS `banned_usernames`): `error=access_denied` with + `error_description=The account's Bifrost identity is reserved.` Creation refuses these names, but a row minted before + that refusal may hold one and there is no rename path, so the issuer never vouches for a Mind's or a system's localpart. The refusal is at authorize only: a code or access token issued for such a row before this check deployed stays redeemable until it expires (minutes), and token and userinfo re-read membership, not the name. Whether any such row exists is not known from the code; one query of `UserAccounts` against the 28 names would say. - Signed in and `Active`: a code is issued. - Token endpoint: membership is read again; an account no longer `Active` gets `invalid_grant` and no tokens. @@ -107,6 +111,22 @@ Configure MAS's upstream provider with the issuer above, the client id and secre from `preferred_username`. MAS mints the Matrix localpart on first sign-in, so the claim must never change for an account. +## Not built + +Checked against the Window B list (2026-11-11). Present and tested: discovery, JWKS, authorize/token/userinfo, PKCE S256, +the one client, claims to a Matrix-legal localpart (`[a-z0-9._-]`, 3-80, lower case), reserved names at creation and now at +issuance, back-channel logout. Not present: + +- **Rate limits and pruning.** No limiter sits on `/connect/*`, and expired OpenIddict rows (parked authorize requests, + tokens, authorizations) are never deleted. The authorize endpoint is anonymous and its client id and redirect URI are + public, so a caller can park rows without bound. Needs a budget (`FixedWindowBudget`) and a prune. +- **A brake.** The Bifrost process has none; the deploy brake belongs to Idunn. The only off switch is removing + `Bifrost:Oidc` and restarting, or membership leaving `Active` (the reconciler then logs the person out). +- **Non-member sign-in.** The gate is `Active` membership, so an account with no membership gets `access_denied`. A "Users" + tier for chat registration, with no vote, is a policy choice not yet made; nothing here writes membership or votes. +- **MAS upstream block.** `gamecult-ops` `chat/matrix-auth/config.yaml` has no `upstream_oauth2` (provider ULID, + `on_conflict: fail`, `on_backchannel_logout: logout_all`, the redirect URI above); that lives in the ops repo. + ## Remaining scar Heimdall still mints its own account id in code (`src/app.ts` `createAccount`). That id is an authenticator handle diff --git a/src/Bifrost.Web/Features/Oidc/OidcEndpoints.cs b/src/Bifrost.Web/Features/Oidc/OidcEndpoints.cs index 35f0297..7fce895 100644 --- a/src/Bifrost.Web/Features/Oidc/OidcEndpoints.cs +++ b/src/Bifrost.Web/Features/Oidc/OidcEndpoints.cs @@ -2,6 +2,7 @@ using System.Security.Claims; using Microsoft.AspNetCore; using Bifrost.Web.Data; using Bifrost.Web.Domain; +using Bifrost.Web.Features.Membership; using Bifrost.Web.Features.Shared; using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Authentication; @@ -21,6 +22,7 @@ public static class OidcEndpoints { public const string NotActiveDescription = "The account is not an active GameCult member."; public const string NoIdentityDescription = "The account has no Bifrost identity."; + public const string ReservedIdentityDescription = "The account's Bifrost identity is reserved."; private const string OpenIddictScheme = OpenIddictServerAspNetCoreDefaults.AuthenticationScheme; private const string IdentityAuthenticationType = "Bifrost.Oidc"; @@ -58,6 +60,13 @@ public static class OidcEndpoints return Deny(Errors.AccessDenied, NoIdentityDescription); } + // The identity becomes the Matrix localpart. Creation refuses reserved names (BifrostIdentityService), but a row minted + // before that refusal may hold one, and there is no rename path: the issuer never vouches for a Mind's or a system's localpart. + if (BifrostIdentityService.IsReserved(account.NormalizedBifrostIdentity)) + { + return Deny(Errors.AccessDenied, ReservedIdentityDescription); + } + var identity = new ClaimsIdentity(IdentityAuthenticationType, Claims.Name, Claims.Role); identity.SetClaim(Claims.Subject, account.Id.ToString("D")); identity.SetClaim(Claims.PreferredUsername, account.NormalizedBifrostIdentity); diff --git a/tests/Bifrost.Web.Tests/OidcIssuerTests.cs b/tests/Bifrost.Web.Tests/OidcIssuerTests.cs index 08e8595..30098f5 100644 --- a/tests/Bifrost.Web.Tests/OidcIssuerTests.cs +++ b/tests/Bifrost.Web.Tests/OidcIssuerTests.cs @@ -139,6 +139,45 @@ public sealed class OidcIssuerTests : IClassFixture Assert.False(redirect.ContainsKey("code")); } + [Theory] + [InlineData("meta")] // an operator's or a Mind's Matrix localpart + [InlineData("eureka")] + [InlineData("admin")] + [InlineData("system")] // a system name + [InlineData("Heimdall")] // the row's casing does not matter + public async Task An_active_member_whose_identity_is_a_reserved_localpart_gets_no_code(string reserved) + { + using var factory = new OidcFixedIdentityFactory(reserved); + using var client = factory.CreateClient(ClientOptions()); + await RegisterAsync(client, $"cookie-{Guid.NewGuid():N}"); // any session: the actor is the fixed account + var request = NewRequest(); + + var final = await DriveAsync(client, request); + + var redirect = AssertRedirectToClient(final, request.State); + Assert.Equal("access_denied", redirect["error"]); + Assert.Equal(Bifrost.Web.Features.Oidc.OidcEndpoints.ReservedIdentityDescription, redirect["error_description"]); + Assert.False(redirect.ContainsKey("code")); + } + + [Theory] + [InlineData("meta2")] // look-alikes are ordinary names + [InlineData("admins")] + [InlineData("kiko-fan")] + public async Task A_name_that_only_resembles_a_reserved_localpart_is_given_a_code(string ordinary) + { + using var factory = new OidcFixedIdentityFactory(ordinary); + using var client = factory.CreateClient(ClientOptions()); + await RegisterAsync(client, $"cookie-{Guid.NewGuid():N}"); + var request = NewRequest(); + + var final = await DriveAsync(client, request); + + var redirect = AssertRedirectToClient(final, request.State); + Assert.True(redirect.ContainsKey("code")); + Assert.False(redirect.ContainsKey("error")); + } + [Theory] [InlineData("unknown-client", OidcTestKit.RedirectUri, true)] [InlineData(OidcTestKit.ClientId, "https://evil.example.test/callback", true)] diff --git a/tests/Bifrost.Web.Tests/Support/OidcTestKit.cs b/tests/Bifrost.Web.Tests/Support/OidcTestKit.cs index 231cd33..0b14d18 100644 --- a/tests/Bifrost.Web.Tests/Support/OidcTestKit.cs +++ b/tests/Bifrost.Web.Tests/Support/OidcTestKit.cs @@ -231,6 +231,33 @@ public sealed class OidcNoIdentityFactory : OidcWebApplicationFactory } } +/// +/// An OIDC host whose actor accessor reports an Active account holding the given Bifrost identity: how a row minted before +/// reserved names were refused at creation (or any identity the test needs) reaches the authorize endpoint, since +/// registration itself now refuses a reserved name. +/// +public sealed class OidcFixedIdentityFactory(string identity) : OidcWebApplicationFactory +{ + protected override void ConfigureActors(IServiceCollection services) + { + services.RemoveAll(); + services.AddScoped(_ => new FixedIdentity(identity)); + } + + private sealed class FixedIdentity(string identity) : ICurrentBifrostActorAccessor + { + public Task GetAsync(CancellationToken cancellationToken) => + Task.FromResult(new CurrentBifrostActor(true, new UserAccount + { + Id = Guid.NewGuid(), + BifrostIdentity = identity, + NormalizedBifrostIdentity = identity, // as given: the guard, not the fixture, must fold case + DisplayName = "Fixed Identity", + Membership = new Bifrost.Web.Domain.Membership { Status = MembershipStatus.Active } + })); + } +} + /// An OIDC host whose public base URL differs from the origin its requests arrive on, so the issuer cannot be request-derived. public sealed class OidcForeignIssuerFactory : OidcWebApplicationFactory { From 5201508bdc58c262bd1a3848648fea33d7ccdc1f Mon Sep 17 00:00:00 2001 From: Heimdall Date: Wed, 11 Nov 2026 08:12:48 +0000 Subject: [PATCH 14/26] OIDC issuer: address budget, token-row cap, scheduled prune, marker brake (#50) --- docs/oidc-issuer.md | 30 ++- docs/oidc-reserved-accounts.sql | 12 + .../Configuration/OidcIssuerOptions.cs | 28 ++ src/Bifrost.Web/Features/Oidc/OidcGuard.cs | 195 ++++++++++++++ .../Oidc/OidcServiceCollectionExtensions.cs | 3 + src/Bifrost.Web/Program.cs | 5 + tests/Bifrost.Web.Tests/OidcIssuerTests.cs | 247 +++++++++++++++++- .../Bifrost.Web.Tests/Support/OidcTestKit.cs | 68 ++++- 8 files changed, 581 insertions(+), 7 deletions(-) create mode 100644 docs/oidc-reserved-accounts.sql create mode 100644 src/Bifrost.Web/Features/Oidc/OidcGuard.cs diff --git a/docs/oidc-issuer.md b/docs/oidc-issuer.md index beaa6df..cb2ee08 100644 --- a/docs/oidc-issuer.md +++ b/docs/oidc-issuer.md @@ -44,6 +44,8 @@ echoes its value. | `Bifrost:Oidc:Clients:Mas:ClientId` | The client id MAS presents. | | `Bifrost:Oidc:Clients:Mas:ClientSecretPath` | File holding the client secret. | | `Bifrost:Oidc:Clients:Mas:RedirectUri` | MAS's callback URL, registered exactly. | +| `Bifrost:Oidc:BrakePath` | Optional. While a file exists at this path, `/connect/authorize`, `/token` and `/userinfo` answer 503 `temporarily_unavailable` and nothing is issued; delete the file to release. Checked per request, so no restart. Unset: no marker brake. | +| `Bifrost:Oidc:Limits:*` | `PerAddressPermits` (30), `Window` (1 min), `MaxTrackedKeys` (10000), `MaxTokenRows` (20000), `PruneInterval` (5 min). | | `Bifrost:Oidc:Clients:Mas:BackchannelLogoutUri` | Optional. MAS's back-channel logout URL, `https:///upstream/backchannel-logout/`; absolute http(s) or startup fails. Unset: no logout is sent and no authorization is revoked. | There are no development or ephemeral keys in the host. Key files are provisioned like the other @@ -111,17 +113,35 @@ Configure MAS's upstream provider with the issuer above, the client id and secre from `preferred_username`. MAS mints the Matrix localpart on first sign-in, so the claim must never change for an account. +## Limits, prune, brake + +`OidcGuardMiddleware` sits ahead of OpenIddict and guards the three endpoints, matched by path segment so a trailing slash +is no way around it. In order: the marker brake (503; a marker that cannot be read is treated as engaged); one per-address +budget over the three endpoints together (429 with `Retry-After`; the address is the connection's after forwarded-headers +handling that trusts only the local proxy, IPv6 keyed by /64); and a cap on token rows: a new authorize request (no +`request_uri`) is refused with 503 while the table holds `MaxTokenRows`. Any failure inside the guard is a 503, never a pass. +MAS's flow is a handful of requests per sign-in from one address, far under the defaults (30 a minute is about 15 sign-ins). + +There is no per-client budget. The one client's id is public and what a caller names is unverified, so a budget keyed on it +would let anyone spend the real client's permits and lock sign-in out. The bound on the anonymous endpoint is the address +budget plus the row cap, and the cap is itself a switch an attacker with many addresses can flip: it refuses new sign-ins +(never resumed ones) until the next prune frees rows, which is the bounded failure chosen over unbounded parked rows. +Not parking state before the user authenticates (as pushed authorization requests avoid by authenticating the client first) +would remove the switch, and OpenIddict's request caching does not offer that. + +`OidcPruner` runs every `PruneInterval` and deletes tokens past expiry (parked authorize requests, codes, access tokens) or no +longer valid, then authorizations no longer valid or ad hoc with no token left (younger than five minutes are left alone). +Live rows are untouched. Plain EF queries rather than OpenIddict's own prune, which uses ExecuteDelete and cannot run on the +in-memory provider the tests use. + +`docs/oidc-reserved-accounts.sql` is the read-only query for existing accounts that hold one of the 28 reserved names. + ## Not built Checked against the Window B list (2026-11-11). Present and tested: discovery, JWKS, authorize/token/userinfo, PKCE S256, the one client, claims to a Matrix-legal localpart (`[a-z0-9._-]`, 3-80, lower case), reserved names at creation and now at issuance, back-channel logout. Not present: -- **Rate limits and pruning.** No limiter sits on `/connect/*`, and expired OpenIddict rows (parked authorize requests, - tokens, authorizations) are never deleted. The authorize endpoint is anonymous and its client id and redirect URI are - public, so a caller can park rows without bound. Needs a budget (`FixedWindowBudget`) and a prune. -- **A brake.** The Bifrost process has none; the deploy brake belongs to Idunn. The only off switch is removing - `Bifrost:Oidc` and restarting, or membership leaving `Active` (the reconciler then logs the person out). - **Non-member sign-in.** The gate is `Active` membership, so an account with no membership gets `access_denied`. A "Users" tier for chat registration, with no vote, is a policy choice not yet made; nothing here writes membership or votes. - **MAS upstream block.** `gamecult-ops` `chat/matrix-auth/config.yaml` has no `upstream_oauth2` (provider ULID, diff --git a/docs/oidc-reserved-accounts.sql b/docs/oidc-reserved-accounts.sql new file mode 100644 index 0000000..8845053 --- /dev/null +++ b/docs/oidc-reserved-accounts.sql @@ -0,0 +1,12 @@ +-- Read-only. Which existing UserAccounts hold one of the 28 reserved localparts +-- (BifrostIdentityService.ReservedIdentities). Written for review; not run on live. +-- Run it in a read-only transaction by whoever holds the database: +-- psql -v ON_ERROR_STOP=1 -c 'BEGIN READ ONLY' -f docs/oidc-reserved-accounts.sql +-- Keep the list in step with ReservedIdentities; the issuer refuses these at authorize (#49). +SELECT a."Id", a."BifrostIdentity", a."NormalizedBifrostIdentity", a."CreatedAtUtc", a."LastSeenAtUtc" +FROM "UserAccounts" a +WHERE lower(a."NormalizedBifrostIdentity") IN ( + 'admin', 'aqua', 'deru', 'eureka', 'heimdall', 'ivixey', 'kiko', 'libby', 'meta', 'nibu', 'unnie', 'void', 'weksa', + 'root', 'system', 'matrix', 'synapse', 'support', 'bot', 'mod', 'moderator', 'postmaster', 'gamecult', + 'bifrost', 'idunn', 'odin', 'huginn', 'ratatoskr') +ORDER BY a."NormalizedBifrostIdentity"; diff --git a/src/Bifrost.Web/Configuration/OidcIssuerOptions.cs b/src/Bifrost.Web/Configuration/OidcIssuerOptions.cs index 13ba192..017648e 100644 --- a/src/Bifrost.Web/Configuration/OidcIssuerOptions.cs +++ b/src/Bifrost.Web/Configuration/OidcIssuerOptions.cs @@ -16,6 +16,15 @@ public sealed class OidcIssuerOptions public OidcClientsOptions Clients { get; init; } = new(); + public OidcLimitsOptions Limits { get; init; } = new(); + + /// + /// Optional path of the issuer's brake marker. While a file exists there, /connect/* answers 503 and nothing is issued; + /// remove it to release. Checked on every request (one stat), so engaging and releasing need no restart. + /// Unset: the issuer has no marker brake (removing this section and restarting is the other off switch). + /// + public string BrakePath { get; init; } = string.Empty; + /// Null when the section is absent. public static OidcIssuerOptions? FromConfiguration(IConfiguration configuration) { @@ -42,3 +51,22 @@ public sealed class OidcClientOptions /// MAS's back-channel logout URL for this provider. Empty: no logout is sent and no session is ended. public string BackchannelLogoutUri { get; init; } = string.Empty; } + +/// +/// One budget on /connect/authorize, /connect/token and /connect/userinfo together, per source address in one fixed window (a refused caller gets 429 with Retry-After), +/// and the bound on what the anonymous authorize endpoint can park. No per-client budget: the client id is public. The defaults suit one client (MAS) behind one proxy. +/// +public sealed class OidcLimitsOptions +{ + public int PerAddressPermits { get; init; } = 30; + + public TimeSpan Window { get; init; } = TimeSpan.FromMinutes(1); + + public int MaxTrackedKeys { get; init; } = 10_000; + + /// The most OpenIddict token rows (parked authorize requests, codes, access tokens) allowed to exist; a new authorize request beyond it is refused until the prune frees room. + public int MaxTokenRows { get; init; } = 20_000; + + /// How often expired and orphaned authorizations and tokens are deleted. + public TimeSpan PruneInterval { get; init; } = TimeSpan.FromMinutes(5); +} diff --git a/src/Bifrost.Web/Features/Oidc/OidcGuard.cs b/src/Bifrost.Web/Features/Oidc/OidcGuard.cs new file mode 100644 index 0000000..ccfd5e1 --- /dev/null +++ b/src/Bifrost.Web/Features/Oidc/OidcGuard.cs @@ -0,0 +1,195 @@ +using System.Net; +using Bifrost.Web.Configuration; +using Bifrost.Web.Data; +using Microsoft.EntityFrameworkCore; +using OpenIddict.EntityFrameworkCore.Models; +using Bifrost.Web.Features.Shared; +using Microsoft.Extensions.Options; +using OpenIddict.Abstractions; +using static OpenIddict.Abstractions.OpenIddictConstants; +using OpenIddict.Server.AspNetCore; + +namespace Bifrost.Web.Features.Oidc; + +/// +/// The issuer's front door, ahead of OpenIddict: the marker brake, the per-address budget, and the bound +/// on token rows. The address is the connection's, already corrected by the forwarded-headers handling that trusts only +/// the local proxy, so a caller cannot pick its own. It fails closed: anything this middleware cannot decide is a 503, never a pass. +/// +public sealed class OidcGuardMiddleware(RequestDelegate next, OidcIssuerOptions options, OidcRateLimit limit, ILogger logger) +{ + public async Task InvokeAsync(HttpContext http, IOpenIddictTokenManager tokens) + { + var endpoint = EndpointOf(http.Request.Path); + if (endpoint is null) + { + await next(http); + return; + } + + try + { + if (BrakeEngaged(options.BrakePath)) + { + await RefuseAsync(http, StatusCodes.Status503ServiceUnavailable, "temporarily_unavailable", TimeSpan.FromMinutes(1)); + return; + } + + if (limit.Spend(AddressKey(http.Connection.RemoteIpAddress)) is { } byAddress) + { + await RefuseAsync(http, StatusCodes.Status429TooManyRequests, "too_many_requests", byAddress.RetryAfter); + return; + } + + // A new authorize request parks a row for anyone who can name the (public) client; a resumed one carries request_uri and parks none. + if (endpoint == Authorize && !http.Request.Query.ContainsKey("request_uri") && await tokens.CountAsync(http.RequestAborted) >= options.Limits.MaxTokenRows) + { + await RefuseAsync(http, StatusCodes.Status503ServiceUnavailable, "temporarily_unavailable", options.Limits.PruneInterval); + return; + } + } + catch (Exception exception) when (exception is not OperationCanceledException) + { + logger.LogError("OIDC guard could not decide ({ExceptionType}); the request is refused.", exception.GetType().Name); + await RefuseAsync(http, StatusCodes.Status503ServiceUnavailable, "temporarily_unavailable", TimeSpan.FromMinutes(1)); + return; + } + + await next(http); + } + + private const string Authorize = "authorize"; + + // OpenIddict answers these paths with or without a trailing slash, so the match is by segments, never exact equality. + private static string? EndpointOf(PathString path) => + path.StartsWithSegments(OidcServiceCollectionExtensions.AuthorizationPath, StringComparison.OrdinalIgnoreCase) ? Authorize + : path.StartsWithSegments(OidcServiceCollectionExtensions.TokenPath, StringComparison.OrdinalIgnoreCase) ? "token" + : path.StartsWithSegments(OidcServiceCollectionExtensions.UserInfoPath, StringComparison.OrdinalIgnoreCase) ? "userinfo" + : null; + + // A marker that cannot be told apart from an absent one (a permission or I/O error) throws, and the guard answers 503: the brake never releases by accident. + private static bool BrakeEngaged(string path) + { + if (string.IsNullOrWhiteSpace(path)) + { + return false; + } + + try + { + File.GetAttributes(path); + return true; + } + catch (Exception exception) when (exception is FileNotFoundException or DirectoryNotFoundException) + { + return false; + } + } + + // An IPv6 caller owns its /64, so rotating the low bits buys nothing. + private static string AddressKey(IPAddress? address) + { + if (address is null) + { + return "unknown"; + } + + if (address.IsIPv4MappedToIPv6) + { + address = address.MapToIPv4(); + } + + if (address.AddressFamily != System.Net.Sockets.AddressFamily.InterNetworkV6) + { + return address.ToString(); + } + + var bytes = address.GetAddressBytes(); + Array.Clear(bytes, 8, 8); + return new IPAddress(bytes).ToString() + "/64"; + } + + private static Task RefuseAsync(HttpContext http, int status, string error, TimeSpan retryAfter) + { + http.Response.StatusCode = status; + http.Response.Headers.RetryAfter = Math.Max(1, (int)Math.Ceiling(retryAfter.TotalSeconds)).ToString(System.Globalization.CultureInfo.InvariantCulture); + http.Response.ContentType = "application/json"; + return http.Response.WriteAsync($"{{\"error\":\"{error}\"}}", http.RequestAborted); + } +} + +/// +/// The issuer's per-address permits on the shared fixed-window budget. There is no per-client budget: the one client's id is +/// public and what a caller names unverified, so a budget on it would let anyone spend the real client's permits. +/// +public sealed class OidcRateLimit(OidcIssuerOptions options, TimeProvider clock) +{ + private readonly FixedWindowBudget addresses = new(options.Limits.PerAddressPermits, options.Limits.Window, options.Limits.MaxTrackedKeys, clock); + + public BudgetDenial? Spend(string address) => addresses.Spend(address); +} + +/// +/// Deletes expired and orphaned authorizations and tokens on a schedule: tokens past their expiry (parked authorize +/// requests, codes, access tokens) or no longer valid, then authorizations that are no longer valid or are ad hoc with no +/// token left. Live rows are never touched. Plain queries, not OpenIddict's ExecuteDelete prune, which the in-memory test provider +/// cannot run; an authorization takes its remaining tokens with it, because the foreign key has no cascade on Postgres. +/// +public sealed class OidcPruner(IServiceScopeFactory scopes, OidcIssuerOptions options, TimeProvider clock, ILogger logger) : BackgroundService +{ + private const int Batch = 500; + + // A fresh authorization briefly has no token yet; leave anything younger than this alone. + private static readonly TimeSpan AuthorizationGrace = TimeSpan.FromMinutes(5); + + public async Task PruneAsync(CancellationToken cancellationToken) + { + await using var scope = scopes.CreateAsyncScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var now = clock.GetUtcNow(); + var young = now - AuthorizationGrace; + + int removed; + do + { + var tokens = await db.Set() + .Where(token => token.ExpirationDate < now || (token.Status != Statuses.Valid && token.CreationDate < young)) + .Take(Batch).ToListAsync(cancellationToken); + db.RemoveRange(tokens); + await db.SaveChangesAsync(cancellationToken); + removed = tokens.Count; + } + while (removed == Batch); + + do + { + // A token row references its authorization (no cascade in the database), so the tokens go with it. + var authorizations = await db.Set() + .Include(authorization => authorization.Tokens) + .Where(authorization => authorization.CreationDate < young + && (authorization.Status != Statuses.Valid || (authorization.Type == AuthorizationTypes.AdHoc && !authorization.Tokens.Any()))) + .Take(Batch).ToListAsync(cancellationToken); + db.RemoveRange(authorizations.SelectMany(authorization => authorization.Tokens)); + db.RemoveRange(authorizations); + await db.SaveChangesAsync(cancellationToken); + removed = authorizations.Count; + } + while (removed == Batch); + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + using var timer = new PeriodicTimer(options.Limits.PruneInterval, clock); + while (await timer.WaitForNextTickAsync(stoppingToken)) + { + try + { + await PruneAsync(stoppingToken); + } + catch (Exception exception) when (exception is not OperationCanceledException) + { + logger.LogError("OIDC prune failed ({ExceptionType}); the next tick tries again.", exception.GetType().Name); + } + } + } +} diff --git a/src/Bifrost.Web/Features/Oidc/OidcServiceCollectionExtensions.cs b/src/Bifrost.Web/Features/Oidc/OidcServiceCollectionExtensions.cs index 5ad30b8..78526af 100644 --- a/src/Bifrost.Web/Features/Oidc/OidcServiceCollectionExtensions.cs +++ b/src/Bifrost.Web/Features/Oidc/OidcServiceCollectionExtensions.cs @@ -67,6 +67,9 @@ public static class OidcServiceCollectionExtensions server.Issuer = issuer; }); + services.AddSingleton(); + services.AddSingleton(); + services.AddHostedService(provider => provider.GetRequiredService()); services.AddHostedService(); if (!string.IsNullOrWhiteSpace(options.Clients.Mas.BackchannelLogoutUri)) { diff --git a/src/Bifrost.Web/Program.cs b/src/Bifrost.Web/Program.cs index 87d8088..2a5ff5f 100644 --- a/src/Bifrost.Web/Program.cs +++ b/src/Bifrost.Web/Program.cs @@ -285,6 +285,11 @@ if (!app.Environment.IsDevelopment()) app.UseHttpLogging(); app.UseHttpsRedirection(); +if (oidcOptions is not null) +{ + app.UseMiddleware(); // before authentication: OpenIddict answers /connect/* inside it +} + app.UseStaticFiles(); app.UseRouting(); app.UseConceptTallyCors(); diff --git a/tests/Bifrost.Web.Tests/OidcIssuerTests.cs b/tests/Bifrost.Web.Tests/OidcIssuerTests.cs index 30098f5..56f2b9a 100644 --- a/tests/Bifrost.Web.Tests/OidcIssuerTests.cs +++ b/tests/Bifrost.Web.Tests/OidcIssuerTests.cs @@ -7,6 +7,7 @@ using System.Text.Json; using Bifrost.Web.Domain; using Bifrost.Web.Features.Shared; using Bifrost.Web.Tests.Support; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; @@ -19,7 +20,7 @@ namespace Bifrost.Web.Tests; public sealed class OidcIssuerTests : IClassFixture { private static readonly Uri Origin = new("https://localhost"); - private readonly OidcWebApplicationFactory _factory; + private OidcWebApplicationFactory _factory; public OidcIssuerTests(OidcWebApplicationFactory factory) => _factory = factory; @@ -178,6 +179,250 @@ public sealed class OidcIssuerTests : IClassFixture Assert.False(redirect.ContainsKey("error")); } + private static IReadOnlyDictionary Settings(params (string Key, string Value)[] pairs) => + pairs.ToDictionary(p => $"Bifrost:Oidc:{p.Key}", p => (string?)p.Value); + + [Fact] + public async Task A_flood_from_one_address_is_refused_with_429_and_a_retry_after() + { + using var factory = new OidcSettingsFactory(Settings(("Limits:PerAddressPermits", "5"))); + using var client = factory.CreateClient(ClientOptions()); + + var statuses = new List(); + HttpResponseMessage? last = null; + for (var i = 0; i < 8; i++) + { + last?.Dispose(); + last = await client.GetAsync(NewRequest().Path); + statuses.Add(last.StatusCode); + } + + Assert.DoesNotContain(HttpStatusCode.TooManyRequests, statuses.Take(5)); + Assert.All(statuses.Skip(5), status => Assert.Equal(HttpStatusCode.TooManyRequests, status)); + Assert.True(int.Parse(last!.Headers.GetValues("Retry-After").Single()) >= 1); + using var tokenFlood = await client.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary())); + Assert.Equal(HttpStatusCode.TooManyRequests, tokenFlood.StatusCode); // one budget over all three endpoints + using var userinfoFlood = await client.GetAsync("/connect/userinfo"); + Assert.Equal(HttpStatusCode.TooManyRequests, userinfoFlood.StatusCode); + last.Dispose(); + } + + [Fact] + public async Task A_flood_naming_the_real_client_does_not_spend_anyone_elses_permits_and_a_trailing_slash_is_no_way_around() + { + using var factory = new OidcSettingsFactory(Settings(("Limits:PerAddressPermits", "4"))); + using var client = factory.CreateClient(ClientOptions()); + + var statuses = new List(); + foreach (var path in new[] { NewRequest().Path, NewRequest().Path, "/connect/authorize/?client_id=x", "/connect/token/", "/connect/userinfo/" }) + { + using var response = await client.GetAsync(path); + statuses.Add(response.StatusCode); + } + + Assert.Equal(HttpStatusCode.TooManyRequests, statuses[4]); // the slashed forms spend the same budget + Assert.DoesNotContain(HttpStatusCode.TooManyRequests, statuses.Take(4)); + } + + [Theory] + [InlineData("/connect/authorize")] + [InlineData("/connect/authorize/")] + [InlineData("/connect/token/")] + [InlineData("/connect/userinfo/")] + [InlineData("/CONNECT/Userinfo")] + public async Task The_brake_holds_for_every_spelling_of_an_endpoint(string path) + { + var marker = Path.Combine(Path.GetTempPath(), $"oidc-brake-{Guid.NewGuid():N}"); + File.WriteAllText(marker, "braked"); + try + { + using var factory = new OidcSettingsFactory(Settings(("BrakePath", marker))); + using var client = factory.CreateClient(ClientOptions()); + using var response = await client.GetAsync(path == "/connect/authorize" || path == "/connect/authorize/" ? path + "?client_id=x" : path); + Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode); + } + finally + { + File.Delete(marker); + } + } + + [Fact] + public async Task A_guard_that_cannot_decide_answers_503_and_never_passes_the_request_on() + { + var reached = false; + var options = new Bifrost.Web.Configuration.OidcIssuerOptions(); + var middleware = new Bifrost.Web.Features.Oidc.OidcGuardMiddleware( + _ => { reached = true; return Task.CompletedTask; }, options, + new Bifrost.Web.Features.Oidc.OidcRateLimit(options, TimeProvider.System), + Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance); + var http = new DefaultHttpContext(); + http.Request.Path = "/connect/authorize"; + http.Request.QueryString = new QueryString("?client_id=x"); + http.Response.Body = new MemoryStream(); + + await middleware.InvokeAsync(http, System.Reflection.DispatchProxy.Create()); + + Assert.False(reached); + Assert.Equal(StatusCodes.Status503ServiceUnavailable, http.Response.StatusCode); + } + + /// Every call fails: the row count the guard asks of the token table cannot be answered. + public class ThrowingProxy : System.Reflection.DispatchProxy + { + protected override object? Invoke(System.Reflection.MethodInfo? targetMethod, object?[]? args) => throw new InvalidOperationException("the token table is down"); + } + + [Fact] + public async Task A_resumed_authorize_request_is_not_refused_by_the_row_cap_but_a_fresh_one_is() + { + using var factory = new OidcSettingsFactory(Settings(("Limits:MaxTokenRows", "3"))); + _factory = factory; + var (client, _) = await SignedInAsync(MembershipStatus.Active, "Resumer"); + using var first = await client.GetAsync(NewRequest().Path); // parks one row, answers with a redirect carrying the request_uri + var resume = first.Headers.Location!.ToString(); + Assert.Contains("request_uri", resume); + + using var anonymous = factory.CreateClient(ClientOptions()); + for (var i = 0; i < 2; i++) + { + using var filler = await anonymous.GetAsync(NewRequest().Path); + } + + using var fresh = await anonymous.GetAsync(NewRequest().Path); + Assert.Equal(HttpStatusCode.ServiceUnavailable, fresh.StatusCode); + using var resumed = await client.GetAsync(resume); + Assert.NotEqual(HttpStatusCode.ServiceUnavailable, resumed.StatusCode); + } + + [Fact] + public async Task A_mas_flow_fits_the_default_budgets_and_the_window_reopens() + { + using var factory = new OidcSettingsFactory(Settings(("Limits:PerAddressPermits", "30"))); + _factory = factory; + var (client, identity) = await SignedInAsync(MembershipStatus.Active, "Within Budget"); + + var verified = await ValidateIdTokenAsync((await CompleteFlowAsync(client)).IdToken, null); + Assert.Equal(identity, verified.Claim("preferred_username")); + + for (var i = 0; i < 40; i++) + { + using var burst = await client.GetAsync("/connect/userinfo"); + } + + using var refused = await client.GetAsync("/connect/userinfo"); + Assert.Equal(HttpStatusCode.TooManyRequests, refused.StatusCode); + factory.Clock.Advance(TimeSpan.FromMinutes(1)); + using var reopened = await client.GetAsync("/connect/userinfo"); + Assert.NotEqual(HttpStatusCode.TooManyRequests, reopened.StatusCode); + } + + [Fact] + public async Task The_brake_marker_stops_the_issuer_and_releasing_it_needs_no_restart() + { + var marker = Path.Combine(Path.GetTempPath(), $"bifrost-oidc-brake-{Guid.NewGuid():N}"); + using var factory = new OidcSettingsFactory(Settings(("BrakePath", marker))); + _factory = factory; + var (client, _) = await SignedInAsync(MembershipStatus.Active, "Braked"); + try + { + Assert.True((await CompleteFlowAsync(client)).IdToken.Length > 0, "released: the flow passes"); + + await File.WriteAllTextAsync(marker, "held"); + foreach (var path in new[] { NewRequest().Path, "/connect/userinfo" }) + { + using var held = await client.GetAsync(path); + Assert.Equal(HttpStatusCode.ServiceUnavailable, held.StatusCode); + Assert.True(held.Headers.Contains("Retry-After")); + } + + using var token = await client.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary())); + Assert.Equal(HttpStatusCode.ServiceUnavailable, token.StatusCode); + using var discovery = await client.GetAsync("/.well-known/openid-configuration"); + Assert.Equal(HttpStatusCode.OK, discovery.StatusCode); // the brake stops issuing, not the published keys + + File.Delete(marker); + Assert.True((await CompleteFlowAsync(client)).IdToken.Length > 0, "released again, same process"); + } + finally + { + File.Delete(marker); + } + } + + [Fact] + public async Task Anonymous_authorize_requests_cannot_park_more_than_the_row_cap_and_the_prune_gives_the_room_back() + { + using var factory = new OidcSettingsFactory(Settings(("Limits:MaxTokenRows", "3"))); + _factory = factory; + using var client = factory.CreateClient(ClientOptions()); + + var parked = new List(); + for (var i = 0; i < 5; i++) + { + using var response = await client.GetAsync(NewRequest().Path); + parked.Add(response.StatusCode); + } + + Assert.Equal(3, await factory.TokenRowsAsync()); + Assert.All(parked.Skip(3), status => Assert.Equal(HttpStatusCode.ServiceUnavailable, status)); + Assert.All(parked.Take(3), status => Assert.NotEqual(HttpStatusCode.ServiceUnavailable, status)); + + factory.Clock.Advance(TimeSpan.FromMinutes(11)); // past the ten-minute parked lifetime + await factory.PruneAsync(); + Assert.Equal(0, await factory.TokenRowsAsync()); + using var again = await client.GetAsync(NewRequest().Path); + Assert.NotEqual(HttpStatusCode.ServiceUnavailable, again.StatusCode); + } + + [Fact] + public async Task The_prune_removes_expired_rows_and_leaves_a_live_token_alone() + { + using var factory = new OidcSettingsFactory(Settings()); + _factory = factory; + var (client, _) = await SignedInAsync(MembershipStatus.Active, "Pruned"); + var tokens = await CompleteFlowAsync(client); // a parked request, a code and an access token existed along the way + using (var anonymous = factory.CreateClient(ClientOptions())) + { + using var parked = await anonymous.GetAsync(NewRequest().Path); // a parked request that will expire + } + + var before = await factory.TokenRowsAsync(); + await factory.PruneAsync(); + Assert.Equal(before, await factory.TokenRowsAsync()); // nothing has expired yet: nothing goes + + factory.Clock.Advance(TimeSpan.FromMinutes(11)); // parked requests and codes are dead, the access token (an hour) is not + await factory.PruneAsync(); + var after = await factory.TokenRowsAsync(); + Assert.True(after < before, "expired rows went"); + Assert.Contains("urn:ietf:params:oauth:token-type:access_token", await factory.TokenTypesAsync()); // the live one, not merely some row + + using var userinfo = new HttpRequestMessage(HttpMethod.Get, "/connect/userinfo"); + userinfo.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken); + using var response = await factory.CreateClient(ClientOptions()).SendAsync(userinfo); + Assert.NotEqual(HttpStatusCode.Unauthorized, response.StatusCode); + } + + [Fact] + public async Task The_prune_takes_a_revoked_authorization_with_its_tokens_and_touches_no_live_authorization() + { + using var factory = new OidcSettingsFactory(Settings()); + _factory = factory; + var (revoked, _) = await SignedInAsync(MembershipStatus.Active, "Revoked"); + await CompleteFlowAsync(revoked); + var (live, _) = await SignedInAsync(MembershipStatus.Active, "Live"); + await CompleteFlowAsync(live); + Assert.Equal(2, await factory.AuthorizationRowsAsync()); + Assert.Equal(6, await factory.TokensOfAuthorizationsAsync()); // a spent code, an access token and an id token each + await factory.RevokeFirstAuthorizationAsync(); + + factory.Clock.Advance(TimeSpan.FromMinutes(6)); // past the grace for a young row; the access tokens (an hour) are still alive + await factory.PruneAsync(); + + Assert.Equal(1, await factory.AuthorizationRowsAsync()); + Assert.Equal(2, await factory.TokenRowsAsync()); // no orphan stays either: the revoked one's tokens went with it; the live one's access and id tokens stayed, spent codes go + } + [Theory] [InlineData("unknown-client", OidcTestKit.RedirectUri, true)] [InlineData(OidcTestKit.ClientId, "https://evil.example.test/callback", true)] diff --git a/tests/Bifrost.Web.Tests/Support/OidcTestKit.cs b/tests/Bifrost.Web.Tests/Support/OidcTestKit.cs index 0b14d18..351ac79 100644 --- a/tests/Bifrost.Web.Tests/Support/OidcTestKit.cs +++ b/tests/Bifrost.Web.Tests/Support/OidcTestKit.cs @@ -5,6 +5,7 @@ using Bifrost.Web.Features.Shared; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Hosting; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; @@ -92,7 +93,9 @@ public sealed class OidcTestKit : IDisposable ["Bifrost:Oidc:EncryptionKeyPath"] = EncryptionKeyPath, ["Bifrost:Oidc:Clients:Mas:ClientId"] = ClientId, ["Bifrost:Oidc:Clients:Mas:ClientSecretPath"] = ClientSecretPath, - ["Bifrost:Oidc:Clients:Mas:RedirectUri"] = RedirectUri + ["Bifrost:Oidc:Clients:Mas:RedirectUri"] = RedirectUri, + // The shared test client has no remote address, so every request is one caller: the budgets are out of the way unless a test sets them. + ["Bifrost:Oidc:Limits:PerAddressPermits"] = "100000", }; public void Dispose() @@ -114,6 +117,11 @@ public sealed class OidcTestClock : TimeProvider public override DateTimeOffset GetUtcNow() => _now; + // The monotonic reading follows the moved time, so a budget's window can be aged the same way as a parked request. + public override long TimestampFrequency => TimeSpan.TicksPerSecond; + + public override long GetTimestamp() => _now.UtcTicks; + public void Advance(TimeSpan by) => _now += by; } @@ -258,6 +266,64 @@ public sealed class OidcFixedIdentityFactory(string identity) : OidcWebApplicati } } +/// An OIDC host with extra settings (limits, a brake path) applied after the kit's own. +public sealed class OidcSettingsFactory(IReadOnlyDictionary settings) : OidcWebApplicationFactory +{ + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + base.ConfigureWebHost(builder); + foreach (var (key, value) in settings) + { + builder.UseSetting(key, value); + } + } + + public Task PruneAsync() => Services.GetRequiredService().PruneAsync(CancellationToken.None); + + public async Task> TokenTypesAsync() + { + using var scope = Services.CreateScope(); + var manager = scope.ServiceProvider.GetRequiredService(); + var types = new List(); + await foreach (var token in manager.ListAsync()) + { + types.Add(await manager.GetTypeAsync(token)); + } + + return types; + } + + public async Task AuthorizationRowsAsync() + { + using var scope = Services.CreateScope(); + return (int)await scope.ServiceProvider.GetRequiredService().CountAsync(); + } + + /// Tokens that belong to some authorization (the request and code rows carry none once spent). + public async Task TokensOfAuthorizationsAsync() + { + using var scope = Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + return await db.Set().CountAsync(token => token.Authorization != null); + } + + public async Task RevokeFirstAuthorizationAsync() + { + using var scope = Services.CreateScope(); + var manager = scope.ServiceProvider.GetRequiredService(); + await foreach (var authorization in manager.ListAsync(count: 1, offset: 0)) + { + Assert.True(await manager.TryRevokeAsync(authorization)); + } + } + + public async Task TokenRowsAsync() + { + using var scope = Services.CreateScope(); + return (int)await scope.ServiceProvider.GetRequiredService().CountAsync(); + } +} + /// An OIDC host whose public base URL differs from the origin its requests arrive on, so the issuer cannot be request-derived. public sealed class OidcForeignIssuerFactory : OidcWebApplicationFactory { From 24f3deb88edbf3c8fc09b51bf3f0025b5e273ace Mon Sep 17 00:00:00 2001 From: Heimdall Date: Wed, 11 Nov 2026 08:13:09 +0000 Subject: [PATCH 15/26] OIDC issuer: chat tier for accounts without Active membership (#52) --- docs/oidc-issuer.md | 28 +++---- .../Features/Oidc/OidcEndpoints.cs | 48 ++++++++---- .../Features/Oidc/OidcLogoutReconciler.cs | 6 +- tests/Bifrost.Web.Tests/OidcIssuerTests.cs | 76 +++++++++++++++---- tests/Bifrost.Web.Tests/OidcLogoutTests.cs | 21 ++++- .../Bifrost.Web.Tests/Support/OidcTestKit.cs | 26 ++++++- 6 files changed, 155 insertions(+), 50 deletions(-) diff --git a/docs/oidc-issuer.md b/docs/oidc-issuer.md index cb2ee08..06dff5f 100644 --- a/docs/oidc-issuer.md +++ b/docs/oidc-issuer.md @@ -68,25 +68,29 @@ The authorize endpoint reads the cookie session and the same actor facts the `/A - No session: challenge to `/auth/sign-in`, which resumes the original request after sign-in. OpenIddict parks the full request (its request token, in the OpenIddict token table, expiring after ten minutes, spent when used) and the return path carries only a short `request_uri`, so `LocalReturnPath.MaxLength` stays 256. -- Signed in, membership not `Active`: redirect to the client's `redirect_uri` with `error=access_denied` and - `error_description=The account is not an active GameCult member.` -- Signed in, `Active`, but `NormalizedBifrostIdentity` empty: `error=access_denied` with +- Signed in, membership `Suspended`: redirect to the client's `redirect_uri` with `error=access_denied` and + `error_description=The account is suspended.` +- Signed in with no membership, `Authenticated` or `PendingApproval` (the chat tier): treated like an active member for + sign-in to chat and nothing else. The token carries the same `openid profile` claims; the issuer writes no membership, + role or vote, and every member or governance surface (`/App`, `/eve/governance`, `/api/sealed-motions`, the fork desk) + still asks for `Active` itself, so neither the session nor the token opens them. +- Signed in, with an account that can chat, but `NormalizedBifrostIdentity` empty: `error=access_denied` with `error_description=The account has no Bifrost identity.` -- Signed in, `Active`, but the identity is a reserved localpart (`BifrostIdentityService.IsReserved`: the 13 Synapse +- Signed in, with an account that can chat, but the identity is a reserved localpart (`BifrostIdentityService.IsReserved`: the 13 Synapse accounts and the system names, the same 28 as MAS `banned_usernames`): `error=access_denied` with `error_description=The account's Bifrost identity is reserved.` Creation refuses these names, but a row minted before that refusal may hold one and there is no rename path, so the issuer never vouches for a Mind's or a system's localpart. The refusal is at authorize only: a code or access token issued for such a row before this check deployed stays redeemable until it expires (minutes), and token and userinfo re-read membership, not the name. Whether any such row exists is not known from the code; one query of `UserAccounts` against the 28 names would say. -- Signed in and `Active`: a code is issued. -- Token endpoint: membership is read again; an account no longer `Active` gets `invalid_grant` and no tokens. +- Otherwise: a code is issued. +- Token endpoint and userinfo: suspension is read again; an account suspended or deleted since gets `invalid_grant` (token) or 403 (userinfo). The logout reconciler ends the sessions of those same accounts; a chat-tier account is never logged out for lacking membership. ## Back-channel logout When `BackchannelLogoutUri` is set, a hosted reconciler (`OidcLogoutReconciler`) runs every 30 seconds. It lists the -subjects holding valid authorizations of the MAS client; for each subject whose membership is not `Active` (the same -reading the token and userinfo endpoints use; a deleted account or a subject that is not an account id counts as not -`Active`) it POSTs one OIDC back-channel logout token to MAS, and only after a 2xx revokes that subject's authorizations, -which also invalidates their tokens. Membership is never written by this path: any way of leaving `Active` (admin -action, sync, a database edit, a future suspend) is covered because the work is derived from membership state. +subjects holding valid authorizations of the MAS client; for each subject who may no longer chat (the same +reading the token and userinfo endpoints use: membership `Suspended`, a deleted account, or a subject that is not an account id; an +account with no membership or a pending one may chat and is left alone) it POSTs one OIDC back-channel logout token to MAS, and only after a 2xx revokes that subject's authorizations, +which also invalidates their tokens. Membership is never written by this path: any way of being suspended or deleted (admin +action, sync, a database edit) is covered because the work is derived from membership state. The token is an ES256 JWT signed with the issuer key, header `typ` `logout+jwt`, with `iss` as the id_token carries it, `aud` the MAS client id, `sub` the account id, `iat`, `exp` two minutes later, a fresh `jti` and the @@ -142,8 +146,6 @@ Checked against the Window B list (2026-11-11). Present and tested: discovery, J the one client, claims to a Matrix-legal localpart (`[a-z0-9._-]`, 3-80, lower case), reserved names at creation and now at issuance, back-channel logout. Not present: -- **Non-member sign-in.** The gate is `Active` membership, so an account with no membership gets `access_denied`. A "Users" - tier for chat registration, with no vote, is a policy choice not yet made; nothing here writes membership or votes. - **MAS upstream block.** `gamecult-ops` `chat/matrix-auth/config.yaml` has no `upstream_oauth2` (provider ULID, `on_conflict: fail`, `on_backchannel_logout: logout_all`, the redirect URI above); that lives in the ops repo. diff --git a/src/Bifrost.Web/Features/Oidc/OidcEndpoints.cs b/src/Bifrost.Web/Features/Oidc/OidcEndpoints.cs index 7fce895..1a90c23 100644 --- a/src/Bifrost.Web/Features/Oidc/OidcEndpoints.cs +++ b/src/Bifrost.Web/Features/Oidc/OidcEndpoints.cs @@ -20,7 +20,7 @@ namespace Bifrost.Web.Features.Oidc; /// public static class OidcEndpoints { - public const string NotActiveDescription = "The account is not an active GameCult member."; + public const string SuspendedDescription = "The account is suspended."; public const string NoIdentityDescription = "The account has no Bifrost identity."; public const string ReservedIdentityDescription = "The account's Bifrost identity is reserved."; @@ -49,9 +49,16 @@ public static class OidcEndpoints } var actor = await actors.GetAsync(http.RequestAborted); - if (actor.UserAccount is null || !actor.IsActiveMember) + // Chat tier: any account that is not suspended may sign in to chat. Membership is neither required nor written; + // every member or governance surface keeps asking for Active itself (the Active-member policy, IsActiveMember, ActiveMemberIdAsync). + if (actor.UserAccount is null) { - return Deny(Errors.AccessDenied, NotActiveDescription); + return Deny(Errors.AccessDenied, NoIdentityDescription); + } + + if (actor.Membership?.Status == MembershipStatus.Suspended) + { + return Deny(Errors.AccessDenied, SuspendedDescription); } var account = actor.UserAccount; @@ -90,13 +97,13 @@ public static class OidcEndpoints private static async Task ExchangeAsync(HttpContext http, BifrostDbContext db) { // Only the authorization code grant is enabled, so OpenIddict has refused every other grant already. - // The code's own principal carries the claims chosen at authorize time; membership is read again here, - // because the code outlives the authorize decision (a member suspended since gets no tokens). + // The code's own principal carries the claims chosen at authorize time; suspension is read again here, + // because the code outlives the authorize decision (an account suspended since gets no tokens). var result = await http.AuthenticateAsync(OpenIddictScheme); var subject = result.Principal?.GetClaim(Claims.Subject); - if (!await IsStillActiveMemberAsync(db, subject, http.RequestAborted)) + if (!await IsStillChatEligibleAsync(db, subject, http.RequestAborted)) { - return Deny(Errors.InvalidGrant, NotActiveDescription); + return Deny(Errors.InvalidGrant, SuspendedDescription); } return Results.SignIn(result.Principal!, properties: null, OpenIddictScheme); @@ -110,10 +117,10 @@ public static class OidcEndpoints return Results.Challenge(authenticationSchemes: [OpenIddictScheme]); } - // An access token outlives the authorize decision too: a member suspended since is no longer answered. - if (!await IsStillActiveMemberAsync(db, result.Principal.GetClaim(Claims.Subject), http.RequestAborted)) + // An access token outlives the authorize decision too: an account suspended since is no longer answered. + if (!await IsStillChatEligibleAsync(db, result.Principal.GetClaim(Claims.Subject), http.RequestAborted)) { - return Deny(Errors.InsufficientAccess, NotActiveDescription); + return Deny(Errors.InsufficientAccess, SuspendedDescription); } var claims = new Dictionary { [Claims.Subject] = result.Principal.GetClaim(Claims.Subject) }; @@ -126,11 +133,24 @@ public static class OidcEndpoints return Results.Json(claims); } - /// The one reading of "this subject is still an Active member", shared by the token and userinfo endpoints, the logout reconciler and the fork desk. - internal static async Task IsStillActiveMemberAsync(BifrostDbContext db, string? subject, CancellationToken cancellationToken) => - await ActiveMemberIdAsync(db, subject, cancellationToken) is not null; + /// + /// The one reading of "this subject may still chat", shared by the token and userinfo endpoints and the logout reconciler: + /// the account exists and its membership is not Suspended. Membership may be absent, pending or Active. Re-read on every call. + /// + internal static async Task IsStillChatEligibleAsync(BifrostDbContext db, string? subject, CancellationToken cancellationToken) + { + if (!Guid.TryParse(subject, out var accountId)) + { + return false; + } - /// The account id of the subject when it is an Active member now, else null. Re-read on every call. + var account = await db.UserAccounts.AsNoTracking() + .Include(x => x.Membership) + .SingleOrDefaultAsync(x => x.Id == accountId, cancellationToken); + return account is not null && account.Membership?.Status != MembershipStatus.Suspended; + } + + /// The account id of the subject when it is an Active member now, else null. The door to the fork desk (cookie surfaces check IsActiveMember); a chat-tier token does not pass it. Re-read on every call. internal static async Task ActiveMemberIdAsync(BifrostDbContext db, string? subject, CancellationToken cancellationToken) { if (!Guid.TryParse(subject, out var accountId)) diff --git a/src/Bifrost.Web/Features/Oidc/OidcLogoutReconciler.cs b/src/Bifrost.Web/Features/Oidc/OidcLogoutReconciler.cs index ac6fa72..63df719 100644 --- a/src/Bifrost.Web/Features/Oidc/OidcLogoutReconciler.cs +++ b/src/Bifrost.Web/Features/Oidc/OidcLogoutReconciler.cs @@ -11,8 +11,8 @@ using static OpenIddict.Abstractions.OpenIddictConstants; namespace Bifrost.Web.Features.Oidc; /// -/// Ends the chat sessions of a subject who is no longer an Active member. Each tick it lists the subjects holding valid -/// authorizations of the configured client; for each one the shared active-member reading refuses, it POSTs one signed +/// Ends the chat sessions of a subject who is no longer allowed to chat (suspended, or the account gone). Each tick it lists the subjects holding valid +/// authorizations of the configured client; for each one the shared chat-eligibility reading refuses, it POSTs one signed /// OIDC back-channel logout token to MAS and, only after a 2xx, revokes that subject's authorizations. The /// authorizations table is the queue: nothing is stored, so a restart, a MAS outage or a later-set URI resumes from it. /// Per-subject backoff lives in memory only. One subject's unexpected exception is logged and skipped, never aborting the @@ -127,7 +127,7 @@ public sealed class OidcLogoutReconciler( { await using var scope = scopeFactory.CreateAsyncScope(); var db = scope.ServiceProvider.GetRequiredService(); - if (await OidcEndpoints.IsStillActiveMemberAsync(db, subject, cancellationToken)) + if (await OidcEndpoints.IsStillChatEligibleAsync(db, subject, cancellationToken)) { _backoff.Remove(subject); return; diff --git a/tests/Bifrost.Web.Tests/OidcIssuerTests.cs b/tests/Bifrost.Web.Tests/OidcIssuerTests.cs index 56f2b9a..68617b8 100644 --- a/tests/Bifrost.Web.Tests/OidcIssuerTests.cs +++ b/tests/Bifrost.Web.Tests/OidcIssuerTests.cs @@ -107,20 +107,74 @@ public sealed class OidcIssuerTests : IClassFixture Assert.Equal("Second Name", second.Claim("name")); } - [Theory] - [InlineData(MembershipStatus.Authenticated)] - [InlineData(MembershipStatus.PendingApproval)] - [InlineData(MembershipStatus.Suspended)] - public async Task A_member_who_is_not_active_is_refused_with_access_denied_and_no_code(MembershipStatus status) + [Fact] + public async Task A_suspended_account_is_refused_with_access_denied_and_no_code() { - var (client, _) = await SignedInAsync(status, "Not Active"); + var (client, _) = await SignedInAsync(MembershipStatus.Suspended, "Suspended One"); var request = NewRequest(); var final = await DriveAsync(client, request); var redirect = AssertRedirectToClient(final, request.State); Assert.Equal("access_denied", redirect["error"]); - Assert.Equal(Bifrost.Web.Features.Oidc.OidcEndpoints.NotActiveDescription, redirect["error_description"]); + Assert.Equal(Bifrost.Web.Features.Oidc.OidcEndpoints.SuspendedDescription, redirect["error_description"]); + Assert.False(redirect.ContainsKey("code")); + } + + [Theory] + [InlineData(MembershipStatus.Authenticated)] + [InlineData(MembershipStatus.PendingApproval)] + public async Task A_non_member_gets_a_chat_only_token_and_writes_no_membership_or_vote(MembershipStatus status) + { + var (client, identity) = await SignedInAsync(status, "Chat Only"); + var accountId = _factory.AccountId(identity); + var before = await _factory.RowCountsAsync(); + + var tokens = await CompleteFlowAsync(client); + + var verified = await ValidateIdTokenAsync(tokens.IdToken, tokens.Request.Nonce); + Assert.Equal(accountId.ToString("D"), verified.Claim("sub")); + Assert.Equal(identity, verified.Claim("preferred_username")); + + // The issuer wrote nothing about membership or votes, and the status is exactly what it was. + Assert.Equal(before, await _factory.RowCountsAsync()); + Assert.Equal(status, await _factory.MembershipStatusAsync(identity)); + Assert.Equal(0, (await _factory.RowCountsAsync()).Votes); + + // Neither the session that signed in to chat nor its token opens a member or governance surface. + foreach (var path in new[] { "/eve/governance/surface", "/api/sealed-motions", "/App" }) + { + using var viaSession = await client.GetAsync(path); + Assert.NotEqual(HttpStatusCode.OK, viaSession.StatusCode); + + using var viaToken = new HttpRequestMessage(HttpMethod.Get, path); + viaToken.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken); + using var bearer = await NewClient().SendAsync(viaToken); + Assert.NotEqual(HttpStatusCode.OK, bearer.StatusCode); + } + + using var desk = new HttpRequestMessage(HttpMethod.Get, "/api/desk/forks"); + desk.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken); + using var deskResponse = await NewClient().SendAsync(desk); + Assert.NotEqual(HttpStatusCode.OK, deskResponse.StatusCode); + + // Last, because userinfo answers 500 on a .NET 10 runtime (the six known failures), and the rest must still run there. + using var userinfo = await UserInfoAsync(tokens.AccessToken); + Assert.Equal(HttpStatusCode.OK, userinfo.StatusCode); + var info = JsonDocument.Parse(await userinfo.Content.ReadAsStringAsync()).RootElement; + Assert.Equal(["name", "preferred_username", "sub"], info.EnumerateObject().Select(x => x.Name).Order().ToArray()); + } + + [Fact] + public async Task A_chat_tier_account_with_a_reserved_identity_is_still_refused() + { + using var factory = new OidcFixedIdentityFactory("eureka", MembershipStatus.Authenticated); + using var client = factory.CreateClient(ClientOptions()); + await RegisterAsync(client, $"cookie-{Guid.NewGuid():N}"); + var request = NewRequest(); + + var redirect = AssertRedirectToClient(await DriveAsync(client, request), request.State); + Assert.Equal("access_denied", redirect["error"]); Assert.False(redirect.ContainsKey("code")); } @@ -619,11 +673,9 @@ public sealed class OidcIssuerTests : IClassFixture } [Theory] - [InlineData(MembershipStatus.Authenticated)] - [InlineData(MembershipStatus.PendingApproval)] [InlineData(MembershipStatus.Suspended)] [InlineData(null)] - public async Task A_member_no_longer_active_after_authorize_gets_no_tokens(MembershipStatus? status) + public async Task An_account_suspended_or_gone_after_authorize_gets_no_tokens(MembershipStatus? status) { var (client, identity) = await SignedInAsync(MembershipStatus.Active, "Soon Gone"); var request = NewRequest(); @@ -640,11 +692,9 @@ public sealed class OidcIssuerTests : IClassFixture } [Theory] - [InlineData(MembershipStatus.Authenticated)] - [InlineData(MembershipStatus.PendingApproval)] [InlineData(MembershipStatus.Suspended)] [InlineData(null)] - public async Task Userinfo_refuses_an_access_token_whose_member_is_no_longer_active(MembershipStatus? status) + public async Task Userinfo_refuses_an_access_token_whose_account_is_suspended_or_gone(MembershipStatus? status) { var (client, identity) = await SignedInAsync(MembershipStatus.Active, "Soon Gone"); var tokens = await CompleteFlowAsync(client); diff --git a/tests/Bifrost.Web.Tests/OidcLogoutTests.cs b/tests/Bifrost.Web.Tests/OidcLogoutTests.cs index 8aa5d15..c167fc2 100644 --- a/tests/Bifrost.Web.Tests/OidcLogoutTests.cs +++ b/tests/Bifrost.Web.Tests/OidcLogoutTests.cs @@ -37,9 +37,7 @@ public sealed class OidcLogoutTests [Theory] [InlineData(MembershipStatus.Suspended)] - [InlineData(MembershipStatus.PendingApproval)] - [InlineData(MembershipStatus.Authenticated)] - public async Task A_member_who_is_not_active_gets_one_signed_logout_and_loses_the_authorizations(MembershipStatus status) + public async Task A_suspended_account_gets_one_signed_logout_and_loses_the_authorizations(MembershipStatus status) { using var factory = new OidcLogoutFactory(); var (identity, idToken) = await factory.GrantAsync(status); @@ -112,6 +110,23 @@ public sealed class OidcLogoutTests Assert.Equal(before, factory.ValidAuthorizations(activeSubject)); } + [Theory] + [InlineData(MembershipStatus.Authenticated)] + [InlineData(MembershipStatus.PendingApproval)] + public async Task A_chat_tier_non_member_is_never_sent_a_logout_and_keeps_the_authorizations(MembershipStatus status) + { + using var factory = new OidcLogoutFactory(); + var (chat, _) = await factory.GrantAsync(status); + var subject = factory.AccountId(chat).ToString("D"); + var before = factory.ValidAuthorizations(subject); + Assert.True(before > 0); + + await factory.Reconciler.ReconcileOnceAsync(CancellationToken.None); + + Assert.Empty(factory.Mas.LogoutTokens()); + Assert.Equal(before, factory.ValidAuthorizations(subject)); + } + [Fact] public async Task One_subject_that_cannot_be_delivered_does_not_block_another() { diff --git a/tests/Bifrost.Web.Tests/Support/OidcTestKit.cs b/tests/Bifrost.Web.Tests/Support/OidcTestKit.cs index 351ac79..a9b304f 100644 --- a/tests/Bifrost.Web.Tests/Support/OidcTestKit.cs +++ b/tests/Bifrost.Web.Tests/Support/OidcTestKit.cs @@ -179,6 +179,24 @@ public class OidcWebApplicationFactory : TestWebApplicationFactory } } + public async Task MembershipStatusAsync(string identity) + { + using var scope = Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var account = db.UserAccounts.Single(x => x.NormalizedBifrostIdentity == identity); + return (await db.Memberships.AsNoTracking().SingleOrDefaultAsync(x => x.UserAccountId == account.Id))?.Status; + } + + public sealed record RowCounts(int Accounts, int Memberships, int Votes, int RoleAssignments); + + /// What the issuer must leave alone: accounts, memberships, role grants and votes. + public async Task RowCountsAsync() + { + using var scope = Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + return new RowCounts(await db.UserAccounts.CountAsync(), await db.Memberships.CountAsync(), await db.Votes.CountAsync(), await db.RoleAssignments.CountAsync()); + } + /// Sets the membership of the account registered under . public async Task SetMembershipAsync(string identity, MembershipStatus status) { @@ -244,15 +262,15 @@ public sealed class OidcNoIdentityFactory : OidcWebApplicationFactory /// reserved names were refused at creation (or any identity the test needs) reaches the authorize endpoint, since /// registration itself now refuses a reserved name. /// -public sealed class OidcFixedIdentityFactory(string identity) : OidcWebApplicationFactory +public sealed class OidcFixedIdentityFactory(string identity, MembershipStatus status = MembershipStatus.Active) : OidcWebApplicationFactory { protected override void ConfigureActors(IServiceCollection services) { services.RemoveAll(); - services.AddScoped(_ => new FixedIdentity(identity)); + services.AddScoped(_ => new FixedIdentity(identity, status)); } - private sealed class FixedIdentity(string identity) : ICurrentBifrostActorAccessor + private sealed class FixedIdentity(string identity, MembershipStatus status) : ICurrentBifrostActorAccessor { public Task GetAsync(CancellationToken cancellationToken) => Task.FromResult(new CurrentBifrostActor(true, new UserAccount @@ -261,7 +279,7 @@ public sealed class OidcFixedIdentityFactory(string identity) : OidcWebApplicati BifrostIdentity = identity, NormalizedBifrostIdentity = identity, // as given: the guard, not the fixture, must fold case DisplayName = "Fixed Identity", - Membership = new Bifrost.Web.Domain.Membership { Status = MembershipStatus.Active } + Membership = new Bifrost.Web.Domain.Membership { Status = status } })); } } From e8722d0abf3c24aa9a1787e3cc88b77f3aaa3aad Mon Sep 17 00:00:00 2001 From: Heimdall Date: Wed, 11 Nov 2026 08:15:38 +0000 Subject: [PATCH 16/26] Mouth: room tail on chat_pending.v3 (#51) --- docs/bridge.md | 19 ++- ...mecult.persona.chat_pending.v3.tag.msgpack | Bin 197 -> 198 bytes ...ecult.persona.chat_pending.v3.tail.msgpack | Bin 0 -> 163 bytes ...na.chat_pending.v3.unproven-notice.msgpack | Bin 153 -> 154 bytes tests/persona-matrix-mouth-runtime.test.mjs | 26 +++- tests/persona-matrix-mouth.test.mjs | 117 ++++++++++++++++++ tools/persona-chat-documents.mjs | 23 +++- tools/persona-matrix-mouth.mjs | 68 ++++++++-- 8 files changed, 236 insertions(+), 17 deletions(-) create mode 100644 tests/fixtures/persona-chat-v5/gamecult.persona.chat_pending.v3.tail.msgpack diff --git a/docs/bridge.md b/docs/bridge.md index 4468988..b9b35a6 100644 --- a/docs/bridge.md +++ b/docs/bridge.md @@ -216,10 +216,25 @@ Proposed runtime half (follow-up `persona-wakes-on-address-only`): 1. Poll with `chat_pending_request.v2` and parse `chat_pending.v3`. 2. A delivery is *addressed* to a Persona when `form == "message"` and either `basis` is `direct`, `tag` or `reply`, or `address::addresses` matches her names or her projects' names on the text. The off-beat wake keeps the sender gate of `heartbeat.rs` `wakes` (the operator or another Mind of the roster): a stranger's tag, `@room` or DM is delivered but wakes no one off the beat. `basis == "room"` is the runtime's policy to weigh; the mouth only reports it. 3. A real-time wake, a live session's tell and the stagger apply to addressed deliveries only. A `notice` or an `edit` never wakes, whatever its basis. -4. A delivery that is not addressed is acked in sequence with the rest, so the position moves (the mouth stops reading at 500 unacked deliveries); it is not lost: Matrix is the record, and the next addressed delivery's `context` (up to 20 lines) and a beat's own room read show it. Reading the tail on a scheduled beat is a runtime read of Matrix, a follow-up of the ruling `wake-reads-room-tail`; the mouth adds no endpoint for it. +4. A delivery that is not addressed is acked in sequence with the rest, so the position moves (the mouth stops reading at 500 unacked deliveries); it is not lost: Matrix is the record, and the next addressed delivery's `context` (up to 20 lines) and a beat's own room read show it. The room tail below gives a scheduled beat those lines without a Matrix read of its own; the mouth adds no endpoint for it. 5. `basis` and `form` are evidence, not authority: a sender's claim is weighed by the runtime's own rule (`address.rs` for names, the sender gate for who may wake her); the mouth reports what the event says and what only Matrix could look up (the author of a reply target). -Golden bytes: `tests/fixtures/persona-chat-v5/`; Huginn pins the same bytes. +### Room tail (`chat_pending.v3`, field 2) + +The v3 answer is `[persona, deliveries, tail]`. `tail` is a list of recent room lines, oldest first, for a reader that was not woken for them (a scheduled beat). It uses the same room read as a delivery's `context` (the same function, a separate read), as her account, with no new endpoint; the cost is one `/messages` call per speakable room per poll (15 seconds cached), including every DM. A `chat_pending_request.v1` is still answered with the two-field `chat_pending.v2`. `chat_pending.v3` has not been deployed, but a reader that decodes it as a two-field tuple fails on the third element (rmp-serde `LengthMismatch`), so Huginn must accept `tail` (a defaulted field) before this mouth is deployed. + +| Bound | Rule | +|---|---| +| Rooms | Only rooms the Persona may speak in: a configured room she has joined, or one of her DMs. The same rule `send` uses (`speakableRoomId`). A room she is in but cannot speak in, and any other room, is absent. | +| Lines | A text line outside threads, not an edit, newest `20` per room, newest `60` over all rooms. Trimmed oldest first if the reply byte budget (256 KiB, deliveries first) would be exceeded. | +| Cursor | None. The tail is each room's newest lines whether or not they were delivered or acked: a message the wake rule left unaddressed is acked and leaves `pending`, and the tail is where a beat still finds it. A reader removes the lines a delivery already shows (by event id). Her own lines are included. | +| Read | A room's read is kept 15 seconds, dropped when a sync brings the room news. A room that cannot be read within 10 seconds is logged and absent; the other rooms and the deliveries are unaffected. | + +A line is the 7-field tuple `[room, event_id, at_unix_ms, sender, form, basis, body]`: `room` is the configured alias (the room id for a DM), `form` and `basis` as for a delivery, `body` clipped to 1000 bytes with the same typed text line for media a delivery has (nothing is fetched). `basis` is what Matrix says on the event itself (`direct` in a DM, `tag`, `room`) and nothing the mouth would have to look up, so a reply is nil here: **nil is unproven, not "not for her"**, and a line in a DM is `direct`. Huginn's rule that an `is_direct` delivery wakes with a nil `basis` is unchanged and lives with Huginn; the tail does not feed it. + +The tail is **data, not instruction**. A line is what a sender typed in a room, and the mouth wakes no one, acks nothing and sends nothing because of it. A reader treats the body as untrusted text and weighs `basis` as evidence, as for a delivery. The tail does not replace a delivery: a message still reaches her as a delivery, with its own `seq`, and only `chat_ack` advances a position. + +Golden bytes: `tests/fixtures/persona-chat-v5/` (`chat_pending.v3.tail` carries two lines); Huginn pins the same bytes. Bifrost admits an observation only when guild, channel, target runtime, canonical repository, Persona, producer, and producer runtime match a preconfigured `bifrost.persona_feedback.target_binding.v0` document in Bifrost's private provider store. That binding also owns source visibility, data classification, and the delivery-store target. Bifrost never infers public visibility from Discord identifiers. It classifies the author using its own `bifrost.discord.actor_link.v0` state: a match yields `linked_governance_feedback`; absence yields `unlinked_social_feedback`. Neither class grants adoption, work, release, or deployment authority. diff --git a/tests/fixtures/persona-chat-v5/gamecult.persona.chat_pending.v3.tag.msgpack b/tests/fixtures/persona-chat-v5/gamecult.persona.chat_pending.v3.tag.msgpack index 006f6cf6f6bbd6d289589bb4ebd1ec33d3ef4b36..6685251d258d7deb362a087216fa725da643e82c 100644 GIT binary patch delta 11 ScmX@gc#M&8@ { test("two clients served at once each get their own answer on their own session", async () => { const mouth = { pendingAfter: () => [], + tail: async () => [], ack: async () => {}, // Slow enough that the two requests are in flight together. send: async request => { await new Promise(r => setTimeout(r, 150)); return { requestId: request.requestId, eventId: `$${request.requestId}`, refusal: null }; }, @@ -138,6 +140,7 @@ test("the RUDP endpoint serves pending, ack and send over CultNet, and answers a const calls = { acks: [], sends: [], reads: [] }; const mouth = { pendingAfter: after => (after < 7 ? [delivery] : []), + tail: async () => { if (calls.tailFails) throw new Error("room read failed"); return [tailLine]; }, ack: async through => { calls.acks.push(through); }, read: async request => { calls.reads.push(request); return null; }, send: async request => { calls.sends.push(request); return request.text === "muted" ? { requestId: request.requestId, eventId: null, refusal: "Brake" } : { requestId: request.requestId, eventId: "$sent", refusal: null }; }, @@ -177,7 +180,18 @@ test("the RUDP endpoint serves pending, ack and send over CultNet, and answers a put(docs.kinds.pendingRequestV3, "eureka", docs.pendingRequestTuple({ persona: "eureka", afterSeq: 0 })); document = await reply; assert.equal(document.schemaId, docs.kinds.pendingV3, "the kind asked by names the answer"); - assert.deepEqual(docs.parsePendingV3(msgpack.decode(document.payload)).deliveries[0], { ...delivery, basis: null, form: "message" }); + const v3 = docs.parsePendingV3(msgpack.decode(document.payload)); + assert.deepEqual(v3.deliveries[0], { ...delivery, basis: null, form: "message" }); + assert.deepEqual(v3.tail, [tailLine], "the v3 answer carries the room tail"); + + calls.tailFails = true; + reply = nextReply(); + put(docs.kinds.pendingRequestV3, "eureka", docs.pendingRequestTuple({ persona: "eureka", afterSeq: 0 })); + document = await reply; + const degraded = docs.parsePendingV3(msgpack.decode(document.payload)); + assert.equal(degraded.deliveries.length, 1, "a tail that throws does not silence the deliveries"); + assert.deepEqual(degraded.tail, []); + calls.tailFails = false; put(docs.kinds.ack, "eureka:7", docs.ackTuple({ persona: "eureka", throughSeq: 7 })); reply = nextReply(); @@ -322,6 +336,10 @@ const v5Dir = resolve(root, "tests", "fixtures", "persona-chat-v5"); const v5Samples = { "gamecult.persona.chat_pending_request.v2": { tuple: docs.pendingRequestTuple({ persona: "eureka", afterSeq: 3 }), parse: docs.parsePendingRequest }, "gamecult.persona.chat_pending.v3.tag": { tuple: docs.pendingV3Tuple({ persona: "eureka", deliveries: [{ ...delivery, isDirect: false, basis: "tag", form: "message" }] }), parse: docs.parsePendingV3 }, + "gamecult.persona.chat_pending.v3.tail": { tuple: docs.pendingV3Tuple({ persona: "eureka", deliveries: [], tail: [ + { roomAlias: "#operator:gamecult.org", eventId: "$e0", atUnixMs: 1_700_000_000_000, sender: "@meta:gamecult.org", form: "message", basis: null, text: "earlier" }, + { roomAlias: "#operator:gamecult.org", eventId: "$e1", atUnixMs: 1_700_000_001_000, sender: "@meta:gamecult.org", form: "notice", basis: "tag", text: "hello eureka" }, + ] }), parse: docs.parsePendingV3 }, "gamecult.persona.chat_pending.v3.unproven-notice": { tuple: docs.pendingV3Tuple({ persona: "eureka", deliveries: [{ ...delivery, isDirect: false, mentions: [], basis: null, form: "notice" }] }), parse: docs.parsePendingV3 }, }; for (const [name, { tuple, parse }] of Object.entries(v5Samples)) { @@ -331,7 +349,9 @@ for (const [name, { tuple, parse }] of Object.entries(v5Samples)) { if (process.env.UPDATE_FIXTURES === "1") { await mkdir(v5Dir, { recursive: true }); await writeFile(file, bytes); } assert.deepEqual(bytes, await readFile(file), "wire bytes changed: Huginn decodes these fixtures"); assert.deepEqual(parse(msgpack.decode(bytes)), parse(tuple)); - if (name.includes("pending.v3")) assert.equal(msgpack.decode(bytes)[1][0].length, 14); + if (name.includes("pending.v3")) assert.equal(msgpack.decode(bytes).length, 3, "persona, deliveries, tail"); + if (name.endsWith("v3.tag") || name.endsWith("unproven-notice")) assert.equal(msgpack.decode(bytes)[1][0].length, 14); + if (name.endsWith("v3.tail")) assert.equal(msgpack.decode(bytes)[2][0].length, 7); }); } @@ -370,7 +390,7 @@ test("RoomMoved is v2 only, and the v2 documents are strict about arity and newe test("the RUDP endpoint answers a v1 send with the three-field receipt and a v2 send with the four-field one", async () => { const seen = []; - const mouth = { pendingAfter: () => [], ack: async () => {}, send: async request => { + const mouth = { pendingAfter: () => [], tail: async () => [], ack: async () => {}, send: async request => { seen.push(request); return request.basis ? { requestId: request.requestId, eventId: null, refusal: "RoomMoved", newer: ["$a"] } : { requestId: request.requestId, eventId: "$sent", refusal: null, newer: [] }; } }; diff --git a/tests/persona-matrix-mouth.test.mjs b/tests/persona-matrix-mouth.test.mjs index 9458370..e4c7731 100644 --- a/tests/persona-matrix-mouth.test.mjs +++ b/tests/persona-matrix-mouth.test.mjs @@ -1475,3 +1475,120 @@ test("addressing facts: a target that failed is not refetched within a minute, a assert.equal(fetched(), 2, "past the minute: read again"); } finally { await hs.close(); } }); + +// The room tail (docs/bridge.md, "Room tail"): her rooms' recent talk past her ack cursor, bounded, as data on the v3 answer. +const OPERATOR = "!operator:gamecult.test"; +const lines = (room, n, from = 0, sender = META) => Array.from({ length: n }, (_, i) => message(`$${room.slice(1, 4)}${from + i}`, sender, `line ${from + i}`, 10 + from + i)); +async function tailed({ preload, direct, member, events = [], act = async () => {} } = {}) { + const hs = await fakeHomeserver({ preload, direct, member, sync: since => since === null ? { next_batch: "b0" } : { next_batch: "b1", ...joined(events.length ? { [ROOM]: { timeline: { events } } } : {}) } }); + try { + const mouth = mouthFor(hs); + await mouth.syncOnce(); + await act(mouth); + return { tail: await mouth.tail(), mouth, hs }; + } finally { await hs.close(); } +} + +test("room tail: 600 lines in a room keep the newest 20, oldest first", async () => { + const { tail } = await tailed({ preload: { [ROOM]: lines(ROOM, 600) } }); + assert.equal(tail.length, 20); + assert.deepEqual(tail.map(l => l.text), Array.from({ length: 20 }, (_, i) => `line ${580 + i}`)); + assert.deepEqual(Object.keys(tail[0]).sort(), ["atUnixMs", "basis", "eventId", "form", "roomAlias", "sender", "text"]); + assert.equal(tail[0].roomAlias, `#swarm:${HS_NAME}`); + assert.equal(tail[0].sender, META); + assert.equal(tail[0].form, "message"); + assert.equal(tail[0].basis, null, "unproven, not 'not for her'"); +}); + +test("room tail: a quiet room, with nothing delivered, still returns its recent lines", async () => { + const { tail } = await tailed({ preload: { [OPERATOR]: lines(OPERATOR, 3) } }); + assert.deepEqual(tail.map(l => l.text), ["line 0", "line 1", "line 2"]); + assert.ok(tail.every(l => l.roomAlias === `#operator:${HS_NAME}`)); +}); + +test("room tail: a room she is not in the speaking set of is absent, and a DM she holds is present with basis direct", async () => { + const STRANGER = "!stranger:gamecult.test", DM = "!mydm:gamecult.test"; + const { tail } = await tailed({ + preload: { [STRANGER]: lines(STRANGER, 2), [DM]: lines(DM, 2), [ROOM]: lines(ROOM, 1) }, + member: [...Object.values(ROOMS), STRANGER, DM], + direct: { [META]: [DM] }, + }); + assert.deepEqual([...new Set(tail.map(l => l.roomAlias))].sort(), [DM, `#swarm:${HS_NAME}`]); + assert.ok(tail.filter(l => l.roomAlias === DM).every(l => l.basis === "direct")); + assert.ok(!tail.some(l => l.eventId.startsWith("$str"))); +}); + +test("room tail: about 60 lines over all rooms, the newest, whatever the room count", async () => { + const dms = ["!d1:gamecult.test", "!d2:gamecult.test", "!d3:gamecult.test", "!d4:gamecult.test"]; + const preload = { [ROOM]: lines(ROOM, 30, 0), [OPERATOR]: lines(OPERATOR, 30, 30) }; + dms.forEach((dm, i) => { preload[dm] = lines(dm, 30, 60 + 30 * i); }); + const { tail } = await tailed({ preload, direct: { [META]: dms }, member: [...Object.values(ROOMS), ...dms] }); + assert.equal(tail.length, 60); + assert.deepEqual(tail.map(l => l.atUnixMs), tail.map(l => l.atUnixMs).sort((a, b) => a - b), "oldest first"); + assert.equal(Math.min(...tail.map(l => l.atUnixMs)), 110, "the oldest kept is the 60th newest of the 120 offered"); +}); + +test("room tail: acking a delivery does not hide it, so an unaddressed line she has acked is still found, and mention basis rides on the line", async () => { + const events = [ + withContent("$a", META, { body: "first" }, 3000), + withContent("$b", META, { body: "tag", "m.mentions": { user_ids: [SELF] } }, 3001), + ]; + const hs = await fakeHomeserver({ sync: since => since === null ? { next_batch: "b0" } : { next_batch: "b1", ...joined({ [ROOM]: { timeline: { events } } }) } }); + try { + const mouth = mouthFor(hs); + await mouth.syncOnce(); + const made = await mouth.syncOnce(); + assert.deepEqual((await mouth.tail()).map(l => [l.eventId, l.basis]), [["$a", null], ["$b", "tag"]]); + await mouth.ack(made[1].seq); + assert.equal(mouth.pendingAfter(0).length, 0); + assert.deepEqual((await mouth.tail()).map(l => l.eventId), ["$a", "$b"], "acked lines are the ones no delivery will bring again"); + } finally { await hs.close(); } +}); + +test("room tail: a room that never answers is absent after the timeout and does not hold the others", async () => { + const hs = await fakeHomeserver({ preload: { [OPERATOR]: lines(OPERATOR, 2) }, sync: since => ({ next_batch: since ?? "b0" }) }); + const hang = (url, init) => String(url).includes(encodeURIComponent(ROOM)) && String(url).includes("/messages") ? new Promise(() => {}) : hs.fetch(url, init); + try { + const mouth = mouthFor(hs, { fetch: hang, sendTimeoutMs: 50 }); + await mouth.syncOnce(); + const tail = await mouth.tail(); + assert.deepEqual([...new Set(tail.map(l => l.roomAlias))], [`#operator:${HS_NAME}`]); + } finally { await hs.close(); } +}); + +test("room tail: a mouth that has not started reads no room", async () => { + const hs = await fakeHomeserver({ preload: { [ROOM]: lines(ROOM, 2) }, sync: since => ({ next_batch: since ?? "b0" }) }); + try { + assert.deepEqual(await mouthFor(hs).tail(), []); + assert.equal(hs.seen.filter(r => r.path.includes("/messages")).length, 0); + } finally { await hs.close(); } +}); + +test("room tail: a body is clipped like a context line, and an unreadable room is absent without silencing the others", async () => { + const { tail } = await tailed({ preload: { [ROOM]: [message("$big", META, "x".repeat(5000), 10)] } }); + assert.equal(Buffer.byteLength(tail[0].text), docs.limits.contextBytes); + const hs = await fakeHomeserver({ failContext: true, sync: since => ({ next_batch: since ?? "b0" }) }); + try { + const mouth = mouthFor(hs); + await mouth.syncOnce(); + assert.deepEqual(await mouth.tail(), []); + } finally { await hs.close(); } +}); + +test("room tail: only the v3 answer carries it; the v2 answer a v1 request gets is unchanged", async () => { + const { tail } = await tailed({ preload: { [ROOM]: lines(ROOM, 2) } }); + const pack = { encode: v => Buffer.from(JSON.stringify(v)) }; + assert.deepEqual(docs.parsePendingV3(pageOf("eureka", [], pack, () => {}, true, tail)).tail.map(l => l.text), ["line 0", "line 1"]); + const v2 = pageOf("eureka", [], pack, () => {}, false, tail); + assert.equal(v2.length, 2); + assert.deepEqual(docs.parsePending(v2), { persona: "eureka", deliveries: [] }); + assert.throws(() => docs.parsePendingV3(["eureka", []]), /3-field/); +}); + +test("room tail: the reply byte budget trims the oldest tail lines, never a delivery", async () => { + const pack = { encode: v => Buffer.from(JSON.stringify(v)) }; + const big = Array.from({ length: 60 }, (_, i) => ({ roomAlias: "#r:x", eventId: `$${i}`, atUnixMs: i, sender: "@a:x", form: "message", basis: null, text: "y".repeat(5000) })); + const kept = docs.parsePendingV3(pageOf("eureka", [], pack, () => {}, true, big)).tail; + assert.ok(kept.length > 0 && kept.length < 60); + assert.equal(kept.at(-1).eventId, "$59", "the newest survive"); +}); diff --git a/tools/persona-chat-documents.mjs b/tools/persona-chat-documents.mjs index 4867901..e47b7c8 100644 --- a/tools/persona-chat-documents.mjs +++ b/tools/persona-chat-documents.mjs @@ -39,7 +39,7 @@ const maxContext = 20; // Per-delivery bounds, applied where the mouth reads Matrix: the body is bounded like a send, // a context line and a display name are smaller, and a reply is paged to a byte budget. -export const limits = Object.freeze({ textBytes: maxTextBytes, contextBytes: 1000, displayBytes: 256, mentions: 64, replyBytes: 256 * 1024 }); +export const limits = Object.freeze({ textBytes: maxTextBytes, contextBytes: 1000, displayBytes: 256, mentions: 64, replyBytes: 256 * 1024, tailPerRoom: 20, tailTotal: 60 }); // `value` cut to at most `maxBytes` of UTF-8 without splitting a character. export function clip(value, maxBytes) { @@ -237,13 +237,26 @@ export function parsePending(t) { return { persona: text(persona, "persona"), deliveries: deliveries.map(parseDelivery) }; } -export function pendingV3Tuple({ persona, deliveries }) { - return [text(persona, "persona"), deliveries.map(deliveryV3Tuple)]; +// One line of a room's recent talk, newest-unacked: room alias (or room id for a DM), event id, origin time, sender, form, basis, body. +// Data for the reader, never an instruction: no delivery, no wake, no ack position hangs on it. +export function tailLineTuple(l) { + return [text(l.roomAlias, "room"), text(l.eventId, "event_id"), seq(l.atUnixMs, "at_unix_ms"), text(l.sender, "sender"), form(l.form), basis(l.basis ?? null), String(l.text)]; +} +function parseTailLine(t) { + const [roomAlias, eventId, atUnixMs, sender, f, b, body] = tuple(t, 7, "tail_line"); + return { roomAlias: text(roomAlias, "room"), eventId: text(eventId, "event_id"), atUnixMs: seq(atUnixMs, "at_unix_ms"), sender: text(sender, "sender"), form: form(f), basis: basis(b), text: String(body) }; +} + +// The v3 answer: the persona, the deliveries, then the tail (at most tailPerRoom lines a room, tailTotal in all, oldest first). +export function pendingV3Tuple({ persona, deliveries, tail = [] }) { + if (tail.length > limits.tailTotal) throw new Error("chat_pending tail is over the line bound"); + return [text(persona, "persona"), deliveries.map(deliveryV3Tuple), tail.map(tailLineTuple)]; } export function parsePendingV3(t) { - const [persona, deliveries] = tuple(t, 2, "chat_pending_v3"); + const [persona, deliveries, tail] = tuple(t, 3, "chat_pending_v3"); if (!Array.isArray(deliveries)) throw new Error("chat_pending deliveries must be a list"); - return { persona: text(persona, "persona"), deliveries: deliveries.map(parseDeliveryV3) }; + if (!Array.isArray(tail)) throw new Error("chat_pending tail must be a list"); + return { persona: text(persona, "persona"), deliveries: deliveries.map(parseDeliveryV3), tail: tail.map(parseTailLine) }; } // The CultNet document_put_raw envelope every chat_* document travels in. diff --git a/tools/persona-matrix-mouth.mjs b/tools/persona-matrix-mouth.mjs index 439816e..d6b4f94 100644 --- a/tools/persona-matrix-mouth.mjs +++ b/tools/persona-matrix-mouth.mjs @@ -63,6 +63,7 @@ const DELIVERED_INDEX = 4000; // delivered event ids remembered so a read report const DM_SCAN = 200; // joined rooms read when looking for an existing DM with a person const NOT_E2EE = "This room is not end-to-end encrypted: the homeserver can read what is said here."; const dayOf = ms => new Date(ms).toISOString().slice(0, 10); +const TAIL_TTL_MS = 15 * 1000; const HEAD_PAGE = 50; // the room head a gated send reads; also the most ids a RoomMoved receipt names // The kind of an event for a delivery: an edit (m.replace) and an automated notice (m.notice) are not messages anyone is addressed by. @@ -141,6 +142,7 @@ export function createMouth({ let started = false; let lastPresence = null; // 'online' | 'unavailable' | 'offline' | null: command-only, suppresses repeat presence PUTs let activeUntil = 0; // her last sign of life in a turn, plus the grace + const tailReads = new Map(); // room id -> { at, lines }: a room's tail read, kept TAIL_TTL_MS or until a sync brings the room news const delivered = new Map(); // event id -> { roomId, roomAlias, seq }: what the mouth handed her, newest last, bounded const readThrough = new Map(); // room id -> { seq, eventId } of the last receipt that landed const readTails = new Map(); // room id -> the read report in flight: reports for one room run one at a time @@ -209,6 +211,8 @@ export function createMouth({ } } const unjoined = () => joinedAliases.size < rooms.length; + // The rooms this Persona may speak in: a configured room she has joined, or one of her DMs. One rule for send and for the tail. + const speakableRoomId = alias => joinedAliases.has(alias) ? roomIdByAlias.get(alias) : directRooms.has(alias) ? alias : null; // Join the configured rooms the account is not in, only while the brake is released. // One room failing is logged and retried on the next round; it never stops the others. @@ -294,11 +298,11 @@ export function createMouth({ // thread under, an event her account wrote, so her Self run's words count as hers), "room" (an @room mention). In that order; null // when none holds. Her names, her projects' names and the sender's role are the heartbeat's to match (Huginn address.rs), never the // mouth's. A reply target that cannot be read is null and logged: unknown, not "not hers". - async function basisOf(roomId, event) { + async function basisOf(roomId, event, lookup = true) { if (directRooms.has(roomId)) return "direct"; const mentions = event.content["m.mentions"]; if (Array.isArray(mentions?.user_ids) ? mentions.user_ids.includes(selfId) : mentions === undefined && pillFor(event.content.formatted_body)) return "tag"; - if (formOf(event.content) === "message" && await repliesToMe(roomId, event.content["m.relates_to"])) return "reply"; // an edit or a notice wakes no one: no fetch for it + if (lookup && formOf(event.content) === "message" && await repliesToMe(roomId, event.content["m.relates_to"])) return "reply"; // an edit or a notice wakes no one: no fetch for it return mentions?.room === true ? "room" : null; } // A pill is an anchor whose href is exactly her matrix.to link; a longer id, an attribute that merely contains one and a quoted reply do not count. @@ -379,6 +383,7 @@ export function createMouth({ const shown = event.content?.displayname; if (event.type === "m.room.member" && typeof event.state_key === "string" && typeof shown === "string" && shown !== "") displayNames.set(event.state_key, docs.clip(shown, docs.limits.displayBytes)); } + if ((room.timeline?.events ?? []).length > 0) tailReads.delete(roomId); for (const event of room.timeline?.events ?? []) { if (event.type !== "m.room.message" || typeof event.content?.body !== "string") continue; if (typeof event.sender !== "string" || event.sender === "" || typeof event.event_id !== "string" || event.event_id === "") continue; @@ -472,6 +477,41 @@ export function createMouth({ touch(); } + // Her rooms' recent talk, for a reader that was not woken for it: in each room she may speak in, the newest tailPerRoom lines + // whether or not they were delivered or acked (a delivery the wake rule left unaddressed is acked and gone from `pending`; the tail + // is where a beat still finds it, and the reader removes what a delivery already shows), the newest tailTotal over all rooms, + // oldest first. From the same /messages read as a delivery's context, as her account, bodies clipped as context lines are. The + // basis is what Matrix says on the event itself (no reply lookup: nil is unproven). A room that cannot be read in sendTimeoutMs + // is logged and absent, so one stuck room never holds the reply. + async function tail() { + if (!started) return []; + const targets = new Set(); + for (const alias of [...joinedAliases, ...directRooms]) { const id = speakableRoomId(alias); if (id) targets.add(id); } + const perRoom = await Promise.all([...targets].map(async roomId => { + let read = tailReads.get(roomId); + if (!read || now() - read.at >= TAIL_TTL_MS) { + let timer; + const lines = Promise.race([ + roomEvents(roomId), + new Promise((_, fail) => { timer = setTimeout(() => fail(new Error(`no answer in ${sendTimeoutMs} ms`)), sendTimeoutMs); }), + ]).catch(error => { say(`tail for ${roomId} not fetched: ${error.message}`); return null; }).finally(() => clearTimeout(timer)); + read = { at: now(), lines }; + tailReads.set(roomId, read); + } + const events = await read.lines; + if (events === null) { if (tailReads.get(roomId) === read) tailReads.delete(roomId); return []; } + const alias = aliasByRoomId.get(roomId) ?? roomId; + const lines = []; + for (const e of events.slice(-docs.limits.tailPerRoom)) { + if (typeof e.event_id !== "string" || e.event_id === "") continue; + const at = Number.isSafeInteger(e.origin_server_ts) && e.origin_server_ts >= 0 ? e.origin_server_ts : 0; + lines.push({ roomAlias: alias, eventId: e.event_id, atUnixMs: at, sender: e.sender, form: formOf(e.content), basis: await basisOf(roomId, e, false), text: bodyText(e.content, docs.limits.contextBytes) }); + } + return lines; + })); + return perRoom.flat().sort((a, b) => a.atUnixMs - b.atUnixMs).slice(-docs.limits.tailTotal); + } + function pendingAfter(afterSeq) { return pending.filter(d => d.seq > afterSeq).map(d => ({ ...d, context: d.context.map(c => ({ ...c })), mentions: [...d.mentions], image: d.image && { ...d.image } })); } @@ -497,7 +537,7 @@ export function createMouth({ if (typeof text !== "string" || text.trim() === "") return refuse("EmptyText"); try { if (!started || unjoined()) await start(); - const roomId = joinedAliases.has(roomAlias) ? roomIdByAlias.get(roomAlias) : directRooms.has(roomAlias) ? roomAlias : null; + const roomId = speakableRoomId(roomAlias); if (!roomId) return refuse("UnknownRoom"); const result = await gate.run(roomId, async () => { if (basis) { @@ -626,7 +666,7 @@ export function createMouth({ } } - return { start, syncOnce, ack, read, pendingAfter, send, openDirect, run, get userId() { return selfId; }, get pendingCount() { return pending.length; } }; + return { start, syncOnce, ack, read, pendingAfter, tail, send, openDirect, run, get userId() { return selfId; }, get pendingCount() { return pending.length; } }; } // Replace every secret in a message. `secrets` is one string or a list. @@ -671,7 +711,8 @@ export function connectionIdFor(connectionKey) { // The reply to a chat_pending_request: deliveries oldest first, each encoded on its own so one that // will not encode is skipped and logged without silencing the rest, up to the reply byte budget. // The caller acks what it has read and asks again. -export function pageOf(persona, deliveries, msgpack, log = () => {}, v3 = false) { +// A v3 reply also carries `tail` (room lines, oldest first), trimmed oldest first to what the byte budget has left after the deliveries. +export function pageOf(persona, deliveries, msgpack, log = () => {}, v3 = false, tail = []) { const page = []; let bytes = 0; for (const delivery of deliveries) { @@ -684,7 +725,19 @@ export function pageOf(persona, deliveries, msgpack, log = () => {}, v3 = false) page.push(tuple); bytes += size; } - return [persona, page]; + if (!v3) return [persona, page]; + const lines = []; + for (const line of tail.slice().reverse()) { + let tuple, size; + try { tuple = docs.tailLineTuple(line); size = msgpack.encode(tuple).length; } catch (error) { + log(`tail line ${line?.eventId} skipped: ${error?.message ?? error}`); + continue; + } + if (bytes + size > docs.limits.replyBytes) break; + lines.unshift(tuple); + bytes += size; + } + return [persona, page, lines]; } // CultNet RUDP endpoint on loopback, one for every Persona in `mouths` (persona -> mouth). @@ -726,7 +779,8 @@ export async function startChatEndpoint({ mouths, runtime, port, connectionKey, const request = docs.parsePendingRequest(tuple); const mouth = mouths.get(request.persona); if (!mouth) return refuseUnknown(peer, request.persona); - respond(peer, request.persona, v3 ? docs.kinds.pendingV3 : docs.kinds.pending, request.persona, pageOf(request.persona, mouth.pendingAfter(request.afterSeq), msgpack, log, v3)); + const tail = v3 ? await mouth.tail().catch(error => { log(`tail not read: ${error?.message ?? error}`); return []; }) : []; + respond(peer, request.persona, v3 ? docs.kinds.pendingV3 : docs.kinds.pending, request.persona, pageOf(request.persona, mouth.pendingAfter(request.afterSeq), msgpack, log, v3, tail)); } else if (document.schemaId === docs.kinds.ack) { const request = docs.parseAck(tuple); const mouth = mouths.get(request.persona); From fc8242f655af50c4c637ef36660989e5ae78c999 Mon Sep 17 00:00:00 2001 From: Heimdall Date: Wed, 11 Nov 2026 08:38:09 +0000 Subject: [PATCH 17/26] Suspension survives sign-in; admin-only Suspend/Unsuspend with a receipt (#53) --- src/Bifrost.Web/Domain/BifrostModels.cs | 3 +- .../Features/Heimdall/HeimdallAuthService.cs | 5 +- .../Membership/MembershipSuspensionService.cs | 79 ++++++++ .../Shared/DashboardSnapshotService.cs | 1 + .../Pages/App/Members/Index.cshtml | 11 +- .../Pages/App/Members/Index.cshtml.cs | 24 ++- src/Bifrost.Web/Program.cs | 1 + .../HeimdallCompletionTests.cs | 39 ++++ .../MembershipSuspensionTests.cs | 179 ++++++++++++++++++ 9 files changed, 337 insertions(+), 5 deletions(-) create mode 100644 src/Bifrost.Web/Features/Membership/MembershipSuspensionService.cs create mode 100644 tests/Bifrost.Web.Tests/MembershipSuspensionTests.cs diff --git a/src/Bifrost.Web/Domain/BifrostModels.cs b/src/Bifrost.Web/Domain/BifrostModels.cs index 2f8be41..3cc0506 100644 --- a/src/Bifrost.Web/Domain/BifrostModels.cs +++ b/src/Bifrost.Web/Domain/BifrostModels.cs @@ -363,7 +363,8 @@ public sealed class Membership public sealed class RoleAssignment { - public Guid Id { get; set; } = Guid.NewGuid(); + // Left empty until saved: a key preset on a row added to an already-saved membership makes EF treat it as existing and fail the save. + public Guid Id { get; set; } public Guid MembershipId { get; set; } diff --git a/src/Bifrost.Web/Features/Heimdall/HeimdallAuthService.cs b/src/Bifrost.Web/Features/Heimdall/HeimdallAuthService.cs index 0d39c2f..8d10b53 100644 --- a/src/Bifrost.Web/Features/Heimdall/HeimdallAuthService.cs +++ b/src/Bifrost.Web/Features/Heimdall/HeimdallAuthService.cs @@ -319,7 +319,8 @@ public sealed class HeimdallAuthService( UserAccountId = userAccount.Id, CreatedAtUtc = now }; - if (grantMemberAccess) + // A suspension is an admin's decision: the claim neither lifts it nor, absent, imposes it. + if (grantMemberAccess && userAccount.Membership.Status != MembershipStatus.Suspended) { userAccount.Membership.Status = MembershipStatus.Active; userAccount.Membership.ApprovedAtUtc ??= now; @@ -332,7 +333,7 @@ public sealed class HeimdallAuthService( userAccount, displayName, cancellationToken); - if (grantMemberAccess) + if (grantMemberAccess && userAccount.Membership!.Status == MembershipStatus.Active) { ApplicationBootstrapper.EnsureRole( userAccount.Membership!, diff --git a/src/Bifrost.Web/Features/Membership/MembershipSuspensionService.cs b/src/Bifrost.Web/Features/Membership/MembershipSuspensionService.cs new file mode 100644 index 0000000..cb5626c --- /dev/null +++ b/src/Bifrost.Web/Features/Membership/MembershipSuspensionService.cs @@ -0,0 +1,79 @@ +using Bifrost.Web.Data; +using Bifrost.Web.Domain; +using Bifrost.Web.Features.Shared; +using Microsoft.EntityFrameworkCore; + +namespace Bifrost.Web.Features.Membership; + +public enum SuspensionOutcome +{ + Done, + NotAdmin, + NoReason, + NoSuchAccount, + NotApplicable, + ProtectedAccount +} + +/// +/// The one place a membership becomes Suspended or stops being. Only a platform admin may call it, every use leaves an audit +/// receipt naming the admin, the account and the reason, and no sign-in path writes or lifts a suspension (Heimdall and GitHub +/// sign-in leave a Suspended membership as it is). Unsuspending restores Active for an account that was once approved, +/// Authenticated for one that never was; a pending request is not remembered. +/// +public sealed class MembershipSuspensionService(BifrostDbContext dbContext, AuditTrailService auditTrailService) +{ + public const int MaxReasonLength = 500; + + public Task SuspendAsync(CurrentBifrostActor actor, Guid userAccountId, string? reason, CancellationToken cancellationToken) => + ChangeAsync(actor, userAccountId, reason, suspend: true, cancellationToken); + + public Task UnsuspendAsync(CurrentBifrostActor actor, Guid userAccountId, string? reason, CancellationToken cancellationToken) => + ChangeAsync(actor, userAccountId, reason, suspend: false, cancellationToken); + + private async Task ChangeAsync(CurrentBifrostActor actor, Guid userAccountId, string? reason, bool suspend, CancellationToken cancellationToken) + { + if (!actor.CanManageMembership || actor.UserAccount is null) + { + return SuspensionOutcome.NotAdmin; + } + + if (string.IsNullOrWhiteSpace(reason)) + { + return SuspensionOutcome.NoReason; + } + + var user = await dbContext.UserAccounts.Include(x => x.Membership!).ThenInclude(x => x.RoleAssignments).SingleOrDefaultAsync(x => x.Id == userAccountId, cancellationToken); + if (user?.Membership is not { } membership) + { + return SuspensionOutcome.NoSuchAccount; + } + + // An admin cannot suspend themselves or another platform admin here (the same derivation that makes an actor an admin): + // bootstrap admins are re-promoted to Active at sign-in by configuration, so the suspension would be undone without a receipt. + if (suspend && (user.Id == actor.UserAccount.Id || new CurrentBifrostActor(true, user).IsPlatformAdmin)) + { + return SuspensionOutcome.ProtectedAccount; + } + + if (suspend == (membership.Status == MembershipStatus.Suspended)) + { + return SuspensionOutcome.NotApplicable; + } + + var before = membership.Status; + membership.Status = suspend + ? MembershipStatus.Suspended + : membership.ApprovedAtUtc is null ? MembershipStatus.Authenticated : MembershipStatus.Active; + var clipped = reason.Trim().Length > MaxReasonLength ? reason.Trim()[..MaxReasonLength] : reason.Trim(); + // One save: the status change and its receipt stand or fall together (RecordAsync saves the shared context). + await auditTrailService.RecordAsync( + actor.UserAccount.Id, + nameof(Membership), + membership.Id, + suspend ? "membership.suspended" : "membership.unsuspended", + $"{(suspend ? "Suspended" : "Unsuspended")} {user.DisplayName} ({before} -> {membership.Status}): {clipped}", + cancellationToken); + return SuspensionOutcome.Done; + } +} diff --git a/src/Bifrost.Web/Features/Shared/DashboardSnapshotService.cs b/src/Bifrost.Web/Features/Shared/DashboardSnapshotService.cs index 3a31269..cd0d8f2 100644 --- a/src/Bifrost.Web/Features/Shared/DashboardSnapshotService.cs +++ b/src/Bifrost.Web/Features/Shared/DashboardSnapshotService.cs @@ -92,6 +92,7 @@ public sealed class DashboardSnapshotService(BifrostDbContext dbContext, TimePro var recentActivity = await dbContext.AuditEvents .AsNoTracking() .Include(x => x.ActorUserAccount) + .Where(x => !x.Action.StartsWith("membership.suspended") && !x.Action.StartsWith("membership.unsuspended")) // a reason is for admins, not the member dashboard .OrderByDescending(x => x.OccurredAtUtc) .Take(8) .Select(x => new DashboardActivity( diff --git a/src/Bifrost.Web/Pages/App/Members/Index.cshtml b/src/Bifrost.Web/Pages/App/Members/Index.cshtml index 0d9d546..b4d67e5 100644 --- a/src/Bifrost.Web/Pages/App/Members/Index.cshtml +++ b/src/Bifrost.Web/Pages/App/Members/Index.cshtml @@ -176,7 +176,7 @@ - @if (Model.Actor.CanManageMembership && user.Status != MembershipStatus.Active) + @if (Model.Actor.CanManageMembership && user.Status != MembershipStatus.Active && user.Status != MembershipStatus.Suspended) {
@@ -184,6 +184,15 @@
} + @if (Model.Actor.CanManageMembership) + { +
+ + + +
+ } + @if (Model.Actor.CanManageMembership) {
diff --git a/src/Bifrost.Web/Pages/App/Members/Index.cshtml.cs b/src/Bifrost.Web/Pages/App/Members/Index.cshtml.cs index 2f385b7..e9b536b 100644 --- a/src/Bifrost.Web/Pages/App/Members/Index.cshtml.cs +++ b/src/Bifrost.Web/Pages/App/Members/Index.cshtml.cs @@ -1,6 +1,7 @@ using System.ComponentModel.DataAnnotations; using Bifrost.Web.Data; using Bifrost.Web.Domain; +using Bifrost.Web.Features.Membership; using Bifrost.Web.Features.Shared; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; @@ -12,6 +13,7 @@ public sealed class IndexModel( BifrostDbContext dbContext, ICurrentBifrostActorAccessor actorAccessor, AuditTrailService auditTrailService, + MembershipSuspensionService suspension, TimeProvider timeProvider) : PageModel { [BindProperty] @@ -102,7 +104,12 @@ public sealed class IndexModel( .ThenInclude(x => x.RoleAssignments) .SingleAsync(x => x.Id == userAccountId, cancellationToken); - user.Membership!.Status = MembershipStatus.Active; + if (user.Membership!.Status == MembershipStatus.Suspended) + { + return RedirectToPage(); // only Unsuspend lifts a suspension + } + + user.Membership.Status = MembershipStatus.Active; user.Membership.ApprovedAtUtc = timeProvider.GetUtcNow(); ApplicationBootstrapper.EnsureRole( user.Membership, @@ -124,6 +131,21 @@ public sealed class IndexModel( return RedirectToPage(); } + public async Task OnPostSuspendAsync(Guid userAccountId, string? reason, CancellationToken cancellationToken) => + await ChangeSuspensionAsync(userAccountId, reason, suspend: true, cancellationToken); + + public async Task OnPostUnsuspendAsync(Guid userAccountId, string? reason, CancellationToken cancellationToken) => + await ChangeSuspensionAsync(userAccountId, reason, suspend: false, cancellationToken); + + private async Task ChangeSuspensionAsync(Guid userAccountId, string? reason, bool suspend, CancellationToken cancellationToken) + { + Actor = await actorAccessor.GetAsync(cancellationToken); + var outcome = suspend + ? await suspension.SuspendAsync(Actor, userAccountId, reason, cancellationToken) + : await suspension.UnsuspendAsync(Actor, userAccountId, reason, cancellationToken); + return outcome == SuspensionOutcome.NotAdmin ? Forbid() : RedirectToPage(); + } + public async Task OnPostToggleRoleAsync(Guid userAccountId, MemberRole role, CancellationToken cancellationToken) { Actor = await actorAccessor.GetAsync(cancellationToken); diff --git a/src/Bifrost.Web/Program.cs b/src/Bifrost.Web/Program.cs index 2a5ff5f..e19b8d0 100644 --- a/src/Bifrost.Web/Program.cs +++ b/src/Bifrost.Web/Program.cs @@ -117,6 +117,7 @@ builder.Services.AddHttpContextAccessor(); builder.Services.AddSingleton(TimeProvider.System); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/tests/Bifrost.Web.Tests/HeimdallCompletionTests.cs b/tests/Bifrost.Web.Tests/HeimdallCompletionTests.cs index 532f669..afcfcba 100644 --- a/tests/Bifrost.Web.Tests/HeimdallCompletionTests.cs +++ b/tests/Bifrost.Web.Tests/HeimdallCompletionTests.cs @@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Authentication.OAuth; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.WebUtilities; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -864,6 +865,44 @@ public sealed class HeimdallCompletionTests } } + [Fact] + public async Task A_suspended_account_stays_suspended_and_gains_no_role_through_a_sign_in_with_the_member_claim() + { + using var host = new HeimdallHost(); + var accountId = HeimdallShapes.NewAccountId(); + Assert.True((await Genuine(host, await host.StartAsync(member: true), accountId, ["member_access"])).SignedIn); + await host.Db(async db => + { + var account = await db.UserAccounts.Include(x => x.Membership).SingleAsync(x => x.HeimdallAccountId == accountId); + account.Membership!.Status = MembershipStatus.Suspended; + await db.SaveChangesAsync(); + return 0; + }); + + var rolesBefore = (await host.Account(accountId))!.Membership!.RoleAssignments.Count; + var again = await Genuine(host, await host.StartAsync(member: true), accountId, ["member_access"]); + + Assert.True(again.SignedIn); + var after = (await host.Account(accountId))!.Membership!; + Assert.Equal(MembershipStatus.Suspended, after.Status); + Assert.Equal(rolesBefore, after.RoleAssignments.Count); + } + + [Fact] + public async Task A_chat_only_account_that_later_signs_in_with_member_access_becomes_a_member() + { + using var host = new HeimdallHost(); + var accountId = HeimdallShapes.NewAccountId(); + Assert.True((await Genuine(host, await host.StartAsync(member: false), accountId)).SignedIn); + Assert.Equal(MembershipStatus.Authenticated, (await host.Account(accountId))!.Membership!.Status); + + Assert.True((await Genuine(host, await host.StartAsync(member: true), accountId, ["member_access"])).SignedIn); + + var account = (await host.Account(accountId))!; + Assert.Equal(MembershipStatus.Active, account.Membership!.Status); + Assert.Contains(account.Membership.RoleAssignments, x => x.Role == MemberRole.StandardMember); + } + [Fact] public async Task A_token_capability_grants_no_role_to_a_sign_in_that_did_not_ask_for_member_access() { diff --git a/tests/Bifrost.Web.Tests/MembershipSuspensionTests.cs b/tests/Bifrost.Web.Tests/MembershipSuspensionTests.cs new file mode 100644 index 0000000..85e12ca --- /dev/null +++ b/tests/Bifrost.Web.Tests/MembershipSuspensionTests.cs @@ -0,0 +1,179 @@ +using Bifrost.Web.Data; +using Bifrost.Web.Domain; +using Bifrost.Web.Features.Membership; +using Bifrost.Web.Features.Shared; +using Bifrost.Web.Pages.App.Members; +using Bifrost.Web.Tests.Support; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Bifrost.Web.Tests; + +/// A suspension is an admin's decision: only the admin path sets or lifts it, with a receipt, and the members page's Approve cannot lift it. +public sealed class MembershipSuspensionTests : IClassFixture +{ + private readonly TestWebApplicationFactory _factory; + + public MembershipSuspensionTests(TestWebApplicationFactory factory) => _factory = factory; + + private sealed record Seeded(Guid AdminId, Guid MemberId, Guid OrdinaryId, Guid OtherAdminId); + + private async Task SeedAsync(MembershipStatus memberStatus = MembershipStatus.Active, bool approved = true) + { + using var scope = _factory.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + UserAccount Account(string name, MembershipStatus status, bool admin, bool wasApproved) => new() + { + DisplayName = name, + BifrostIdentity = $"{name}-{Guid.NewGuid():N}"[..20], + NormalizedBifrostIdentity = $"{name}-{Guid.NewGuid():N}"[..20], + Membership = new Bifrost.Web.Domain.Membership + { + Status = status, + IsPlatformAdmin = admin, + ApprovedAtUtc = wasApproved ? DateTimeOffset.UtcNow : null + } + }; + + var admin = Account("admin", MembershipStatus.Active, true, true); + var otherAdmin = Account("admin2", MembershipStatus.Active, true, true); + var member = Account("member", memberStatus, false, approved); + var ordinary = Account("ordinary", MembershipStatus.Active, false, true); + db.UserAccounts.AddRange(admin, otherAdmin, member, ordinary); + await db.SaveChangesAsync(); + return new Seeded(admin.Id, member.Id, ordinary.Id, otherAdmin.Id); + } + + private async Task InScopeAsync(Func> body) + { + using var scope = _factory.Services.CreateScope(); + return await body(scope.ServiceProvider, scope.ServiceProvider.GetRequiredService()); + } + + private Task ActorAsync(Guid id) => InScopeAsync(async (_, db) => + new CurrentBifrostActor(true, await db.UserAccounts.Include(x => x.Membership!).ThenInclude(x => x.RoleAssignments).SingleAsync(x => x.Id == id))); + + private Task StatusAsync(Guid id) => InScopeAsync(async (_, db) => + (await db.Memberships.AsNoTracking().SingleAsync(x => x.UserAccountId == id)).Status); + + private Task> AuditAsync(string action) => InScopeAsync((_, db) => + db.AuditEvents.AsNoTracking().Where(x => x.Action == action).ToListAsync()); + + private Task SuspendAsync(Guid by, Guid target, string? reason) => InScopeAsync(async (sp, _) => + await sp.GetRequiredService().SuspendAsync(await ActorAsync(by), target, reason, CancellationToken.None)); + + private Task UnsuspendAsync(Guid by, Guid target, string? reason) => InScopeAsync(async (sp, _) => + await sp.GetRequiredService().UnsuspendAsync(await ActorAsync(by), target, reason, CancellationToken.None)); + + [Fact] + public async Task Only_a_platform_admin_can_suspend_and_the_receipt_names_who_and_why() + { + var seed = await SeedAsync(); + + Assert.Equal(SuspensionOutcome.NotAdmin, await SuspendAsync(seed.OrdinaryId, seed.MemberId, "because")); + Assert.Equal(MembershipStatus.Active, await StatusAsync(seed.MemberId)); + Assert.Equal(SuspensionOutcome.NoReason, await SuspendAsync(seed.AdminId, seed.MemberId, " ")); + Assert.Equal(MembershipStatus.Active, await StatusAsync(seed.MemberId)); + + Assert.Equal(SuspensionOutcome.Done, await SuspendAsync(seed.AdminId, seed.MemberId, "spam in chat")); + Assert.Equal(MembershipStatus.Suspended, await StatusAsync(seed.MemberId)); + var receipt = Assert.Single(await AuditAsync("membership.suspended"), x => x.Detail.Contains("spam in chat")); + Assert.Equal(seed.AdminId, receipt.ActorUserAccountId); + Assert.Contains("Active -> Suspended", receipt.Detail); + } + + [Fact] + public async Task An_admin_cannot_suspend_themselves_or_another_platform_admin() + { + var seed = await SeedAsync(); + + Assert.Equal(SuspensionOutcome.ProtectedAccount, await SuspendAsync(seed.AdminId, seed.AdminId, "oops")); + Assert.Equal(SuspensionOutcome.ProtectedAccount, await SuspendAsync(seed.AdminId, seed.OtherAdminId, "coup")); + Assert.Equal(MembershipStatus.Active, await StatusAsync(seed.AdminId)); + Assert.Equal(MembershipStatus.Active, await StatusAsync(seed.OtherAdminId)); + } + + [Fact] + public async Task A_suspension_reason_does_not_reach_the_member_dashboard_snapshot() + { + var seed = await SeedAsync(); + await SuspendAsync(seed.AdminId, seed.MemberId, "private-reason-canary"); + + var snapshot = await InScopeAsync(async (sp, _) => await sp.GetRequiredService().GetAsync(seed.OrdinaryId, CancellationToken.None)); + + Assert.DoesNotContain(snapshot.RecentActivity, x => x.Detail.Contains("private-reason-canary")); + Assert.Contains(await AuditAsync("membership.suspended"), x => x.Detail.Contains("private-reason-canary")); + } + + [Fact] + public async Task A_role_granted_admin_is_protected_like_a_flagged_one() + { + var seed = await SeedAsync(); + await InScopeAsync(async (_, db) => + { + var membership = await db.Memberships.SingleAsync(x => x.UserAccountId == seed.MemberId); + db.RoleAssignments.Add(new RoleAssignment { MembershipId = membership.Id, Role = MemberRole.PlatformAdmin }); + await db.SaveChangesAsync(); + return 0; + }); + + Assert.Equal(SuspensionOutcome.ProtectedAccount, await SuspendAsync(seed.AdminId, seed.MemberId, "role admin")); + } + + [Theory] + [InlineData(true, MembershipStatus.Active)] + [InlineData(false, MembershipStatus.Authenticated)] + public async Task Unsuspend_works_only_by_the_admin_path_and_with_a_reason(bool approved, MembershipStatus restored) + { + var seed = await SeedAsync(MembershipStatus.Suspended, approved); + + Assert.Equal(SuspensionOutcome.NotAdmin, await UnsuspendAsync(seed.OrdinaryId, seed.MemberId, "please")); + Assert.Equal(SuspensionOutcome.NoReason, await UnsuspendAsync(seed.AdminId, seed.MemberId, null)); + Assert.Equal(MembershipStatus.Suspended, await StatusAsync(seed.MemberId)); + + Assert.Equal(SuspensionOutcome.Done, await UnsuspendAsync(seed.AdminId, seed.MemberId, "appeal upheld")); + Assert.Equal(restored, await StatusAsync(seed.MemberId)); + Assert.Contains(await AuditAsync("membership.unsuspended"), x => x.Detail.Contains("appeal upheld") && x.ActorUserAccountId == seed.AdminId); + Assert.Equal(SuspensionOutcome.NotApplicable, await UnsuspendAsync(seed.AdminId, seed.MemberId, "again")); + } + + [Fact] + public async Task The_members_page_Approve_cannot_lift_a_suspension() + { + var seed = await SeedAsync(MembershipStatus.Suspended); + + var result = await InScopeAsync(async (sp, db) => + { + var page = new IndexModel( + db, new FixedActor(await ActorAsync(seed.AdminId)), sp.GetRequiredService(), + sp.GetRequiredService(), TimeProvider.System); + return await page.OnPostApproveAsync(seed.MemberId, CancellationToken.None); + }); + + Assert.IsType(result); + Assert.Equal(MembershipStatus.Suspended, await StatusAsync(seed.MemberId)); + } + + [Fact] + public async Task The_members_page_suspend_handler_forbids_a_non_admin_and_changes_nothing() + { + var seed = await SeedAsync(); + + var result = await InScopeAsync(async (sp, db) => + { + var page = new IndexModel( + db, new FixedActor(await ActorAsync(seed.OrdinaryId)), sp.GetRequiredService(), + sp.GetRequiredService(), TimeProvider.System); + return await page.OnPostSuspendAsync(seed.MemberId, "nope", CancellationToken.None); + }); + + Assert.IsType(result); + Assert.Equal(MembershipStatus.Active, await StatusAsync(seed.MemberId)); + } + + private sealed class FixedActor(CurrentBifrostActor actor) : ICurrentBifrostActorAccessor + { + public Task GetAsync(CancellationToken cancellationToken) => Task.FromResult(actor); + } +} From 33be966b7dff7fc6bb7443d47ef8f2e2186a83ec Mon Sep 17 00:00:00 2001 From: Heimdall Date: Wed, 11 Nov 2026 10:15:46 +0000 Subject: [PATCH 18/26] Registration gates: proof-of-work CAPTCHA and training-notice consent receipt (#54) --- README.md | 2 +- docs/bridge.md | 2 +- docs/registration-gates.md | 86 ++++ .../Configuration/RegistrationOptions.cs | 31 ++ src/Bifrost.Web/Data/BifrostDbContext.cs | 10 + .../20261111090000_AddConsentReceipts.cs | 40 ++ .../BifrostDbContextModelSnapshot.cs | 37 ++ src/Bifrost.Web/Domain/BifrostModels.cs | 19 + .../Membership/BifrostIdentityService.cs | 8 + src/Bifrost.Web/Features/Oidc/OidcGuard.cs | 25 +- .../RegistrationChallengeService.cs | 137 +++++++ .../Registration/RegistrationRateLimit.cs | 43 ++ .../Features/Registration/TrainingNotice.cs | 17 + .../Features/Shared/ClientAddress.cs | 32 ++ .../Shared/StartupConfigurationValidator.cs | 15 +- src/Bifrost.Web/Program.cs | 80 +++- tests/Bifrost.Web.Tests/DeskOptionsTests.cs | 3 +- tests/Bifrost.Web.Tests/HostTrustTests.cs | 3 +- .../MigrationDiscoveryTests.cs | 1 + tests/Bifrost.Web.Tests/OidcIssuerTests.cs | 2 +- .../PlatformEndpointsTests.cs | 23 +- .../RegistrationGateTests.cs | 367 ++++++++++++++++++ .../RegistrationLimitTests.cs | 239 ++++++++++++ .../ReservedIdentityTests.cs | 10 +- .../Support/OidcLogoutKit.cs | 2 +- .../Support/RegistrationKit.cs | 63 +++ .../Support/TestAuthHandler.cs | 10 + .../Support/TestWebApplicationFactory.cs | 2 + 28 files changed, 1253 insertions(+), 56 deletions(-) create mode 100644 docs/registration-gates.md create mode 100644 src/Bifrost.Web/Configuration/RegistrationOptions.cs create mode 100644 src/Bifrost.Web/Data/Migrations/20261111090000_AddConsentReceipts.cs create mode 100644 src/Bifrost.Web/Features/Registration/RegistrationChallengeService.cs create mode 100644 src/Bifrost.Web/Features/Registration/RegistrationRateLimit.cs create mode 100644 src/Bifrost.Web/Features/Registration/TrainingNotice.cs create mode 100644 src/Bifrost.Web/Features/Shared/ClientAddress.cs create mode 100644 tests/Bifrost.Web.Tests/RegistrationGateTests.cs create mode 100644 tests/Bifrost.Web.Tests/RegistrationLimitTests.cs create mode 100644 tests/Bifrost.Web.Tests/Support/RegistrationKit.cs diff --git a/README.md b/README.md index 0e4ea9d..33302e1 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ For the full staged roadmap, including contribution points, revenue share, patro ## Local Dev Notes - set `GitHubOAuth:ClientId` and `GitHubOAuth:ClientSecret` before using sign-in -- POST `/auth/bifrost/register` with `identity`, optional `displayName`, and optional `returnUrl` to create a Bifrost-native account without OAuth; this signs in as `Authenticated` but does not grant active member access +- POST `/auth/bifrost/register` with `identity`, optional `displayName`, optional `returnUrl`, a solved proof-of-work `challenge` and `nonce` (from `GET /auth/bifrost/challenge`), and `acceptedNoticeVersion` (the `version` from `GET /auth/bifrost/notice`) to create a Bifrost-native account without OAuth; this signs in as `Authenticated` but does not grant active member access - set `Bootstrap:AdminGitHubLogins` with at least one GitHub login for the first active admin path - HTTP bridge ledger configuration (`BIFROST_BRIDGE_BASE_URL` / `BIFROST_BRIDGE_TOKEN`) is removed from live CLI transport. Request-lane, governance, dispatch, and bridge receipts converge on Bifrost-owned `bifrost.crossing_receipt.v1` documents in `.bifrost/bridge-receipts.cc`. - external bridge actions must carry typed CultMesh command provenance such as `BIFROST_CULTMESH_COMMAND_ID`; unreceipted and HTTP-ledger hatches fail closed. diff --git a/docs/bridge.md b/docs/bridge.md index b9b35a6..da23705 100644 --- a/docs/bridge.md +++ b/docs/bridge.md @@ -23,7 +23,7 @@ That means Bifrost owns the operational meaning of the crossing: It does not mean Bifrost should own OAuth sludge. Bifrost is both an identity provider and identity consumer: a GameCult actor should be able to register with Bifrost without OAuth, and that Bifrost identity should be enough to authenticate with most GameCult services. Heimdall owns outside-provider OAuth, linked-account token custody, grants, and signed account claims. Bifrost associates those Heimdall-backed accounts with the Bifrost identity, consumes the permission facts, and decides whether a bridge action is allowed in the Bifrost domain. -The native registration surface is `POST /auth/bifrost/register`. It creates a Bifrost-owned `UserAccount` with `BifrostIdentity` and `NormalizedBifrostIdentity`, signs the caller in with the Bifrost cookie, and leaves membership at `Authenticated`. It does not grant active member access, mint outside-provider tokens, or replace Heimdall's account-link and capability custody. +The native registration surface is `POST /auth/bifrost/register`, which takes `identity`, optional `displayName` and `returnUrl`, a solved proof-of-work `challenge` and `nonce` (from `GET /auth/bifrost/challenge`), and `acceptedNoticeVersion` (from `GET /auth/bifrost/notice`). It creates a Bifrost-owned `UserAccount` with `BifrostIdentity` and `NormalizedBifrostIdentity`, signs the caller in with the Bifrost cookie, and leaves membership at `Authenticated`. It does not grant active member access, mint outside-provider tokens, or replace Heimdall's account-link and capability custody. ## Ownership diff --git a/docs/registration-gates.md b/docs/registration-gates.md new file mode 100644 index 0000000..1b9278a --- /dev/null +++ b/docs/registration-gates.md @@ -0,0 +1,86 @@ +# Public registration gates + +Meta ruled yes to public registration on 2026-11-10. The gates she required: self-hosted CAPTCHA, email verification, +a training-notice consent receipt, rate limits and a brake, reserved names, and new accounts in Users with no vote. +This page scopes the three that did not exist, says where each lives, and records what is built. + +## What exists + +| Gate | Where | State | +| --- | --- | --- | +| Rate limits and brake | `OidcGuardMiddleware` (`docs/oidc-issuer.md`) | Merged. Covers `/connect/*` only, not the register endpoint. | +| Reserved names | `BifrostIdentityService.IsReserved`, 28 names | Merged, at creation and at authorize. | +| Chat tier, no vote | `Authenticated` / `PendingApproval` accounts chat and open no member or governance surface | Merged. | +| Suspension | `MembershipSuspensionService` | Merged. | +| CAPTCHA | `Features/Registration/RegistrationChallengeService` | Built here. | +| Consent receipt | `ConsentReceipt`, `TrainingNotice` | Built here. | +| Email verification | nowhere | Scoped below, not built. | + +The hole these gates close: `POST /auth/bifrost/register` is anonymous, and since the chat tier merged, an +`Authenticated` account it creates can sign in to chat. Before this cut it took an identity name and nothing else. + +## CAPTCHA + +Lives in Bifrost, because the register endpoint is Bifrost's. Heimdall authenticates people through providers and has no +registration form. + +Design: a hashcash proof of work. `GET /auth/bifrost/challenge` returns a Data Protection-protected token (random id and +expiry, five minutes) and a difficulty in bits. The client finds a decimal `nonce` such that +`SHA-256(challenge + ":" + nonce)` has at least that many leading zero bits, and posts both with the registration. The +server verifies the protection, the expiry and the hash, and spends the id once. + +- Self-hosted: no third-party script, request or cookie; nothing to track. +- Accessible: there is nothing to see or hear and nothing to solve by hand. A screen-reader user, a keyboard user and a + script all pass the same way, at the cost of CPU time on the client. +- Cost: difficulty 18 is about 260,000 hashes on average, well under a second natively and a few seconds in browser + JavaScript. It prices a bulk registration run in CPU; it does not stop a determined attacker with CPUs, and it is not a + proof of humanity. The difficulty is embedded in the signed challenge, so a challenge verifies at the difficulty it was issued with. +- Replay: the spent-id set is in memory and bounded (10,000, entries dropped at expiry); a restart forgets it, so a challenge + can be replayed for at most its five-minute life. When the set is full the endpoint refuses rather than grows. One address + cannot fill it alone: `GET /auth/bifrost/challenge` and `POST /auth/bifrost/register` share one per-address budget + (`Bifrost:Registration:Limits`, 10 per minute by default, IPv6 by /64, over budget is 429 with Retry-After). An attacker + with many addresses can still fill the set; that is the accepted bound, the same as oidc-issuer.md. + +Not built: the sign-up form. No registration page exists today; the endpoint is the contract and the only client is a +script. The form (solving in a worker, a `