Skip to content

JP-470: publish lifecycle — teardown seam, frozen-artifact carry, read-only spellcheck - #429

Open
QR-Madness wants to merge 4 commits into
masterfrom
jp-470-publish-lifecycle
Open

JP-470: publish lifecycle — teardown seam, frozen-artifact carry, read-only spellcheck#429
QR-Madness wants to merge 4 commits into
masterfrom
jp-470-publish-lifecycle

Conversation

@QR-Madness

Copy link
Copy Markdown
Collaborator

What

Staging verification of published documents (JP-464) surfaced three defects; this is the app-side fix (pairs with JPE-Net-Technologies/docushark-web's JP-470 PR — the web POST change is additive, so merge order is not load-bearing).

  1. Publish state didn't follow the document. Deleting a published doc left the link serving forever; restoring one orphaned the publication.
  2. Spellcheck ran on read-only prose — squiggles, popover, "Add to Dictionary", and the formatting context menu were all live for viewers (programmatic chains bypass editable: false).
  3. (Rider) The connect modal labeled a localhost relay "Toronto, Canada".

How

  • teardown_publish — the single removal seam. Document delete (REST + MCP via after_doc_deleted, now async), restore retirement, and explicit unpublish all end a publication through one path: hydrate the registry first (a cold machine must not fail open), remove the entry + local artifacts, best-effort object deletes (run even when the registry write errs after its in-memory removal — otherwise a cold boot from the un-rewritten mirror serves a deleted doc forever), then one registry mirror PUT.
  • Restore carries the FROZEN artifact to the successor id — never a re-projection: since-publish edits stay unexposed, the meter is net-neutral, status reads stale until an explicit republish. Cold-machine fallback reads the frozen bytes from the object store. The ack reports publishCarried + new keys.
  • Editor: deleteFromHost revokes the share row first (single choke point for trash/browser/transfer; failure swallowed — the relay teardown is the guarantee). VersionHistoryPanel repoints the row to the successor via the web's transactional move; a failed repoint stashes a breadcrumb that PublishRung replays.
  • Spellcheck: gated on live editability at the decorations prop, recheck debounce, plugin init, onCreate, the whole context-menu handler, and both popover writes — with JP-370 promotion/demotion re-entering everything without a remount.

Verification

  • Relay: 783 lib + 13 publish integration tests, including an in-process fake-S3 cold-machine teardown (empty volume, mirror-only state) and a byte-identity carry pin. cargo check --all-targets clean.
  • Editor: 3253 tests, typecheck clean.
  • Eleven mutation checks across the relay seam (hydration, seam call, carry) and every editor gate — each killed by exactly its paired test.
  • Live E2E on the local stack: publish → restore through the Version History panel → carried artifact byte-identical to the frozen snapshot, zero post-publish content leak, status published + stale, old id 404 + artifacts gone; trash via UI → publication died with the doc (registry rewritten). The real move_share_link SQL exercised against local Supabase (token/view-count/revoked_at/published_at preserved; winner rule holds).
  • Guest dead-link handling (review F10) verified already fail-closed in shipped code: the Worker 404s a live-row/dead-object state and the guest shell maps 404 → unavailable.
  • Reader-facing serving (live URL, guest link, viewer-role spellcheck) is staging-scope — same as JP-464 — and rides the next staging deploy.

Assisted-by: Fable 5

- teardown_publish: the single removal seam (document delete, restore
  retirement, explicit unpublish) — hydrates the registry first so a
  cold machine cannot fail open, and still clears the public objects
  when the registry write errs after its in-memory removal
- after_doc_deleted is now async and ends the publication, so REST
  delete, MCP delete, and restore retirement cannot drift
- restore carries the FROZEN artifact to the successor id (never a
  re-projection: since-publish edits stay unexposed, the meter is
  net-neutral, status reads stale until an explicit republish); the
  ack reports publishCarried + object keys + bytes
- tests: lifecycle pins for delete + restore-carry, an in-process fake
  S3 endpoint driving the cold-machine teardown, and all three new
  guards mutation-checked

Assisted-by: Fable 5
Tiptap's editable:false blocks typing, not programmatic commands — the
spellcheck popover's insertContent, Add to Dictionary, and every
formatting context-menu action were live for view-only collab members
and published-page readers.

- extension: decorations prop, recheck debounce, plugin init, and
  onCreate all gate on live editability; read-only surfaces never prep
  the dictionary (ProsePreview recreates its editor per render — no
  more full-doc scan per tick)
- chrome: whole onContextMenu returns early when not editable (no
  preventDefault, so the native menu keeps copy); the native-spellcheck
  attribute + rebuild effect now tracks editability, and the
  editable+custom arm preps the dictionary so a JP-370 mid-session
  promotion re-enables everything without a remount
- popover: both writes no-op on a read-only editor (permission can
  flip while it is open)
- tests: every gate paired with an editable control

Assisted-by: Fable 5
- deleteFromHost revokes the share row BEFORE the relay delete — the
  single choke point every delete path funnels through (trash, browser,
  transfer), row-first like unpublish, swallowed on failure (the
  relay's artifact teardown keeps readers dark regardless)
- restore: the relay ack now reports the publish carry
  (RestoreRecoveryAck); VersionHistoryPanel moves the share row to the
  successor id via repointShareLink (same token, view count, revocation
  state — migration 0022's move), or darkens the retired id's row when
  nothing was carried
- a failed repoint stashes a pending-repoint breadcrumb; PublishRung's
  refresh replays it when it observes published-but-linkless, clearing
  on success (and on forbidden, which never self-heals)
- pins: revoke ordering + failure isolation, repoint request shape,
  forbidden-is-terminal, breadcrumb round-trip

Assisted-by: Fable 5
A dev/OSS build (no VITE_RELAY_BASE_URL) aliased the primary region
label onto localhost — the switcher said "Toronto, Canada" while
ADVANCED showed http://localhost:9876. The label now derives from the
URL the build actually points at: loopback origins present as
"Development (localhost)"; hosted builds keep their region's name.
Small UX correction, only visible to developers.

Assisted-by: Fable 5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant