zudo-text

検索したい単語を入力

いつでも検索バーを開ける

Doc Cloud Unified Frame

The portable architecture, state, pin, transport, preview, and retirement contract for core.doc-cloud.

Doc Cloud is zudo-text's external authoring surface for zudo-doc-cloud. Its load-bearing rule is simple:

One core.doc-cloud provider owns one frame containing Projects, project overview/activity, the outline pane, page tabs, editor/preview, and publish UI. Opening an outline page changes the route and tab inside that frame. It never creates another frameset leaf or a second provider.

The outline is not a file chooser frame. It is a service-style recursive tree inside the provider. When recursiveOutline is present, nested categories and pages are projected from it; a flat v1 category/page outline is normalized as a fallback. Root categories alone carry »; category rows use a boxed / toggle, and continuous dashed connectors run through nested children. The selected row uses an inverted aria-current band expressed with component tokens; this is a documented exception to the dropdown active-item token pair. The same tree has a flat variant for narrow panels. Toolbar switches expose the slug/count view options, while bulk controls collapse or open all categories. outlineCollapsedIds, outlineShowSlug, outlineShowCount, and outlineFocusedId are session-cache state, not serialized props. The palette is expressed with zudo-text's warm design tokens while the zudo-doc-cloud hierarchy and affordances remain intact.

Component and ownership map

singleton frameset leaf (`core.doc-cloud`, one per frameset)
└─ DocCloudFrame (`frameId` owns the session-cache entry)
   ├─ DocCloudOutlinePane (mounted while collapsed)
   │  └─ category/page tree → entry.openPage(...)
   ├─ DocCloudOutlineEdgeTab (seam button)
   ├─ DocCloudRoute
   │  ├─ Projects
   │  ├─ Overview / Activity
   │  └─ DocCloudEditor
   │     ├─ page tab + raw frontmatter/Markdown source
   │     ├─ guarded page save machine
   │     └─ labelled Content preview
   └─ checkpoint / publish / conflict / dirty-boundary UI

doc-cloud-provider.tsx is the one provider and doc-cloud-frame-cache.ts is the one Doc Cloud cache. The cache is keyed by frameId, owns project/navigation/search/outline/tab/editor/preview state, and owns one save machine per (project, pageId) tab. Closing or replacing a leaf releases that entry; the frameset sweep destroys entries whose frameId is no longer live. A second page provider, page cache, or cross-frame page-open event would split ownership and is forbidden.

singletonScope: "frameset" prevents a second Doc Cloud leaf in the same frameset. canPopOut: false avoids a second renderer realm for the per-window authoring session. The provider hides the generic layout selector: Edit, Preview, and Split belong to its own toolbar.

Frame state and pins

State has three intentionally different lifetimes:

TierStateRule
Serialized leaf propsprojectSlug, initialSurface, editorLayout, outlineCollapsedSafe seeds only. Validation defaults unknown values. Collapse persists but is excluded from pin identity.
Pin identityprojectSlug, initialSurface, editorLayoutTwo project/layout pins remain distinguishable; transient collapse never changes pin highlight.
Session cacheloaded project, outline expansion/scroll, search, tabs, active page, page source/editor, save machines, preview assets, activity/history/hosted stateNever serialized into settings or pins. There is exactly one module cache.

A Doc Cloud pin is a one-leaf template, not a 30/70 outline/editor split. Its optional providerProps are the same four safe seeds above. Activating it rebuilds the singleton frameset and mints a fresh frameId; the new entry starts from those seeds and fetches authoritative service state. No source text, calling token, generation, history head, tab list, search query, or server response is allowed in a pin or settings document.

The default-project preference is separate from a pin. Only docCloud.defaultProjectSlug may be overridden in deviceOverrides; the other Doc Cloud settings stay in the synced base. Explicit leaf props win. If the effective slug is inaccessible or missing, the provider falls back to Projects and explains why instead of retrying forever.

Settings, availability, and credentials

AppSettings.docCloud contains:

{
  defaultProjectSlug: string | null;
  checkpointOnEdit: boolean;
  publishNotifications: boolean;
  serverOrigin: string; // bare validated origin; never a token
}

The default is null / true / true / "". defaultDocCloudSettings and validateSettings() are the canonical producer and validator. Unknown fields, including any attempted credential field, are dropped.

isDocCloudAvailable() is the single UI gate used by registration, pickers, settings, pins, and commands:

EnvironmentAvailableTransport / token posture
Tauri macOSOnly with a valid injected VITE_DOC_CLOUD_ORIGINRust native HTTP/SSE transport; calling token in macOS Keychain
Tauri iOSNoPackaged transport is not yet proven
Browser desktop/iOSYesFetch/EventSource-compatible bridge; token memory-only by default
MockYesIn-memory deterministic service and token store
Non-macOS desktop developmentOnly when explicitly configuredToken memory-only; localStorage persistence requires the explicit DEV-only adapter opt-in

