/** * Factory board full view (S2+S3). * * Unique render seam: (books, view, now) → HTML. * - books: explicit bookKey + ledgerDir (no git-remote guess) * - view: BoardSnapshot success OR loud binding/api error * - now: injected clock for unaccepted bands, leg age, wall-now rule * * Reuses S1 tracer (`loadTicketTrajectoryRuns` + station HTML) for each ticket. * Owns per-book lanes, #162 yamen-column placement (ADR 0053 totality table), * family aggregation + 已完成列 clusters, unknown badge, page filters, blocked * badges, closed drill, and S3 latest-run state / wallclock / cost aggregation * (no parallel receipt parser). * * Page lifecycle: startFactoryBoardPage owns refresh regeneration to an explicit * output path outside every ledger and declares the bound; one-shot write does * not advertise refresh. Snapshot bindings stay fixed; each tick re-reads ledgers. */ import { randomUUID } from "node:crypto"; import { lstat, mkdir, realpath, rename, rm, writeFile } from "node:fs/promises"; import { basename, dirname, join, relative, resolve, sep } from "node:path"; import { formatDurationZh, formatLocalDateTime, formatTokensCompact, formatUsdPrecise, } from "./human-format.ts"; import { DEFAULT_REFRESH_BOUNDARY_SECONDS, buildTicketTrajectoryBookIndex, loadTicketTrajectoryRuns, loadUnboundTrajectoryRuns, renderTicketTrajectoryStationHtml, type TicketTrajectoryBookIndex, type TicketTrajectoryRun, type TrajectoryClock, type TrajectoryScheduler, type UnboundTrajectoryRun, } from "./ticket-trajectory.ts"; import type { BoardSnapshot, SnapshotTicket, TicketIssueState } from "./ticket-snapshot.ts"; /** Unaccepted latest-run mtime bands (page-visible thresholds). */ export const UNACCEPTED_FLYING_MS = 2 * 60 * 1000; export const UNACCEPTED_WATCH_MS = 15 * 60 * 1000; /** Same declared refresh bound as the S1 trajectory surface (seconds). */ export { DEFAULT_REFRESH_BOUNDARY_SECONDS }; /** Injected clock + scheduler for the production board lifecycle. */ export type FactoryBoardClock = TrajectoryClock; export type FactoryBoardScheduler = TrajectoryScheduler; export type FactoryBoardPageHandle = { readonly outputPath: string; /** Settles when the first page write finishes (or rejects on first failure). */ readonly started: Promise<{ outputPath: string; html: string }>; /** * Settles when the lifecycle ends. * Resolves on a clean stop with no regeneration fault; rejects with the * original cause when a post-start regeneration fails (or the initial write fails). */ readonly closed: Promise; /** * Stop further regeneration. In-flight write is awaited. * Re-throws the original regeneration failure when the lifecycle faulted. */ stop: () => Promise; }; /** Latest-run state bands (blocked is a badge, never a state; escalate is an awaiting overlay). */ export type TicketCurrentState = | "closed" | "pending" | "unaccepted-flying" | "unaccepted-watch" | "unaccepted-suspect" | "accepted-awaiting" | "escalate-awaiting"; /** * Yamen-column placement (#162 totality table, priority top-down): * retained closed → done; open zero-run → pending; latest unknown → unknown set; * latest judge → court (no identified non-judge run in full history) or marshal; * latest coder → coder; fixer/reviewer → marshal; collector → collector; * any other known station → non-resident `other:` column. * "unknown" never forms a column (badge + expand instead). */ export type BoardPlacement = | "pending" | "court" | "coder" | "marshal" | "collector" | "done" | "unknown" | `other:${string}`; export type FactoryBoardBook = { bookKey: string; ledgerDir: string; }; export type FactoryBoardError = { kind: "binding" | "api"; message: string; bookKey?: string; }; export type FactoryBoardView = | { ok: true; snapshot: BoardSnapshot } | { ok: false; error: FactoryBoardError }; type PreparedTicket = { ticket: SnapshotTicket; runs: TicketTrajectoryRun[]; /** Display runs with wallMs applying the latest-unaccepted→now rule. */ displayRuns: TicketTrajectoryRun[]; pending: boolean; currentState: TicketCurrentState; /** Incomplete blockers only (open blockers), for non-closed tickets. */ activeBlockedBy: number[]; costUsd: number; totalTokens: number; /** Cumulative construction wall (runs + axis legs). */ wallMs: number; /** First run start → now (open) or issue closedAt (closed); never last ledger record. */ landingCycleMs: number; /** Present only for unaccepted latest-run states. */ legAgeMs?: number; lastActivityAt?: string; lastActivityMtimeMs?: number; }; function formatUsd(value: number): string { return Number.isFinite(value) ? String(value) : "0"; } function wallMsBetween(startedAt: string | undefined, endedAtIso: string | undefined): number { if (!startedAt || !endedAtIso) return 0; const start = Date.parse(startedAt); const end = Date.parse(endedAtIso); if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) return 0; return end - start; } function sortRunsByStart(runs: readonly TicketTrajectoryRun[]): TicketTrajectoryRun[] { return [...runs].sort((a, b) => { const at = a.startedAt ?? ""; const bt = b.startedAt ?? ""; if (at !== bt) return at.localeCompare(bt); return a.runId.localeCompare(b.runId); }); } function unacceptedBand(mtimeMs: number, nowMs: number): TicketCurrentState { const age = Math.max(0, nowMs - mtimeMs); // Authority: <2min flying / 2–15min watch / >15min suspect (exact 15 stays watch). if (age < UNACCEPTED_FLYING_MS) return "unaccepted-flying"; if (age <= UNACCEPTED_WATCH_MS) return "unaccepted-watch"; return "unaccepted-suspect"; } /** * Current state is decided only by the latest run (session-start order). * Historical unaccepted runs never flip the state; blocked is not a state. */ export function decideTicketCurrentState(input: { ticketState: TicketIssueState; runs: readonly TicketTrajectoryRun[]; now: Date; }): TicketCurrentState { if (input.ticketState === "closed") return "closed"; if (input.runs.length === 0) return "pending"; const latest = sortRunsByStart(input.runs).at(-1)!; if (!latest.hasResult) return unacceptedBand(latest.mtimeMs, input.now.getTime()); // Escalate is an awaiting overlay: same placement and sort band, distinct state. if ((latest.resultStatus) === "escalate") return "escalate-awaiting"; return "accepted-awaiting"; } /** Yamen-column placement of one prepared ticket (mechanical, see BoardPlacement). */ function placeTicket(prepared: PreparedTicket): BoardPlacement { if (prepared.ticket.state === "closed") return "done"; if (prepared.runs.length === 0) return "pending"; const sorted = sortRunsByStart(prepared.runs); const latest = sorted.at(-1)!; if (latest.station === "unknown") return "unknown"; if (latest.station === "judge") { // Unknown history never counts as construction evidence. const started = sorted.some((run) => run.station !== "unknown" && run.station !== "judge"); return started ? "marshal" : "court"; } if (latest.station === "coder") return "coder"; if (latest.station === "fixer" || latest.station === "reviewer") return "marshal"; // ADR 0053: marshal-driven runs land in 尚书省 once the seat ships (station maps now). if (latest.station === "marshal") return "marshal"; if (latest.station === "collector") return "collector"; return `other:${latest.station}`; } /** Column-internal sort band: 在飞 → 观察 → 疑挂 → 已交卷(escalate 同档) → 待发 → 已完成. */ function stateSortBand(state: TicketCurrentState): number { switch (state) { case "unaccepted-flying": return 0; case "unaccepted-watch": return 1; case "unaccepted-suspect": return 2; case "accepted-awaiting": case "escalate-awaiting": return 3; case "pending": return 4; case "closed": return 5; } } /** Last-activity sort key (desc): content activity first, then mtime; closed uses closedAt. */ function ticketSortActivityMs(prepared: PreparedTicket): number { if (prepared.ticket.state === "closed") { const parsed = prepared.ticket.closedAt ? Date.parse(prepared.ticket.closedAt) : NaN; return Number.isFinite(parsed) ? parsed : 0; } const latest = sortRunsByStart(prepared.runs).at(-1); if (!latest) return 0; const activity = latest.lastActivityAt ?? latest.endedAt; const parsed = activity ? Date.parse(activity) : NaN; return Number.isFinite(parsed) ? parsed : latest.mtimeMs; } function enrichRunsForBoard( runs: readonly TicketTrajectoryRun[], currentState: TicketCurrentState, now: Date, ): TicketTrajectoryRun[] { const sorted = sortRunsByStart(runs); const latest = sorted.at(-1); const extendLatest = latest !== undefined && !latest.hasResult && (currentState === "unaccepted-flying" || currentState === "unaccepted-watch" || currentState === "unaccepted-suspect"); const nowIso = now.toISOString(); return sorted.map((run) => { const isLatestUnaccepted = extendLatest && run.runId === latest!.runId; const endIso = isLatestUnaccepted ? nowIso : run.endedAt; const wallMs = wallMsBetween(run.startedAt, endIso); return { ...run, wallMs }; }); } function aggregateTicketMetrics( displayRuns: readonly TicketTrajectoryRun[], ticket: Pick, now: Date, ): { costUsd: number; totalTokens: number; wallMs: number; landingCycleMs: number } { let costUsd = 0; let totalTokens = 0; let wallMs = 0; let firstStart: string | undefined; for (const run of displayRuns) { costUsd += run.costUsd; totalTokens += run.totalTokens; wallMs += (run.wallMs ?? wallMsBetween(run.startedAt, run.endedAt)) + run.axisWallMs; if (run.startedAt && (firstStart === undefined || run.startedAt < firstStart)) { firstStart = run.startedAt; } } let landingCycleMs = 0; if (firstStart) { if (ticket.state === "closed") { // Authority (#136): 落地周期 = 首 run 起点至 now/关票 — closure, not last ledger record. if (!ticket.closedAt) { throw new Error("closed ticket landing cycle requires closedAt"); } landingCycleMs = wallMsBetween(firstStart, ticket.closedAt); } else { landingCycleMs = wallMsBetween(firstStart, now.toISOString()); } } return { costUsd, totalTokens, wallMs, landingCycleMs }; } function currentStateLabel(state: TicketCurrentState): string { switch (state) { case "closed": return "已完成"; case "pending": return "待发"; case "unaccepted-flying": return "在飞"; case "unaccepted-watch": return "观察"; case "unaccepted-suspect": return "疑挂"; case "accepted-awaiting": return "已交卷待派"; case "escalate-awaiting": return "escalate 待裁"; } } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } function isMissingPathError(error: unknown): boolean { return error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR"); } function escapeHtml(text: string): string { return text .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } function attr(value: string): string { return escapeHtml(value); } function isPathInside(parent: string, child: string): boolean { const rel = relative(parent, child); return rel === "" || (!rel.startsWith(`..${sep}`) && rel !== ".." && !rel.startsWith("..")); } function activeBlockedBy(ticket: SnapshotTicket): number[] { if (ticket.state === "closed") return []; return ticket.blockedBy .filter((edge) => edge.state === "open") .map((edge) => edge.issueNumber) .sort((a, b) => a - b); } /** First duplicated bookKey in order, or null when all unique. */ function firstDuplicateBookKey(keys: readonly string[]): string | null { const seen = new Set(); for (const key of keys) { if (seen.has(key)) return key; seen.add(key); } return null; } function renderErrorHtml(error: FactoryBoardError, generatedAt: string): string { const bookAttr = error.bookKey !== undefined && error.bookKey !== "" ? ` data-error-book="${attr(error.bookKey)}"` : ""; return ` Factory board error

工厂进度板无法加载

错误类型:${escapeHtml(error.kind)}

${ error.bookKey ? `

册:${escapeHtml(error.bookKey)}

` : "" }
${escapeHtml(error.message)}

generated-at

`; } /** Yamen display label per station (color = yamen, dot = state). */ const YAMEN_LABELS: Readonly> = { judge: "大理寺", coder: "将作监", fixer: "修内司", reviewer: "御史台", collector: "通进司", doctor: "太医署", merger: "校书郎", marshal: "尚书省", }; function latestKnownStation(runs: readonly TicketTrajectoryRun[]): string | undefined { const latest = sortRunsByStart(runs).at(-1); if (!latest || latest.station === "unknown") return undefined; return latest.station; } /** * Unbound activation card for the existing unknown-set presentation seam. * Not a ticket: no issue number, never placed in a yamen column. */ function renderUnboundRunCard(input: { bookKey: string; unbound: UnboundTrajectoryRun; }): string { const { run, role, observedAt } = input.unbound; const correlationId = input.unbound.correlationId; const stationLabel = run.station === "unknown" ? "未知站" : (YAMEN_LABELS[run.station] ?? run.station); const corrAttr = correlationId === undefined ? "" : ` data-correlation="${attr(correlationId)}"`; const corrMeta = correlationId === undefined ? "" : ` · corr ${escapeHtml(correlationId)}`; return [ `
`, `
`, `未绑定 · ${escapeHtml(run.runId)}`, `${escapeHtml(stationLabel)}`, `
`, `

`, `role ${escapeHtml(role)}${corrMeta} · ${escapeHtml(observedAt)}`, `

`, `
`, ].join(""); } function renderTicketArticle(input: { bookKey: string; prepared: PreparedTicket; nested: boolean; /** Column placement — carried by standalone cards and cluster members. */ placement?: BoardPlacement; /** Family root issue when the ticket belongs to an in-snapshot family (族徽章). */ familyRoot?: number; }): string { const { ticket, runs, displayRuns, pending, currentState, activeBlockedBy: blockers, costUsd, totalTokens, wallMs, landingCycleMs, legAgeMs, lastActivityAt, lastActivityMtimeMs, } = input.prepared; const state: TicketIssueState = ticket.state; const milestone = ticket.milestone ?? ""; const blockedAttr = blockers.join(" "); const badges = blockers .map( (n) => `等 #${escapeHtml(String(n))}`, ) .join(" "); const trajectory = displayRuns.length > 0 ? `
${renderTicketTrajectoryStationHtml(displayRuns)}
` : pending ? `

待发(零卷)

` : `

no runs

`; const parentAttr = ticket.parentIssueNumber !== null ? ` data-parent-issue="${attr(String(ticket.parentIssueNumber))}"` : ""; const placementAttr = input.placement !== undefined ? ` data-placement="${attr(input.placement)}"` : ""; const latestStation = latestKnownStation(runs); const yamenTag = latestStation !== undefined ? `${escapeHtml(YAMEN_LABELS[latestStation] ?? latestStation)}` : ""; const familyBadge = input.familyRoot !== undefined ? `族 #${escapeHtml(String(input.familyRoot))}` : ""; const breadcrumb = runs.length > 0 ? renderBreadcrumbHtml(runs) : ""; const ticketNo = String(ticket.issueNumber); const unaccepted = currentState === "unaccepted-flying" || currentState === "unaccepted-watch" || currentState === "unaccepted-suspect"; // Visible fact labels carry their own machine keys (data-*-label). Article-level // data-leg-age-ms / data-last-activity-* are projection attributes only — tests that // bite only the article cannot prove the human-visible spans exist (AC2 false-green). const activityBits = unaccepted && legAgeMs !== undefined ? [ `腿龄 ${escapeHtml(formatDurationZh(legAgeMs))}`, lastActivityAt ? `末次活动 ${escapeHtml(formatLocalDateTime(lastActivityAt))}` : lastActivityMtimeMs !== undefined ? `末次活动 ${escapeHtml(formatLocalDateTime(new Date(lastActivityMtimeMs).toISOString()))}` : "", ].join("") : ""; return [ `
`, `
`, `

#${escapeHtml(ticketNo)} · ${escapeHtml(ticket.title)}

`, breadcrumb, `

`, ` ${escapeHtml(currentStateLabel(currentState))}`, yamenTag, familyBadge, milestone ? `milestone: ${escapeHtml(milestone)}` : `milestone: —`, `$${escapeHtml(formatUsdPrecise(costUsd))} · ${escapeHtml(formatTokensCompact(totalTokens))} tok`, `施工墙钟 ${escapeHtml(formatDurationZh(wallMs))}`, `落地周期 ${escapeHtml(formatDurationZh(landingCycleMs))}`, activityBits, badges, `

`, `
`, runs.length > 0 ? `
明细 · ${runs.length} 腿${trajectory}
` : trajectory, `
`, ].join(""); } function renderFamily(input: { bookKey: string; parent: PreparedTicket; /** Whole in-snapshot descendant set (not only direct children). */ descendants: PreparedTicket[]; placement: BoardPlacement; /** * closed root → children nest inside the family section (父卡置顶、子卡缩进); * open root → children place independently; the section carries an index only. */ nestChildren: boolean; }): string { const childCount = input.descendants.length; const pendingCount = input.descendants.filter((c) => c.pending).length; const closedCount = input.descendants.filter((c) => c.ticket.state === "closed").length; // Family lane-entry burn = parent + every in-snapshot descendant (retains S2 nest; // child burns such as #130 participate in the sortable family key). const costUsd = input.parent.costUsd + input.descendants.reduce((sum, d) => sum + d.costUsd, 0); const totalTokens = input.parent.totalTokens + input.descendants.reduce((sum, d) => sum + d.totalTokens, 0); const parentBlock = renderTicketArticle({ bookKey: input.bookKey, prepared: input.parent, nested: false, }); const childrenBlock = input.nestChildren ? input.descendants .map((child) => renderTicketArticle({ bookKey: input.bookKey, prepared: child, nested: true, placement: "done", familyRoot: input.parent.ticket.issueNumber, }), ) .join("\n") : input.descendants .map( (child) => `
  • #${escapeHtml(String(child.ticket.issueNumber))} · ${escapeHtml(child.ticket.title)}
  • `, ) .join("\n"); return [ `
    `, `
    `, `

    族 #${escapeHtml(String(input.parent.ticket.issueNumber))} · ${escapeHtml(input.parent.ticket.title)}

    `, `

    `, `子轨迹 ${childCount}`, `待发 ${pendingCount}`, `收官 ${closedCount}`, `$${escapeHtml(formatUsdPrecise(costUsd))} · ${escapeHtml(formatTokensCompact(totalTokens))} tok`, `

    `, `
    `, `
    ${parentBlock}
    `, `
    `, `子轨迹(${childCount})`, input.nestChildren ? childrenBlock : `
      ${childrenBlock}
    `, `
    `, `
    `, ].join(""); } /** 已完成列 family cluster for closed members of an open-root family (族簇嵌套). */ function renderFamilyCluster(input: { bookKey: string; rootIssueNumber: number; members: PreparedTicket[]; }): string { const costUsd = input.members.reduce((sum, m) => sum + m.costUsd, 0); const memberHtml = input.members .map((member) => renderTicketArticle({ bookKey: input.bookKey, prepared: member, nested: true, placement: "done", familyRoot: input.rootIssueNumber, }), ) .join("\n"); return [ `
    `, `

    族 #${escapeHtml(String(input.rootIssueNumber))} · 已交卷簇(${input.members.length})

    `, memberHtml, `
    `, ].join(""); } /** Direct in-snapshot children keyed by parent issue number. */ function buildDirectChildrenByParent( prepared: ReadonlyMap, ): Map { const childrenByParent = new Map(); for (const item of prepared.values()) { const parent = item.ticket.parentIssueNumber; if (parent === null) continue; if (!prepared.has(parent)) continue; // parent not in snapshot — child stays top-level const list = childrenByParent.get(parent) ?? []; list.push(item); childrenByParent.set(parent, list); } for (const list of childrenByParent.values()) { list.sort((a, b) => a.ticket.issueNumber - b.ticket.issueNumber); } return childrenByParent; } /** * Walk native parent edges from a root and collect every in-snapshot descendant * once (BFS). Intermediate parents are members of the root family, not new roots. */ function collectDescendants( rootIssueNumber: number, childrenByParent: ReadonlyMap, ): PreparedTicket[] { const out: PreparedTicket[] = []; const seen = new Set(); const queue = [...(childrenByParent.get(rootIssueNumber) ?? [])]; while (queue.length > 0) { const item = queue.shift()!; const n = item.ticket.issueNumber; if (seen.has(n)) continue; seen.add(n); out.push(item); const kids = childrenByParent.get(n); if (kids) queue.push(...kids); } out.sort((a, b) => a.ticket.issueNumber - b.ticket.issueNumber); return out; } async function prepareTicket( ledgerDir: string, ticket: SnapshotTicket, now: Date, bookIndex?: TicketTrajectoryBookIndex, ): Promise { const runs = await loadTicketTrajectoryRuns(ledgerDir, ticket.issueNumber, bookIndex); const pending = ticket.state !== "closed" && runs.length === 0; const currentState = decideTicketCurrentState({ ticketState: ticket.state, runs, now, }); const displayRuns = enrichRunsForBoard(runs, currentState, now); const metrics = aggregateTicketMetrics(displayRuns, ticket, now); let legAgeMs: number | undefined; let lastActivityAt: string | undefined; let lastActivityMtimeMs: number | undefined; if ( currentState === "unaccepted-flying" || currentState === "unaccepted-watch" || currentState === "unaccepted-suspect" ) { const latest = sortRunsByStart(runs).at(-1)!; lastActivityMtimeMs = latest.mtimeMs; // Newest parent/axis *content* activity (mtime stays on data-last-activity-mtime-ms). lastActivityAt = latest.lastActivityAt ?? latest.endedAt ?? (latest.mtimeMs > 0 ? new Date(latest.mtimeMs).toISOString() : undefined); if (latest.startedAt) { legAgeMs = wallMsBetween(latest.startedAt, now.toISOString()); } else if (latest.mtimeMs > 0) { legAgeMs = Math.max(0, now.getTime() - latest.mtimeMs); } else { legAgeMs = 0; } } return { ticket, runs, displayRuns, pending, currentState, activeBlockedBy: activeBlockedBy(ticket), costUsd: metrics.costUsd, totalTokens: metrics.totalTokens, wallMs: metrics.wallMs, landingCycleMs: metrics.landingCycleMs, ...(legAgeMs !== undefined ? { legAgeMs } : {}), ...(lastActivityAt !== undefined ? { lastActivityAt } : {}), ...(lastActivityMtimeMs !== undefined ? { lastActivityMtimeMs } : {}), }; } type LaneEntry = { placement: BoardPlacement; band: number; activityMs: number; /** Entry identity for sort tiebreak (family root / cluster root / ticket number). */ issueNumber: number; html: string; }; function entryCompare(a: LaneEntry, b: LaneEntry): number { if (a.band !== b.band) return a.band - b.band; if (a.activityMs !== b.activityMs) return b.activityMs - a.activityMs; return a.issueNumber - b.issueNumber; } /** * Six resident columns always render (empty ones carry data-column-count=0). * Non-resident `other:*` columns slot before 已完成 when occupied. */ const RESIDENT_COLUMN_ORDER: readonly string[] = [ "pending", "court", "coder", "marshal", "collector", "done", ]; const COLUMN_LABELS: Readonly> = { pending: "待发", court: "大理寺 · 审票", coder: "将作监", marshal: "尚书省", collector: "通进司", done: "已完成", }; /** Terminal result statuses that mark a breadcrumb station step as 被拒 (strikethrough). */ const REJECTED_RESULT_STATUSES: ReadonlySet = new Set(["continue", "refused", "rejected"]); type BreadcrumbStep = { station: string; count: number; isReturn: boolean; isRejected: boolean; }; /** * Compact ledger-order breadcrumb: consecutive same-station collapse (×N), * ↩ when a station reappears after another, 被拒 when any run in the step was rejected. */ function buildBreadcrumbSteps(runs: readonly TicketTrajectoryRun[]): BreadcrumbStep[] { const sorted = sortRunsByStart(runs); if (sorted.length === 0) return []; type Group = { station: string; runs: TicketTrajectoryRun[] }; const groups: Group[] = []; for (const run of sorted) { const last = groups.at(-1); if (last && last.station === run.station) { last.runs.push(run); } else { groups.push({ station: run.station, runs: [run] }); } } const seen = new Set(); const steps: BreadcrumbStep[] = []; for (const group of groups) { const isReturn = seen.has(group.station); seen.add(group.station); const isRejected = group.runs.some( (run) => run.hasResult && REJECTED_RESULT_STATUSES.has((run.resultStatus)), ); steps.push({ station: group.station, count: group.runs.length, isReturn, isRejected, }); } return steps; } function renderBreadcrumbHtml(runs: readonly TicketTrajectoryRun[]): string { const steps = buildBreadcrumbSteps(runs); if (steps.length === 0) return ""; const parts = steps.map((step, index) => { const label = step.station === "unknown" ? "未知站" : (YAMEN_LABELS[step.station] ?? step.station); return [ ``, step.isReturn ? `` : "", `${escapeHtml(label)}`, step.count > 1 ? `×${escapeHtml(String(step.count))}` : "", ``, ].join(""); }); return [ ``, ].join(""); } function columnLabel(key: string): string { if (key.startsWith("other:")) return `${key.slice("other:".length)}(非常驻)`; return COLUMN_LABELS[key] ?? key; } type RenderedLane = { html: string; /** Unknown-station tickets in this lane (drives the page-level badge). */ unknownTickets: Array<{ issueNumber: number; title: string }>; }; async function renderLaneHtml( bookKey: string, ledgerDir: string, tickets: readonly SnapshotTicket[], now: Date, options?: { hidden?: boolean }, ): Promise { // One book-level flat-run index shared by every ticket in this lane. const bookIndex = await buildTicketTrajectoryBookIndex(ledgerDir); const prepared = new Map(); for (const ticket of tickets) { prepared.set(ticket.issueNumber, await prepareTicket(ledgerDir, ticket, now, bookIndex)); } const childrenByParent = buildDirectChildrenByParent(prepared); // Tickets that appear as a child of any in-snapshot parent. const nestedInSnapshot = new Set(); for (const list of childrenByParent.values()) { for (const child of list) nestedInSnapshot.add(child.ticket.issueNumber); } // Family roots = in-snapshot tickets with descendants that are not themselves // nested under another in-snapshot parent. One rooted whole-family aggregate. const familyRoots = [...childrenByParent.keys()] .filter((parentNum) => !nestedInSnapshot.has(parentNum)) .sort((a, b) => a - b); const entries: LaneEntry[] = []; const familyMembers = new Set(); /** Closed members of open-root families → 已完成列 clusters keyed by family root. */ const clusterMembersByRoot = new Map(); for (const rootNum of familyRoots) { const parent = prepared.get(rootNum)!; const descendants = collectDescendants(rootNum, childrenByParent); familyMembers.add(rootNum); for (const d of descendants) familyMembers.add(d.ticket.issueNumber); if (parent.ticket.state === "closed") { // Closed root: the whole family travels into 已完成 (父卡置顶、子卡缩进嵌套). entries.push({ placement: "done", band: stateSortBand("closed"), activityMs: ticketSortActivityMs(parent), issueNumber: rootNum, html: renderFamily({ bookKey, parent, descendants, placement: "done", nestChildren: true }), }); continue; } // Open root: family section placed by the root's own placement; members place // independently (open) or extract to the 已完成 cluster (closed). const rootPlacement = placeTicket(parent); entries.push({ placement: rootPlacement, band: stateSortBand(parent.currentState), activityMs: ticketSortActivityMs(parent), issueNumber: rootNum, html: renderFamily({ bookKey, parent, descendants, placement: rootPlacement, nestChildren: false }), }); for (const member of descendants) { if (member.ticket.state === "closed") { const list = clusterMembersByRoot.get(rootNum) ?? []; list.push(member); clusterMembersByRoot.set(rootNum, list); continue; } const placement = placeTicket(member); entries.push({ placement, band: stateSortBand(member.currentState), activityMs: ticketSortActivityMs(member), issueNumber: member.ticket.issueNumber, html: renderTicketArticle({ bookKey, prepared: member, nested: false, placement, familyRoot: rootNum, }), }); } } // Standalone tickets (no in-snapshot family): each places by its own facts. for (const item of prepared.values()) { if (familyMembers.has(item.ticket.issueNumber)) continue; const placement = placeTicket(item); entries.push({ placement, band: stateSortBand(item.currentState), activityMs: ticketSortActivityMs(item), issueNumber: item.ticket.issueNumber, html: renderTicketArticle({ bookKey, prepared: item, nested: false, placement }), }); } // 已完成列 clusters for closed members of open-root families. for (const [rootNum, members] of [...clusterMembersByRoot.entries()].sort((a, b) => a[0] - b[0])) { const sortedMembers = [...members].sort((a, b) => a.ticket.issueNumber - b.ticket.issueNumber); entries.push({ placement: "done", band: stateSortBand("closed"), activityMs: Math.max(...sortedMembers.map((m) => ticketSortActivityMs(m))), issueNumber: rootNum, html: renderFamilyCluster({ bookKey, rootIssueNumber: rootNum, members: sortedMembers }), }); } // Group entries into columns; unknown never forms a column. const byPlacement = new Map(); const unknownEntries: LaneEntry[] = []; const unknownTickets: Array<{ issueNumber: number; title: string }> = []; for (const entry of entries) { if (entry.placement === "unknown") { unknownEntries.push(entry); unknownTickets.push({ issueNumber: entry.issueNumber, title: prepared.get(entry.issueNumber)?.ticket.title ?? "", }); continue; } const list = byPlacement.get(entry.placement) ?? []; list.push(entry); byPlacement.set(entry.placement, list); } // Unbound flat runs: isolated from every ticket, honest on the unknown seam. const unboundRuns = await loadUnboundTrajectoryRuns(ledgerDir, bookIndex); for (const unbound of unboundRuns) { unknownEntries.push({ placement: "unknown", band: 0, activityMs: unbound.run.mtimeMs, // Sort after real tickets; no issue identity (unbound is not a ticket). issueNumber: Number.MAX_SAFE_INTEGER, html: renderUnboundRunCard({ bookKey, unbound }), }); } unknownEntries.sort(entryCompare); unknownTickets.sort((a, b) => a.issueNumber - b.issueNumber); // Six resident columns always present (empty → count 0); non-resident only when occupied. const otherColumns = [...byPlacement.keys()] .filter((key) => key.startsWith("other:")) .sort((a, b) => a.localeCompare(b)); const columnOrder = [ ...RESIDENT_COLUMN_ORDER.slice(0, -1), // pending…collector ...otherColumns, "done", ]; const columnHtml = columnOrder .map((key) => { const group = (byPlacement.get(key) ?? []).slice().sort(entryCompare); return [ `
    `, `

    ${escapeHtml(columnLabel(key))} ${group.length}

    `, group.map((entry) => entry.html).join("\n"), `
    `, ].join(""); }) .join("\n"); const unknownSetHtml = unknownEntries.length > 0 ? [ `
    `, `未知票 ×${unknownEntries.length}(本册,点击展开)`, unknownEntries.map((entry) => entry.html).join("\n"), `
    `, ].join("") : ""; return { html: [ ``, ].join("\n"), unknownTickets, }; } function boardStyles(): string { return ` :root { color-scheme: light dark; font-family: system-ui, sans-serif; line-height: 1.45; } body { margin: 0; padding: 1rem; } header.page { margin-bottom: 1rem; max-width: 72rem; } .generated { font-size: 0.9rem; opacity: 0.8; } .thresholds { font-size: 0.85rem; opacity: 0.9; display: flex; flex-wrap: wrap; gap: 0.5rem 1rem; } .controls { margin: 0.5rem 0 0; display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: center; font-size: 0.9rem; } .lane { border: 1px solid color-mix(in srgb, CanvasText 22%, Canvas); border-radius: 0.6rem; padding: 0.85rem; margin: 1rem 0; } .lane-title { margin: 0 0 0.75rem; font-size: 1.2rem; } .lane-columns { display: flex; flex-wrap: nowrap; overflow-x: auto; gap: 0.75rem; align-items: flex-start; } .column { flex: 1 0 14rem; min-width: 13rem; border: 1px solid color-mix(in srgb, CanvasText 14%, Canvas); border-radius: 0.5rem; padding: 0.5rem 0.6rem; } .column-title { margin: 0 0 0.4rem; font-size: 0.95rem; } .column-count { opacity: 0.75; font-weight: 400; } .unknown-set { flex: 1 0 14rem; min-width: 13rem; } .family-cluster { margin: 0.4rem 0; } .family-cluster-title { margin: 0.2rem 0; font-size: 0.85rem; opacity: 0.85; } .family-child-index-list { margin: 0.25rem 0; padding-left: 1.2rem; font-size: 0.88rem; } .ticket-meta .state-label, .ticket-meta .milestone, .ticket-meta .cost, .ticket-meta .wall, .ticket-meta .landing, .ticket-meta .leg-age, .ticket-meta .last-activity { display: none; } .family-agg .cost { display: none; } .state-dot { font-size: 0.8em; } .yamen-tag, .family-badge { display: inline-block; padding: 0.02rem 0.4rem; border-radius: 0.3rem; font-size: 0.8rem; background: color-mix(in srgb, CanvasText 10%, Canvas); } .yamen-coder { background: color-mix(in srgb, teal 30%, Canvas); } .yamen-fixer { background: color-mix(in srgb, blueviolet 28%, Canvas); } .yamen-reviewer { background: color-mix(in srgb, deepskyblue 30%, Canvas); } .yamen-judge { background: color-mix(in srgb, palevioletred 30%, Canvas); } .unknown-badge { margin-left: auto; } .unknown-item { display: list-item; } .family { border: 1px dashed color-mix(in srgb, CanvasText 28%, Canvas); border-radius: 0.5rem; padding: 0.65rem; margin: 0.75rem 0; } .family-title { margin: 0 0 0.35rem; font-size: 1.05rem; } .family-agg { margin: 0 0 0.5rem; display: flex; flex-wrap: wrap; gap: 0.75rem; font-size: 0.9rem; opacity: 0.9; } .ticket { border-top: 1px solid color-mix(in srgb, CanvasText 12%, Canvas); padding: 0.55rem 0; } /* Five-state top strip: same hue family as the state dot (card stays put, strip changes). */ .ticket[data-state-strip="unaccepted-flying"] { border-top: 3px solid color-mix(in srgb, seagreen 80%, CanvasText); } .ticket[data-state-strip="unaccepted-watch"] { border-top: 3px solid color-mix(in srgb, goldenrod 85%, CanvasText); } .ticket[data-state-strip="unaccepted-suspect"] { border-top: 3px solid color-mix(in srgb, tomato 85%, CanvasText); } .ticket[data-state-strip="accepted-awaiting"] { border-top: 3px solid color-mix(in srgb, dodgerblue 75%, CanvasText); } .ticket[data-state-strip="escalate-awaiting"] { border-top: 3px solid color-mix(in srgb, darkorange 90%, CanvasText); } .ticket-child { margin-left: 0.75rem; padding-left: 0.5rem; border-left: 2px solid color-mix(in srgb, CanvasText 18%, Canvas); } .ticket-title { margin: 0; font-size: 1rem; } .breadcrumb { margin: 0.2rem 0 0.35rem; font-size: 0.82rem; display: flex; flex-wrap: wrap; gap: 0.2rem 0.35rem; align-items: center; opacity: 0.92; } .breadcrumb-sep { opacity: 0.55; } .breadcrumb-step[data-rejected="true"] { text-decoration: line-through; opacity: 0.75; } .ticket-meta { margin: 0.25rem 0; display: flex; flex-wrap: wrap; gap: 0.5rem 0.85rem; font-size: 0.88rem; } .state { font-weight: 600; } .ticket.current-unaccepted-flying .state { color: color-mix(in srgb, seagreen 80%, CanvasText); } .ticket.current-unaccepted-watch .state { color: color-mix(in srgb, goldenrod 85%, CanvasText); } .ticket.current-unaccepted-suspect .state { color: color-mix(in srgb, tomato 85%, CanvasText); } .ticket.current-pending .state { opacity: 0.85; } .ticket.current-accepted-awaiting .state { color: color-mix(in srgb, dodgerblue 75%, CanvasText); } .ticket.current-escalate-awaiting .state { color: color-mix(in srgb, darkorange 90%, CanvasText); } .ticket.current-closed .state { opacity: 0.75; } .blocked-badge { display: inline-block; padding: 0.05rem 0.4rem; border-radius: 999px; background: color-mix(in srgb, darkorange 30%, Canvas); font-size: 0.8rem; } .pending-label { margin: 0.25rem 0; opacity: 0.85; } .station { border: 1px solid color-mix(in srgb, CanvasText 16%, Canvas); border-radius: 0.4rem; padding: 0.5rem; margin: 0.4rem 0; } .station-title { margin: 0 0 0.35rem; font-size: 0.95rem; } .run { padding: 0.35rem 0; border-top: 1px solid color-mix(in srgb, CanvasText 10%, Canvas); } .run:first-of-type { border-top: 0; } .run-head { display: flex; flex-wrap: wrap; gap: 0.5rem 1rem; justify-content: space-between; } .run-id { font-family: ui-monospace, monospace; font-size: 0.85rem; word-break: break-all; } .run-model { font-size: 0.8rem; opacity: 0.85; } .run-meta { margin: 0.2rem 0; font-size: 0.85rem; display: flex; flex-wrap: wrap; gap: 0.65rem; } .ledger { margin: 0.2rem 0 0; font-size: 0.75rem; word-break: break-all; } @media (max-width: 640px) { body { padding: 0.75rem; } .ticket-child { margin-left: 0.35rem; } } `; } function boardPageScript(): string { // Presentation-only reorder + filter; machine facts stay on data-* attrs. // Singular render-seam comparator lives only in this embedded page script. // Family lane entries carry aggregate data-cost-usd (parent + descendants) so nested // per-ticket burns (e.g. #130 under #78) participate without flattening the S2 nest. return ``; } /** * Unique S2 seam: books + snapshot-or-error + now → HTML. * Read-only against every ledger. * When refreshBoundarySeconds is a positive finite number, the page declares that * bound (backed by startFactoryBoardPage regeneration). One-shot omits it. */ export async function renderFactoryBoardHtml( books: readonly FactoryBoardBook[], view: FactoryBoardView, now: Date, options?: { refreshBoundarySeconds?: number }, ): Promise { if (!Array.isArray(books)) { throw new Error("books must be an array"); } if (!isRecord(view) || typeof view.ok !== "boolean") { throw new Error("view must be a FactoryBoardView"); } const generatedAt = now.toISOString(); const refreshActive = options?.refreshBoundarySeconds !== undefined && Number.isFinite(options.refreshBoundarySeconds) && options.refreshBoundarySeconds > 0; const refreshBoundarySeconds = refreshActive ? options!.refreshBoundarySeconds! : undefined; if (!view.ok) { return renderErrorHtml(view.error, generatedAt); } // Fail closed on duplicate bookKey — never Map-last-wins across lanes/ledgers. const duplicateKey = firstDuplicateBookKey(books.map((b) => b.bookKey)); if (duplicateKey !== null) { return renderErrorHtml( { kind: "binding", bookKey: duplicateKey, message: `duplicate bookKey in books: ${duplicateKey}`, }, generatedAt, ); } const duplicateSnapKey = firstDuplicateBookKey(view.snapshot.books.map((b) => b.bookKey)); if (duplicateSnapKey !== null) { return renderErrorHtml( { kind: "binding", bookKey: duplicateSnapKey, message: `duplicate bookKey in snapshot: ${duplicateSnapKey}`, }, generatedAt, ); } const bookByKey = new Map(books.map((b) => [b.bookKey, b])); // Project dropdown default: the home book when present, else the first snapshot book. const defaultBookKey = (view.snapshot.books.find((b) => b.repo === "ak-pi-workflow-roles") ?? view.snapshot.books[0]) ?.bookKey; const laneHtmlParts: string[] = []; const unknownByBook = new Map>(); for (const bookSnap of view.snapshot.books) { const book = bookByKey.get(bookSnap.bookKey); if (!book) { return renderErrorHtml( { kind: "binding", bookKey: bookSnap.bookKey, message: `no ledger binding for book ${bookSnap.bookKey}`, }, generatedAt, ); } let lane: RenderedLane; lane = await renderLaneHtml( book.bookKey, resolve(book.ledgerDir), bookSnap.tickets, now, { hidden: defaultBookKey !== undefined && bookSnap.bookKey !== defaultBookKey }, ); laneHtmlParts.push(lane.html); unknownByBook.set(bookSnap.bookKey, lane.unknownTickets); } // Family dropdown (default book): parents with at least one open direct child + mechanical child count. const defaultBookSnap = view.snapshot.books.find((b) => b.bookKey === defaultBookKey); const familyOptions: Array<{ issueNumber: number; title: string; childCount: number }> = []; if (defaultBookSnap) { const byNumber = new Map(defaultBookSnap.tickets.map((t) => [t.issueNumber, t])); const openChildCountByParent = new Map(); for (const t of defaultBookSnap.tickets) { if (t.state !== "open" || t.parentIssueNumber === null) continue; if (!byNumber.has(t.parentIssueNumber)) continue; openChildCountByParent.set( t.parentIssueNumber, (openChildCountByParent.get(t.parentIssueNumber) ?? 0) + 1, ); } for (const n of [...openChildCountByParent.keys()].sort((a, b) => a - b)) { familyOptions.push({ issueNumber: n, title: byNumber.get(n)?.title ?? "", childCount: openChildCountByParent.get(n) ?? 0, }); } } const projectOptions = view.snapshot.books .map( (b) => ``, ) .join(""); const familyOptionsHtml = [ ``, ...familyOptions.map( (f) => ``, ), ].join(""); const allUnknown = view.snapshot.books.flatMap((b) => (unknownByBook.get(b.bookKey) ?? []).map((u) => ({ bookKey: b.bookKey, ...u })), ); const defaultUnknownCount = defaultBookKey === undefined ? 0 : (unknownByBook.get(defaultBookKey) ?? []).length; const unknownBadgeHtml = allUnknown.length > 0 ? [ `
    `, `未知票 ×${defaultUnknownCount}`, `
      `, ...allUnknown.map( (u) => `
    • #${escapeHtml(String(u.issueNumber))} · ${escapeHtml(u.title)}(${escapeHtml(u.bookKey)})
    • `, ), `
    `, `
    `, ].join("") : ""; const lifecycleAttrs = refreshActive ? ` data-lifecycle="refresh" data-refresh-boundary-seconds="${attr(String(refreshBoundarySeconds))}"` : ` data-lifecycle="oneshot"`; const refreshMeta = refreshActive ? ` ` : ""; const refreshNote = refreshActive ? `\n · refresh ≤ ${escapeHtml(String(refreshBoundarySeconds))}s` : ""; return ` ${refreshMeta} 工厂进度板

    工厂进度板

    生成于 ${refreshNote}

    未受理阈值: <${escapeHtml(formatDurationZh(UNACCEPTED_FLYING_MS))} 在飞 ${escapeHtml(formatDurationZh(UNACCEPTED_FLYING_MS))}–${escapeHtml(formatDurationZh(UNACCEPTED_WATCH_MS))} 观察 >${escapeHtml(formatDurationZh(UNACCEPTED_WATCH_MS))} 疑挂

    图例:圆点=状态 · 方色=衙门 · 列=票据当前所在衙门机械归位

    ${unknownBadgeHtml}

    ${laneHtmlParts.join("\n") || "

    no books in snapshot

    "}
    ${boardPageScript()} `; } async function assertOutputOutsideAllLedgers( books: readonly FactoryBoardBook[], outputPath: string, ): Promise<{ ledgerRoots: string[]; outputAbsolute: string }> { const outputAbsolute = resolve(outputPath); const ledgerRoots: string[] = []; for (const book of books) { const ledgerResolved = resolve(book.ledgerDir); let ledgerRoot: string; try { ledgerRoot = await realpath(ledgerResolved); } catch (error) { if (!isMissingPathError(error)) throw error; ledgerRoot = ledgerResolved; } ledgerRoots.push(ledgerRoot); const missingTail: string[] = []; let cursor = outputAbsolute; for (;;) { try { await lstat(cursor); break; } catch (error) { if (!isMissingPathError(error)) throw error; const parent = dirname(cursor); if (parent === cursor) break; missingTail.push(basename(cursor)); cursor = parent; } } let realPrefix: string; try { realPrefix = await realpath(cursor); } catch (error) { if (!isMissingPathError(error)) throw error; realPrefix = resolve(cursor); } const prospectiveReal = missingTail.length === 0 ? realPrefix : resolve(realPrefix, ...missingTail.reverse()); if ( isPathInside(ledgerRoot, prospectiveReal) || isPathInside(ledgerRoot, realPrefix) || isPathInside(ledgerRoot, outputAbsolute) ) { throw new Error("factory board outputPath must be outside every ledger directory"); } } return { ledgerRoots, outputAbsolute }; } async function writeHtmlAtomically(outputAbsolute: string, html: string, ledgerRoots: string[]): Promise { const parent = dirname(outputAbsolute); await mkdir(parent, { recursive: true }); const parentReal = await realpath(parent); for (const root of ledgerRoots) { if (isPathInside(root, parentReal) || isPathInside(root, resolve(parentReal, basename(outputAbsolute)))) { throw new Error("factory board outputPath must be outside every ledger directory"); } } const temporary = join(parent, `.factory-board-${randomUUID()}.html.tmp`); try { await writeFile(temporary, html, "utf8"); await rename(temporary, outputAbsolute); } catch (error) { await rm(temporary, { force: true }).catch(() => undefined); throw error; } return realpath(outputAbsolute); } export async function writeFactoryBoardPage(input: { books: readonly FactoryBoardBook[]; view: FactoryBoardView; now: Date; outputPath: string; refreshBoundarySeconds?: number; }): Promise<{ outputPath: string; html: string }> { const gate = await assertOutputOutsideAllLedgers(input.books, input.outputPath); const html = await renderFactoryBoardHtml( input.books, input.view, input.now, input.refreshBoundarySeconds !== undefined ? { refreshBoundarySeconds: input.refreshBoundarySeconds } : undefined, ); const outputPath = await writeHtmlAtomically(gate.outputAbsolute, html, gate.ledgerRoots); return { outputPath, html }; } const defaultBoardScheduler: FactoryBoardScheduler = { every(ms, tick) { const timer = setInterval(tick, ms); timer.unref?.(); return () => clearInterval(timer); }, }; /** * Production board lifecycle: write immediately, then regenerate on the declared * refresh boundary so the same viewing surface observes new runs / generated-at. * Snapshot bindings stay fixed; each tick re-reads ledgers (read-only). * Stop cancels further regeneration. A post-start regeneration failure faults the * lifecycle with the original cause (no silent continuation). */ export function startFactoryBoardPage(input: { books: readonly FactoryBoardBook[]; /** Fixed snapshot view (one-shot-style watch). Omit when loadView is supplied. */ view?: FactoryBoardView; /** * Snapshot loader invoked for every write, including the first (#162: watch does * not pin the startup snapshot — retention candidates refresh each tick). */ loadView?: () => Promise; outputPath: string; refreshBoundarySeconds?: number; clock?: FactoryBoardClock; scheduler?: FactoryBoardScheduler; }): FactoryBoardPageHandle { const refreshBoundarySeconds = input.refreshBoundarySeconds ?? DEFAULT_REFRESH_BOUNDARY_SECONDS; if (!(refreshBoundarySeconds > 0) || !Number.isFinite(refreshBoundarySeconds)) { throw new Error("refreshBoundarySeconds must be a positive finite number"); } if (input.view === undefined && input.loadView === undefined) { throw new Error("view or loadView is required"); } const clock = input.clock ?? (() => new Date()); const scheduler = input.scheduler ?? defaultBoardScheduler; let stopped = false; let cancel: (() => void) | undefined; let inFlight: Promise | undefined; let lastRejection: unknown; let closedSettled = false; let resolveClosed!: () => void; let rejectClosed!: (error: unknown) => void; const closed = new Promise((resolve, reject) => { resolveClosed = resolve; rejectClosed = reject; }); void closed.catch(() => undefined); const fault = (error: unknown): void => { if (lastRejection !== undefined) return; lastRejection = error; stopped = true; cancel?.(); cancel = undefined; if (!closedSettled) { closedSettled = true; rejectClosed(error); } }; const settleClean = (): void => { if (closedSettled) return; closedSettled = true; resolveClosed(); }; const writeOnce = async (): Promise<{ outputPath: string; html: string }> => { if (stopped && lastRejection === undefined) { throw new Error("factory board page lifecycle already stopped"); } if (lastRejection !== undefined) throw lastRejection; const view = input.loadView !== undefined ? await input.loadView() : input.view!; return writeFactoryBoardPage({ books: input.books, view, now: clock(), outputPath: input.outputPath, refreshBoundarySeconds, }); }; const queueWrite = (): void => { if (stopped || lastRejection !== undefined) return; inFlight = (inFlight ?? Promise.resolve()).then(async () => { if (stopped || lastRejection !== undefined) return; try { await writeOnce(); } catch (error) { fault(error); } }); }; const started = writeOnce() .then((first) => { if (stopped || lastRejection !== undefined) return first; const intervalMs = Math.max(1, Math.round(refreshBoundarySeconds * 1000)); cancel = scheduler.every(intervalMs, () => { queueWrite(); }); return first; }) .catch((error) => { fault(error); throw error; }); const outputPath = resolve(input.outputPath); return { outputPath, started, closed, async stop() { stopped = true; cancel?.(); cancel = undefined; await started.catch(() => undefined); if (inFlight) await inFlight.catch(() => undefined); if (lastRejection !== undefined) { throw lastRejection; } settleClean(); }, }; }