Mechanically re-derived: bin/debrand runs over a fresh import of posthog/posthog@fa1a9a60d4 (2026-08-06). The pipeline (.hanzo/workflows), the Dockerfiles and bin/docker* entrypoints, compose files, LLM.md and the squashed initial migration are restored from the shipped tree, same as the previous import. The prune's import-closure guard kept 21 products the new core now imports; dropping them is a refactor now, recorded rather than forced.
5.9 KiB
name, description
| name | description |
|---|---|
| validating-and-publishing-canvases | Validate and publish a canvas source project safely: the source-project shape, declared capabilities, reading the current version pointer, iterating on validation diagnostics, guarded publishing with expected_current_version_id, waiting out the queued build, and recovering from a 409 version_conflict or a 429 capacity limit without overwriting concurrent work. Use whenever a canvas edit is ready to save, a canvas publish or build returns diagnostics or a conflict, or a task needs to understand canvas version history. |
Validating and publishing canvases
A canvas's source lives in Insights, versioned per publish. Publishing is guarded: every edit is based on a specific version, and the server refuses to overwrite newer work. Every publish queues a server-side build, and the canvas renders the last successful build.
The source project
canvas-source-retrieve returns:
project—schemaVersion(1),files(path → content),entryHtml("index.html"),dependencies(exact platform-pinned versions),canvasSdkVersion,capabilities.current_version_id— the version your edits are based on. Keep it; the publish needs it. It isnullfor a canvas that has never been published — pass thatnullon the first publish.
Keep index.html and dependencies exactly as returned. You may add relative source files and
admitted assets to the project. Use ?worker for a self-contained module worker and represent
binary assets as base64 entries in assets; new npm dependencies or dependency-version drift fail
validation.
Declare capabilities
The host enforces project.capabilities at runtime, so an undeclared ph call builds fine and
then dies in the rendered canvas. Declare:
capabilities.insights.insights— every insight short id the canvas passes toph.loadInsight.capabilities.insights.captureEvents— every event name it passes toph.capture.capabilities.insights.inlineQueries: true— when it callsph.queryat all.
Validation rejects undeclared literal calls (capability_missing_* diagnostics) so you can fix
them before publishing; dynamic ids it can only warn about, so keep the declarations complete.
Validate until clean
canvas-validate-create is side-effect free; call it as often as needed.
Diagnostics carry severity, a stable code, a message, and (for file-specific problems)
path and line:
errordiagnostics block publishing — fix all of them. Common ones:import_not_allowed(bare imports are limited to react, react-dom, @hanzo/quill, recharts, lucide-react, and dayjs),forbidden_dynamic_import/forbidden_require/forbidden_inline_script,invalid_path,capability_missing_insight/capability_missing_capture_event/capability_missing_inline_queries,dependency_not_admitted/dependency_version_mismatch, and path/size violations.warningdiagnostics don't block, but heed them:network_fetch/network_xhrmean the code reaches for the network directly — the sandbox will block it at runtime; use thephbridge.
Publish guarded
Two ways to save, both guarded:
- Whole project —
canvas-publish-createwith the completeproject. - Per-file edits —
canvas-edit-createwithoperations(each sets a file's complete content, or deletes it withcontent: null). Prefer this for small changes to a large project; the guard is mandatory here because a diff's meaning depends on its base.
For a whole-project publish with canvas-publish-create:
- Always pass
expected_current_version_id— thecurrent_version_idyou read (or explicitnullon a first publish). Unguarded publishes can silently clobber concurrent edits. - Include a short
promptdescribing the change; it becomes the version-history entry's label. - Pass
nameonly to rename the canvas (e.g. a first build of an untitled canvas). - Publish once per requested change. If the user asks for another edit afterwards, re-read the source (the head may have moved) and publish again — don't batch unrelated changes into one version, and don't publish work-in-progress after every micro-edit.
- A 429 means the team's build capacity is temporarily exhausted. Wait ~30 seconds and retry the same publish; nothing was saved.
The response returns the new current_version_id.
After publishing: wait for the build
A publish queues a server-side build of the version. The canvas does not update until the build
is ready, and nobody else is watching the result — you own it. Poll canvas-builds-retrieve
every few seconds (up to ~2 minutes) until the build you queued is terminal:
queued/building— in progress; poll again shortly.ready— the canvas'spublished_build_idadvances to this build (unless a newer publish superseded it first). The task's canvas work is done.failed— read the build's error diagnostics, fix the project, and publish again. A failed build never replaces the last good one, so the canvas keeps rendering the previous version — finishing the task here would leave the user with a stale canvas and a silent failure.
Recovering from 409 version_conflict
A 409 means the canvas moved past your base — a concurrent publish or a revert. The response
includes the live current_version_id. Never retry unguarded to force your version through:
- Re-read the source with
canvas-source-retrieve. - Re-apply your edits to the fresh source (the new head may contain someone else's changes — preserve them).
- Publish again with the new
current_version_id.
Version history semantics
Each publish appends a full source version and moves the head pointer; users can revert to older versions in the app (which republishes and rebuilds them). The guard matters because basing your publish on the version you actually read is what keeps a user's revert, another agent's publish, and your edit from silently erasing each other.