export type AmqPriority = "urgent" | "normal" | "low"; export interface AmqHeaderV1 { readonly schema: 1; readonly id: string; readonly from: string; readonly to: readonly string[]; readonly thread: string; readonly subject: string; readonly created: string; readonly priority: AmqPriority; readonly kind: string; readonly labels: readonly string[]; readonly context: Readonly>; readonly replyTo: string; readonly replyProject: string; readonly fromProject: string; } export interface AmqMessage { readonly header: AmqHeaderV1; readonly body: string; } export interface WatchCandidate { readonly id: string; readonly priority: AmqPriority; readonly created: string; readonly parseError?: string; } export interface WatchCohort { readonly event: "existing" | "new_message" | "timeout"; readonly messages: readonly WatchCandidate[]; } export type AmqReadOutcome = | { readonly kind: "message"; readonly message: AmqMessage } | { readonly kind: "dlq"; readonly id: string; readonly diagnostic: string }; const PRIORITY_RANK: Readonly> = { urgent: 0, normal: 1, low: 2, }; const VALID_KINDS = new Set([ "", "brainstorm", "review_request", "review_response", "question", "answer", "decision", "status", "todo", ]); export function parseWatchCohort(value: unknown): WatchCohort { const object = record(value, "amq watch --json"); const event = object.event; if (event !== "existing" && event !== "new_message" && event !== "timeout") { throw new Error("amq watch event is invalid"); } const rawMessages = object.messages ?? []; if (!Array.isArray(rawMessages)) throw new Error("amq watch messages must be an array"); return { event, messages: rawMessages.map(parseWatchCandidate), }; } export function parseMessageList(value: unknown): readonly WatchCandidate[] { if (!Array.isArray(value)) throw new Error("amq list --json must return an array"); return value.map(parseWatchCandidate); } export function parseWatchCandidate(value: unknown): WatchCandidate { const object = record(value, "AMQ message header projection"); const id = messageId(object.id); const parseError = optionalString(object.parse_error, "parse_error"); const created = stringOrEmpty(object.created, "created"); const rawPriority = stringOrEmpty(object.priority, "priority"); if (!parseError) requireIso(created, "created"); return { id, priority: parsePriority(rawPriority), created, ...(parseError ? { parseError } : {}), }; } export function parseAmqReadOutcome(value: unknown): AmqReadOutcome { const object = record(value, "amq read --json"); if (object.moved_to_dlq === true) { return { kind: "dlq", id: messageId(object.id), diagnostic: requiredString(object.parse_error, "parse_error"), }; } const header = parseAmqHeaderV1(object.header); const body = object.body; if (typeof body !== "string") throw new Error("amq read body must be a string"); return { kind: "message", message: { header, body } }; } export function parseAmqHeaderV1(value: unknown): AmqHeaderV1 { const object = record(value, "AMQ header"); if (number(object.schema, "schema") !== 1) throw new Error("AMQ header schema must be 1"); const kind = stringOrEmpty(object.kind, "kind"); if (!VALID_KINDS.has(kind)) throw new Error(`AMQ header kind is invalid: ${kind}`); const context = object.context === undefined ? {} : record(object.context, "AMQ header context"); const header: AmqHeaderV1 = { schema: 1, id: messageId(object.id), from: requiredString(object.from, "from"), to: stringArray(object.to, "to", false), thread: requiredString(object.thread, "thread"), subject: stringOrEmpty(object.subject, "subject"), created: requireIso(requiredString(object.created, "created"), "created"), priority: parsePriority(stringOrEmpty(object.priority, "priority")), kind, labels: stringArray(object.labels ?? [], "labels", true), context, replyTo: stringOrEmpty(object.reply_to, "reply_to"), replyProject: stringOrEmpty(object.reply_project, "reply_project"), fromProject: stringOrEmpty(object.from_project, "from_project"), }; return header; } export function sortAndDeduplicateCandidates( candidates: readonly WatchCandidate[], ): readonly WatchCandidate[] { const sorted = [...candidates].sort(compareIncoming); const seen = new Set(); return sorted.filter((candidate) => { if (seen.has(candidate.id)) return false; seen.add(candidate.id); return true; }); } export function sortMessages(messages: readonly AmqMessage[]): readonly AmqMessage[] { return [...messages].sort((left, right) => compareIncoming( { id: left.header.id, priority: left.header.priority, created: left.header.created }, { id: right.header.id, priority: right.header.priority, created: right.header.created }, )); } function compareIncoming(left: WatchCandidate, right: WatchCandidate): number { const invalidOrder = Number(Boolean(right.parseError)) - Number(Boolean(left.parseError)); if (invalidOrder !== 0) return invalidOrder; const priorityOrder = PRIORITY_RANK[left.priority] - PRIORITY_RANK[right.priority]; if (priorityOrder !== 0) return priorityOrder; const createdOrder = left.created.localeCompare(right.created); return createdOrder !== 0 ? createdOrder : left.id.localeCompare(right.id); } function parsePriority(value: string): AmqPriority { if (value === "" || value === "normal") return "normal"; if (value === "urgent" || value === "low") return value; throw new Error(`AMQ priority is invalid: ${value}`); } function messageId(value: unknown): string { const id = requiredString(value, "id"); if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,254}$/.test(id)) { throw new Error("AMQ message ID is not safe for the extension namespace"); } return id; } function record(value: unknown, name: string): Record { if (typeof value !== "object" || value === null || Array.isArray(value)) { throw new Error(`${name} must be an object`); } return value as Record; } function requiredString(value: unknown, name: string): string { if (typeof value !== "string" || value.length === 0) { throw new Error(`${name} must be a non-empty string`); } return value; } function optionalString(value: unknown, name: string): string | undefined { if (value === undefined || value === "") return undefined; if (typeof value !== "string") throw new Error(`${name} must be a string`); return value; } function stringOrEmpty(value: unknown, name: string): string { if (value === undefined) return ""; if (typeof value !== "string") throw new Error(`${name} must be a string`); return value; } function stringArray(value: unknown, name: string, allowEmpty: boolean): readonly string[] { if (!Array.isArray(value) || (!allowEmpty && value.length === 0)) { throw new Error(`${name} must be ${allowEmpty ? "an" : "a non-empty"} array`); } return value.map((item, index) => requiredString(item, `${name}[${index}]`)); } function number(value: unknown, name: string): number { if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`${name} must be a number`); return value; } function requireIso(value: string, name: string): string { if (!Number.isFinite(Date.parse(value))) throw new Error(`${name} must be an ISO timestamp`); return value; }