Files
pbakaus_impeccable/ui/component-review/model.ts
T
Paul Bakaus 6a93a35293 Add human component review and prepare Impeccable 4.4.0
Carry native comp capture and completion integrity fixes into the reviewed component workflow. Bump the skill to 4.4.0 and engine/platform pins to 0.1.6; keep publication separate from this release candidate.

AI assistance: implemented and validated with OpenAI Codex.
2026-09-13 20:41:09 -07:00

83 lines
5.1 KiB
TypeScript

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<string, Decision>; missing: Missing[]; inventoryConfirmed: boolean };
export type ReviewHistory = {
packet: ReviewPacket; draft: Draft; submitted: boolean;
changes: Record<string, { kind: 'added' | 'changed' | 'unchanged'; files: string[]; reasons: string[]; carried?: boolean }>;
feedback?: Record<string, { round: number; decision: Decision }>;
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',
};
}