export type Box = { x: number; y: number; w: number; h: number }; export type Component = { id: string; revision: string; name: string; medium: string; note: string; box: Box; material?: { format: string; width: number; height: number; alpha: 'transparent' | 'opaque' | 'unknown' }; context?: { kind?: 'image' | 'page'; sourceKind?: 'page'; url: string; layering: string }; thumbnail?: { url: string; box?: Box }; preview: { kind: 'image' | 'page'; sourceKind?: 'page'; url: string; position?: string }; }; export type ReviewPacket = { id: string; revision: string; title: string; round: number; comp: { url: string; width: number; height: number; background?: string }; components: Component[]; }; export type Decision = { revision: string; action: 'approve' | 'revise'; feedback: string; split: boolean }; export type Missing = { id: string; name: string; box: Box; feedback: string }; export type Draft = { packetRevision: string; decisions: Record; missing: Missing[]; inventoryConfirmed: boolean }; export type ReviewHistory = { packet: ReviewPacket; draft: Draft; submitted: boolean; changes: Record; feedback?: Record; removed: { id: string; name: string }[]; }; export function repairStatus(id: string, history?: ReviewHistory | null) { const change = history?.changes[id]; const outstanding = history?.feedback?.[id]; const prior = outstanding?.decision ?? (history?.submitted ? history.draft.decisions[id] : undefined); const carried = change?.kind === 'unchanged' && (change.carried ?? (history?.submitted && prior?.action === 'approve')) === true; return { change, prior, feedbackRound: outstanding?.round ?? history?.packet.round, carried, label: change?.kind === 'added' ? 'New component' : change?.kind === 'changed' ? 'Review again' : carried ? 'Approval kept' : prior?.action === 'revise' ? 'Changes still requested' : 'Awaiting review', }; } export function componentState(component: Component, draft: Draft, history?: ReviewHistory | null) { const saved = draft.decisions[component.id]; const decision = saved?.revision === component.revision ? saved : undefined; const repair = repairStatus(component.id, history); if (decision?.action === 'approve') return { kind: 'approved' as const, label: repair.carried ? 'Approval kept' : 'Approved', priority: 3 }; if (decision?.action === 'revise') return { kind: 'feedback' as const, label: 'Feedback ready', priority: 2 }; return { kind: 'pending' as const, label: repair.change?.kind === 'changed' ? 'Review again' : repair.change?.kind === 'added' ? 'New ยท review needed' : 'Not reviewed', priority: repair.change?.kind === 'changed' || repair.change?.kind === 'added' ? 0 : 1 }; } export function newDraft(packet: ReviewPacket): Draft { return { packetRevision: packet.revision, decisions: {}, missing: [], inventoryConfirmed: false }; } export function validBox(b: Box) { return Object.values(b).every(Number.isFinite) && b.x >= 0 && b.y >= 0 && b.w > 0 && b.h > 0 && b.x + b.w <= 1.00001 && b.y + b.h <= 1.00001; } export function summarize(packet: ReviewPacket, draft: Draft) { const decisions = packet.components.map(c => draft.decisions[c.id]?.revision === c.revision ? draft.decisions[c.id] : undefined); const approved = decisions.filter(d => d?.action === 'approve').length; const revisions = decisions.filter(d => d?.action === 'revise').length; const pending = decisions.length - approved - revisions; const hasFeedback = revisions > 0 || draft.missing.length > 0; return { approved, revisions, pending, hasFeedback, canSubmit: draft.packetRevision === packet.revision && draft.missing.every(m => m.name.trim() && validBox(m.box)) && (hasFeedback || (!pending && draft.inventoryConfirmed)) }; } export function approveRemaining(packet: ReviewPacket, draft: Draft): Draft { const decisions = { ...draft.decisions }; for (const c of packet.components) { if (!decisions[c.id] || decisions[c.id].revision !== c.revision) decisions[c.id] = { revision: c.revision, action: 'approve', feedback: '', split: false }; } return { ...draft, decisions }; } /** UI drafts are not authority. A trusted adapter must verify versions and actor before persisting. */ export function submission(packet: ReviewPacket, draft: Draft) { if (!summarize(packet, draft).canSubmit) throw new Error('Review is incomplete or stale'); return { schemaVersion: 1, requestId: packet.id, ...structuredClone(draft) }; } /** A code capture is an image for display, but is still a code implementation. */ export function componentPresentation(component: Component) { const code = component.preview.kind === 'page' || component.preview.sourceKind === 'page'; const captured = component.preview.sourceKind === 'page'; return { code, captured, label: code ? (component.medium.match(/html|css|svg/i) ? component.medium : 'HTML / CSS / SVG') : 'Raster', caption: code ? (captured ? 'Rendered component' : 'Live component') : 'Produced asset', fileLabel: captured ? 'Open captured preview' : 'Open source image', }; }