/** * The model-facing text for one `team` call. * * A handoff is stated as what happened and what did not: `handed off` names the peer * and the card, a failed send says the card is untouched, and an assignment that lost * a race after the send says the peer should claim it. Nothing here claims the peer * read the message — `delivered` means a message file was written for it. */ import { type Card, cardRef } from "../../lib/board/card.ts"; import { type DeliveryFailure, notDeliveredLine } from "../../lib/session-tool.ts"; /** `/team join` landed: the label peers address, and whether mail will reach this session. */ export function formatJoined(label: string, watching: { armed: boolean }): string { const state = watching.armed ? "listening for handoffs" : "not listening"; return `team: on the team as ${label}, ${state}.`; } /** `/team join` did not land, so nothing about this session changed. */ export function formatNotJoined(reason: string): string { return `team: not on the team — ${reason}`; } /** * A card that was handed over, and the peer that got the message. * * `notListening` is the one fact the send cannot tell you and the record can: a peer * that is not listening reads the handoff at its next turn rather than being woken for * it. Saying so is the difference between "delivered" and "delivered and read". */ export function formatHandoff( card: Card, to: string, delivered: readonly string[], notListening: readonly string[] = [], failed: readonly DeliveryFailure[] = [], ): string { const who = delivered.length > 0 ? delivered.join(", ") : to; return [ `${cardRef(card.id)} handed off to ${who}.`, ` ${card.title}`, card.paths.length > 0 ? ` paths: ${card.paths.join(", ")}` : undefined, " The card is already owned by the peer; the handoff is in its inbox.", notListeningLine(notListening), indent(notDeliveredLine(failed)), ] .filter((line): line is string => line !== undefined) .join("\n"); } /** * The line that names a peer which will not be woken, or nothing when every peer is * listening. * * One sentence for both verbs: what the mail does instead, and no pronoun, because the * list can hold one peer or three. */ export function notListeningLine(labels: readonly string[]): string | undefined { if (labels.length === 0) return undefined; return ` Not listening: ${labels.join(", ")} — it reads this at its next turn.`; } /** A continuation line, when a shared sentence has one to add. */ function indent(line: string | undefined): string | undefined { return line === undefined ? undefined : ` ${line}`; } /** * A question sent: who has it, and where the answer will arrive. * * No card is named, because an ask touches none — the reader's next question is whether it * landed, so the line leads with the peer and then says the one thing a send cannot: that * nothing is waiting on an answer. */ export function formatAsk( delivered: readonly string[], notListening: readonly string[] = [], to = "", failed: readonly DeliveryFailure[] = [], ): string { const who = delivered.length > 0 ? delivered.join(", ") : to === "" ? "nobody" : to; return [ `Asked ${who}.`, " The answer arrives as mail; nothing is waiting on it.", notListeningLine(notListening), indent(notDeliveredLine(failed)), ] .filter((line): line is string => line !== undefined) .join("\n"); } /** The send that never landed: the card was not touched. */ export function formatHandoffFailure(card: Card, to: string, reason: string): string { return `${cardRef(card.id)} was not handed off to ${to}: ${reason}\n The card is unchanged.`; } /** * A card offered to the team: who heard, and that nobody holds it yet. * * `offer` records the moment and no ownership, so the sentence has to say the ownership * question is still open — a reader who took "offered" for "assigned" would stop looking * at the card. */ export function formatOffer( card: Card, delivered: readonly string[], notListening: readonly string[] = [], failed: readonly DeliveryFailure[] = [], ): string { const who = delivered.length > 0 ? delivered.join(", ") : "nobody"; return [ `${cardRef(card.id)} offered to ${who}.`, ` ${card.title}`, card.paths.length > 0 ? ` paths: ${card.paths.join(", ")}` : undefined, " The card is unowned until one of them claims it.", notListeningLine(notListening), indent(notDeliveredLine(failed)), ] .filter((line): line is string => line !== undefined) .join("\n"); } /** The broadcast that never landed: no card was touched, and nobody heard. */ export function formatOfferFailure(card: Card, reason: string): string { return `${cardRef(card.id)} was not offered: ${reason}\n The card is unchanged.`; } /** * The broadcast landed and recording the offer then lost a race. * * The peers have an event for a card somebody else has since claimed, moved, or taken * over, so the correction says the offer stands as a message and the card is what the * board now says — not that nothing happened. No second message follows it: an offer's only * next step is a claim, and a claim for a card somebody holds is refused naming the holder, * so the stale text cannot send a peer anywhere the store will not correct it. */ export function formatRacedOffer(card: Card, reason: string): string { return ( `${cardRef(card.id)} was offered, but the card changed before the offer could be recorded: ${reason}\n` + " The peers have the message; the card is as the board shows it now." ); } /** * The send landed and the assignment then lost a race. * * The peer has a message telling it the card is its, so the correction names the * claim rather than pretending nothing happened. */ export function formatRacedAssignment(card: Card, to: string, reason: string): string { return ( `${cardRef(card.id)} was handed off to ${to}, but the card changed before it could be assigned: ${reason}\n` + ` ${to} should run \`board\` action \`claim\` with card "${card.id}".` ); } /** A card taken back to the pool. */ export function formatReclaimed(card: Card, previous: string): string { return `${cardRef(card.id)} reclaimed from ${previous} and back in Backlog; its notes are kept.\n ${card.title}`; } /** * A card the lead took itself. * * The next step is the board's, so it is named: a claim is the one round action that * leaves a card in `plan` waiting on its owner, and a line that stopped at "yours" would * leave the lead holding a card nobody said what to do with. */ export function formatClaimed(card: Card): string { return ( `${cardRef(card.id)} is yours, in plan.\n` + ` ${card.title}\n` + ` next: design it in plan mode, \`plan\` action \`write\`, then \`board\` action \`contract\`` ); }