Production never infers token grants or expiry: the upstream calling-token API does not expose that metadata. A pasted calling token is the working primary sign-in path. zudo-doc-cloud #925 is future browser-handoff copy only; it is not a disabled or hidden primary. Global zudo-text sign-out crosses the workspace dirty boundary first and then disconnects Doc Cloud too, with explicit copy. Doc Cloud-only sign-out removes only its local credential.

The configured origin must be a bare HTTPS origin (loopback HTTP is allowed only under an explicit development signal). Packaged macOS traffic uses the Rust doc_cloud_transport commands rather than depending on WebView CORS, and the bridge pins that native bearer transport to its exact build-owned origin. An empty settings override restores the adapter origin; only fetch-backed web or explicit development adapters can select a different runtime origin. A1 remains the upstream CORS decision for direct packaged-webview traffic; it is not evidence that real-service traffic has passed locally.

Authoring, live state, and publishing

The client resolves Personal authoring state, loads the generation-qualified authoring page, and keeps a paired (generation, historyHead) guard. Every PUT includes that pair plus clientId, historyGroupId, and an idempotency key. The editor changes its own state only from a successful guarded response. 409 becomes a retained-draft conflict with Retry my draft and Discard my draft; a network failure stays dirty/offline; a removed page becomes a detached, copyable, read-only tab.

The first edit of a session creates the configured best-effort checkpoint before the first save. Checkpoint failure is visible but does not silently turn the page write into an unguarded write. Closing a page/frame, switching the workspace or Doc Cloud origin, signing out, or quitting must use the shared dirty coordinator and offer save/discard/cancel where applicable.

Live observation uses three independent SSE routes, all owned by the shared project session:

  1. Personal change-set events for page, outline, and asset advisories.

  2. Project events for hosted-site and history advisories.

  3. Publication events for accepted-source movement.

An asset relocation emits one personal change-set-changed advisory with resource: "asset", plus assetIds and pageIds from publishAuthoring in server/routes/change-set-authoring.ts. It represents one commit rather than one event per rewritten page. The client invalidates preview assets and refetches the listed open pages.

Events are body-free invalidation hints, not authoritative state. Consumers refetch after an advisory. Hosted delivery specifically combines the project stream with an authoritative hosted-site GET. Streams reconnect with bounded backoff and do not multiply when several tabs use the same project session.

Publish first flushes every dirty project tab, then obtains one fresh paired Personal state and fresh project/accepted-publication revisions. The request carries all of those guards and an idempotency key. A 202 means Accepted, not live: the dialog shows an Accepted chip and the project/publication streams continue tracking building/live/failure after the dialog closes.

Hosted status exposes visibility.access (public, unlisted, or private) as a chip. Open site is disabled for private sites; zudo-text does not pretend that a private viewer handoff is a public URL action.

Content preview and vendored seams

The UI calls the preview Content preview. It promises Markdown/content parity, not zudo-doc-cloud theme or chrome parity. Processing order is fixed:

renderHtml → admonitions → contextual asset rewrite → DOMPurify 3.4.13

Personal assets resolve filename → assetId from the current AuthoringSnapshot, then fetch the generation-qualified change-set authoring asset route. Asset paths use dir/filename; a root asset has an empty dir and therefore uses its bare filename. The legacy project asset-library route is never used. Object URLs are revoked on replacement/unmount.

Content preview promises Markdown parity. Authored <Asset> cards and the Asset Viewer are hosted-site features and are deliberately not rendered in the preview. The sanitizer policy is unchanged and remains guarded by the hostile fixture.

Every upstream-derived surface has a named drift alarm:

Vendored surfaceLocal ownerDrift alarm
wire DTOs, route builders, typed error mapping, strict frontmatter split/serialize@takazudo/doc-cloud-clientcontract fixtures and client tests compare routes/status/error bodies against the pinned upstream contract
guarded page-save state machine and idempotency@takazudo/doc-cloud-clientdeterministic conflict, retry, discard, generation/history-head, and duplicate-key tests
Personal/project/publication SSE parsing and shared project-session lifecycle@takazudo/doc-cloud-clientthree-channel reconnect/refusal/body-free-advisory tests; no second session per tab
Markdown renderer and admonition transform@takazudo/doc-cloud-previewparity fixtures run the fixed transform order and pin the wasm runtime
contextual asset rewrite and object-URL lifecycle@takazudo/doc-cloud-preview + providergeneration-qualified route/revoke tests reject the legacy asset route
sanitizer policy@takazudo/doc-cloud-previewDOMPurify 3.4.13 pin plus malicious HTML/URL fixtures
recursive service-style page treeDocCloudOutlinePane + provider CSSrecursiveOutline nested categories with v1 fallback; root-only », boxed / toggle, continuous dashed connectors, inverted aria-current band via component tokens (documented exception to the dropdown active-item token pair), flat variant, toolbar switches, and session-cache fields outlineCollapsedIds, outlineShowSlug, outlineShowCount, outlineFocusedId; keyboard/ARIA/tree-row tests plus the T1 service-style-outline assertion
upstream baselinevendoring notes and dependency pinsupstream commit/route audit; re-run all alarms before changing the pinned contract

