);
}
const runDraft = async () => {
if (drafting) return;
setDrafting(true); setDraftError('');
try {
const draft = await CG.agents.artifactMediator.draft();
// only host writes the authoritative draft
if (isHost) {
CG.session.setShared({ artifact: { status: 'reviewing', draft, edits: [], signedA: false, signedB: false } });
} else {
// guest doesn't normally run this; but if they do somehow, they can't write the draft.
// Keep it in local state as a preview; but guest should really wait for host. We show an error.
setDraftError('only the host can commit the initial draft. ask them to click "ask the mediator to draft".');
}
} catch (e) {
setDraftError(String(e.message || e));
} finally {
setDrafting(false);
}
};
// no artifact yet — show the drafting invitation
if (!art || !art.draft) {
return (
phase 08 · artifact
a record you both keep.
the mediator will draft the shortest true thing from the public record: what you
disagreed about, what you came to agree on, what each of you offered, and what
remains open. you'll both edit it together. you both have to sign.
{!isHost && (
waiting for {otherName} to ask the mediator for a draft.
)}
{isHost && (
)}
{draftError && (
error
{draftError}
)}
);
}
// artifact present — show the document
const proposeEdit = (edit) => {
const curEdits = (art.edits || []);
const newEdits = [...curEdits, { ...edit, id: `e-${Date.now()}-${Math.random().toString(36).slice(2,6)}`, seat, approvedA: seat === 'A', approvedB: seat === 'B', ts: Date.now() }];
if (isHost) CG.session.setShared({ artifact: { ...art, edits: newEdits } });
else CG.peer.send('state:propose', { artifact: { edits: newEdits } });
};
const approveEdit = (id) => {
// guest approves via the merge-on-append contract; host materializes applied edits into the draft.
if (isHost) {
const e = (art.edits || []).find(x => x.id === id);
if (!e) return;
// flip host's side
const next = { ...art, edits: art.edits.map(x => x.id === id ? { ...x, approvedA: true } : x) };
const applied = applyIfBothApproved(next, id);
CG.session.setShared({ artifact: applied });
} else {
CG.peer.send('state:propose', { artifact: { edits: [{ id, approvedB: true }] } });
}
};
const rejectEdit = (id) => {
// remove the edit — allowed on either side as a local decision; host is authoritative.
if (isHost) {
CG.session.setShared({ artifact: { ...art, edits: art.edits.filter(x => x.id !== id) } });
} else {
// guest can't force removal; they can only decline to approve. But we can propose a rejection
// by sending the edit back with approvedB=false (no-op under current guard). For v1: guest has
// to ask the host to reject. UX fallback: disable the reject button for guests.
}
};
const sign = () => {
if (seat === 'A') {
if (isHost) {
const next = { ...art, signedA: true };
if (next.signedB) { next.status = 'signed'; next.signedAt = Date.now(); }
CG.session.setShared({ artifact: next });
}
} else {
if (isHost) {
const next = { ...art, signedB: true };
if (next.signedA) { next.status = 'signed'; next.signedAt = Date.now(); }
CG.session.setShared({ artifact: next });
} else {
CG.peer.send('state:propose', { artifact: { signedB: true } });
}
}
};
const draft = art.draft;
const pendingEdits = (art.edits || []).filter(e => !(e.approvedA && e.approvedB));
return (
phase 08 · artifact
the record.
{signed && (
)}
{signed && (
signed by both parties on {new Date(art.signedAt || Date.now()).toLocaleString()}. this is frozen — a historical record.