Paul's real clipboard bytes closed the second half
Captured off the actual Google Docs copy, and they say something the reconstruction did not. The blank line is a bare <br> sibling between the <p>s, as expected. But the trailing blank line was never Google's:
…<p>…Four…</p></b><br class="Apple-interchange-newline"><br class="Apple-interchange-newline">
Two of them, after the closing </b>, added by WebKit to mark where the copy stopped. ProseMirror's clipboard parser ignores a <br> that is the last child of a non-inline parent, which disposes of exactly one; the first has a next sibling and survives.
Commit 9782cbf ignores that marker by its class. Discriminating on the class rather than on position is what makes it safe: a blank line the author really did leave at the end is a <br> Google puts INSIDE its wrapper carrying no class, and a positional "drop a trailing empty paragraph" rule would have eaten it. Both are checked against the real bytes — Paul's copy now yields exactly the five blocks Google shows, and the same copy with a real trailing blank keeps six.
Local gate green, hub CI green on 1bfbcd7. Twelve tests in paste.test.mjs.
Plan 7 is up for review
docs/superpowers/plans/2026-09-02-images-links-tables.md (3f4ac69), from Paul's brief: "make a plan to handle images, links, and tables. Images can be rendered immediately and uploaded async to object storage". With Fable now.
The load-bearing decision is D1. fragment_html's output is template.HTML written unescaped, and it is safe today only because no byte from a document reaches attribute position. Tables keep that property — colspan/rowspan are integers, clamped like heading.level. Links and images spend it. So the plan buys it back once and deliberately (an escaper, a scheme allowlist, a rejected URL rendering as text rather than as a dead anchor, adversarial tests) before either caller exists — which is why links come before images and why Task 4 lands with no caller at all.
D4 is the ordering trick images need. The src in the CRDT is /d/{sqid}/blobs/{plaintext sha-256}, hashed in the browser before the upload starts, so the browser knows the final URL up front, inserts the node with its real src immediately, and shows a blob: preview through a NodeView until the upload lands — no placeholder attribute, no rewrite pass. §11.5 requires blobs.Sealed, whose address is the ciphertext's hash, so a small owned document_blobs table maps plaintext hash to blob hash and carries the per-document authorisation with it.
D6 is the open risk. A Google Docs paste gives a googleusercontent.com URL, not bytes, and fetching it from the browser is subject to CORS with no canvas fallback. The slice degrades loudly per §11.2 rather than smuggling in a server-side fetcher, which is an SSRF surface and its own decision. Worth testing against a real Doc before Task 7 is built — the answer changes the deliverable.
Coordination
Discussion #20 (worktree-import-documents) is building DOCX/Markdown/PDF import, which produces these same table/image/link nodes. Whoever gets there first should post the node names and attribute shapes here or there so the two branches do not invent different vocabularies. Plan 7 proposes prosemirror-tables' own four types with column resizing off, so colwidth stays null and nothing new-shaped crosses the wire.