The provisional wasm move is atomic: move all nine consumer manifests and EXPECTED_RUNTIME_VERSION in lockstep. If parity fails, use a Doc Cloud-only package alias and leave the core pin unchanged. Never partially bump the shared consumers.

Directory View retirement and EFE

core.dir-view is retired. External File Editor (core.external-file-editor) now owns the extracted directory tree as a pane inside the same frame as its tabs and editor. A tree click calls that frame's session.loadFile(path); there is no cross-frame open-file event, active-else-first EFE scan, focus hop, or split fallback.

EFE serializes { openPaths, activePath, treeRoot, treeCollapsed }, and its pin identity includes { layoutId, paths, treeRoot } but excludes collapse. Its frameId-keyed session cache owns the tree slice. The width remains the global clamped AppSettings.directoryView.lastSidebarWidthPx setting; the key is retained for width compatibility even though there is no Directory View provider. The seam edge tab is a named button with aria-expanded, pointer and native keyboard activation, visible :focus-visible, and state-specific Collapse/Expand labels. The pane remains mounted while collapsed (aria-hidden and inert) so tree/session state is not lost.

Pre-release state is disposable: there is deliberately no migration for old pins, frameset leaves, or leaf state naming core.dir-view. Validation may preserve that opaque pin, which can render provider-not-found. The user may need to remove it or reset settings. This is the only supported meaning of the retired id; do not re-register it.

Open preference questions

These are still owner questions. The text after the arrow is executable v1 behavior, explicitly provisional rather than a preference answer:

  1. Preview visual/theme parity? — OPEN. Ship a clearly labelled Content preview; do not claim published-theme parity.

  2. Should zudo-text sign-out also disconnect Doc Cloud? — OPEN. Handle dirty buffers, then use coupled global sign-out and say that Doc Cloud will disconnect.

  3. Should the default project sync? — OPEN. Only defaultProjectSlug is device-scoped; inaccessible values fall back to Projects.

  4. How should non-macOS tokens persist? — OPEN. Memory-only by default, with explicit DEV localStorage opt-in.

  5. Should this feature move the shared wasm version? — OPEN. Move the nine consumer manifests plus EXPECTED_RUNTIME_VERSION together; use the Doc Cloud-only alias fallback if parity fails.

D26 is closed, not a sixth open question: token paste is primary and #925 is future handoff copy only.

Verification map

ContractDeterministic evidence
one provider/cache, safe seeds, frameset singleton, dirty lifecycledoc-cloud-provider.test.tsx, doc-cloud-provider.render-pressure.test.tsx, registration/pin/settings tests
guarded wire/save/SSE/publish/previewpackages/doc-cloud-client/**, packages/doc-cloud-preview/**, backend bridge tests
one complete untagged mock journeye2e/doc-cloud-walking-skeleton.spec.ts
Doc Cloud seam, same DOM pane, focus, 1280×800 and 1024×800 overflowthe same T1 spec
EFE seam and mounted treecomponent tests plus e2e/efe-tree-toggle.spec.ts
transport allowlist/body/SSE behaviordeterministic TypeScript/Rust transport suites; these prove the contract, not a real account session
macOS WebKit UI compatibilitythe documented macOS WebKit workflow (exam); it does not launch a packaged app against the real service
packaged macOS + real-service trafficoutstanding unless a run has disposable credentials and records that exact packaged-app evidence; neither a Linux/WSL run nor the WebKit workflow may be reported as this

The T1 journey is intentionally narrow and singular: token paste → Projects → service-style outline → same-frame page tab → edit → saved → direct guarded refetch proving the content persisted → publish dialog → Accepted chip. It asserts the original frameId and leaf count throughout. Conflict matrices, stream failures, sanitizer cases, and preference validation belong in unit or component suites; real-service/macOS checks belong in their explicitly tagged workflow.

Forbidden architecture audit

Before release, source search must find no core.doc-cloud-page, no DOC_CLOUD_OPEN_PAGE_EVENT, no second Doc Cloud cache, no registered retired Directory View provider, no obsolete Directory View cross-frame event, and no toolbar panel button for either embedded pane. Historical documents may name a retired id only when unmistakably recording the old design; executable code may name it only in the no-migration validator/regression test.