diff --git a/docs/global-ai-userdraft-requirements.md b/docs/global-ai-userdraft-requirements.md index 458dded510..5deb9995a5 100644 --- a/docs/global-ai-userdraft-requirements.md +++ b/docs/global-ai-userdraft-requirements.md @@ -2,285 +2,211 @@ ## Purpose -Global mode AI chat needs to inspect and modify workspace items without -immediately deploying them. For frontend-only draft state, it should use the -same `UserDraft` service as editors, but with a stricter contract: +This document enumerates the hard requirements that global mode AI chat places +on the frontend `UserDraft` service. It is intentionally limited to required +behavior and does not prescribe API names, data structures, or implementation +strategy. -- editor state must remain safe from accidental deletion or deployment; -- AI-written drafts must be discoverable and deployable; -- live editor handles and persisted localStorage entries must stay consistent; -- backend draft-only items must not be confused with frontend `UserDraft` - entries. +The requirements are based on the current base branch state of `UserDraft`: -This document defines the requirements for that contract. It is intended to -guide a small UserDraft-only PR before wiring global mode AI chat to those -methods. - -## Terms - -### UserDraft - -The frontend local draft service backed by localStorage and optional live -Svelte handles. It is scoped by `(workspace, itemKind, storagePath)`. - -### Storage path - -The path used in the localStorage key and live-handle map. For existing items -this is normally the item path. For new-item editors, this can be an empty -string while the draft value already contains its final workspace path. - -### Item path - -The workspace path inside the draft value, for example -`f/scripts/my_script`. Global mode must operate by item path. - -### Entry source - -Where the current `UserDraft` value came from: - -- `persisted`: localStorage only; -- `live`: live editor handle only; -- `both`: live handle plus localStorage entry. - -This is not the same as draft ownership. - -### Draft origin - -Who wrote the value: - -- editor-originated draft: editor autosave or editor baseline hydration; -- external-originated draft: global mode AI chat, or any future external actor - that writes into UserDraft outside the editor's normal mutation flow. - -Only external-originated drafts are deployable by global AI mode. +- drafts are scoped by workspace, item kind, and storage path; +- persisted drafts are stored in localStorage under `userdraft/w/...`; +- live editor handles may hold draft state that is not present in + localStorage; +- draft values are wrapped with freshness metadata used by editor staleness + checks; +- existing editor flows rely on `UserDraft` for autosave, restore, metadata, + removal, discard, and live-handle behavior; +- the service does not provide a public way to enumerate all draft entries; +- the service does not identify whether a draft was written by an editor or by + an external actor. ## Requirements -### R1. Preserve the existing editor contract +### R1. Existing Editor Behavior Must Be Preserved -Existing editor usage of `UserDraft.save`, `UserDraft.saveIfChanged`, -`UserDraft.use`, `UserDraft.useMany`, `setDraftAndMeta`, and `setMeta` must keep -working without global mode installed. +All existing editor behavior that depends on `UserDraft` must remain unchanged. -Opening an editor and hydrating its current backend state must not create a -deployable AI draft. A clean open editor baseline is useful read context, but it -is not unsaved AI work. +Opening an editor, hydrating current backend state, autosaving local edits, +restoring local edits, checking staleness, removing local drafts, and discarding +to a fallback value must keep the same observable behavior. -### R2. External writes must be explicit +Global mode must not cause a clean editor baseline to become deployable AI work. -Global mode must not call the normal editor-oriented `UserDraft.save` when it -creates or updates an AI draft. It needs an explicit method such as: +### R2. Global Mode Writes Must Be Distinguishable From Editor Writes -```ts -UserDraft.saveExternal(itemKind, storagePath, value, opts) -``` +When global mode creates or updates a frontend local draft, later code must be +able to distinguish that draft from drafts created by normal editor autosave or +editor hydration. -That method must: +This distinction must survive page reloads. -- mark metadata so the entry can later be recognized as external-originated; -- preserve existing freshness metadata such as `remoteRev` and - `remoteDraftRev`; -- update any live handle for the same key immediately; -- persist to localStorage immediately, even if a live handle exists. +This distinction must survive freshness metadata updates. -Immediate persistence matters because global mode listing, deployment, and page -refresh recovery must see the AI-written draft without waiting for an editor -autosave effect. +Legacy drafts with no such distinction must not be treated as global mode AI +drafts. -### R3. Normal editor saves must remain neutral +### R3. External Draft Writes Must Be Visible Immediately -`UserDraft.save` and `UserDraft.saveIfChanged` must not mark drafts as external. -Absence of external-origin metadata must be treated as editor-originated or -legacy state. +After global mode writes a frontend local draft, the written value must be +visible immediately to: -This keeps existing localStorage entries backward-compatible and prevents old -editor drafts from becoming deployable by global mode. +- any live editor handle for the same workspace, item kind, and storage path; +- any localStorage-based lookup for that draft; +- any global mode read, list, deploy, or delete flow that runs after the write. -### R4. Listing must include enough metadata for safe filtering +This must hold even when a live editor handle already exists before the write. -Global mode needs a generic listing API, for example: +### R4. Freshness Metadata Must Not Be Lost -```ts -UserDraft.list({ workspace, itemKinds }) -``` +Global mode writes and any later metadata updates must preserve the freshness +metadata needed by existing editor staleness checks. -Each entry must include: +Freshness metadata changes must not erase the marker that distinguishes global +mode drafts from editor drafts. -- `workspace`; -- `itemKind`; -- `storagePath`; -- best-effort `path` for the storage key; -- `source`: `persisted`, `live`, or `both`; -- `draftOrigin` or equivalent metadata; -- cloned `value`. +The marker that distinguishes global mode drafts from editor drafts must not +erase freshness metadata. -The listing API must merge persisted and live entries for the same key. It must -not expose the same draft twice. +### R5. Draft Enumeration Must Be Possible -### R5. Listing must tolerate live editor runtime values +Global mode must be able to enumerate frontend local drafts for a workspace and +a bounded set of item kinds. -Some live editor values contain functions, class instances, or other -non-structured-cloneable fields. `UserDraft.list` must still return the usable -draft fields instead of crashing. +Enumeration must include persisted-only drafts. -A safe implementation can try `$state.snapshot` and `structuredClone`, then fall -back to a shallow/object-preserving clone when a runtime-only field cannot be -cloned. +Enumeration must include live-only draft values. -### R6. Storage path and item path must stay separate +Enumeration must include draft values that exist both in localStorage and in a +live editor handle. -Global mode works by workspace item path, but UserDraft storage is keyed by -storage path. UserDraft must expose storage path in list entries so callers can -later delete or overwrite the exact stored draft. +Enumeration must not return duplicate logical entries for the same workspace, +item kind, and storage path. -This is required for new-item editors where: +### R6. Enumeration Must Expose Storage Identity -- storage path can be empty; -- draft value path can be `u/admin/new_script`; -- global mode must find the item by value path; -- delete/clear must target the original storage path. +For each enumerated draft, global mode must be able to identify the exact +workspace, item kind, and storage path used by `UserDraft`. -### R7. External drafts must be deployable only when persisted +This is required because global mode operates on workspace item paths, while +`UserDraft` storage paths are not always equal to item paths. -Global mode should consider an entry deployable only if: +New-item editor drafts with an empty storage path must remain representable. -- it is external-originated; and -- it is persisted (`source` is `persisted` or `both`). +### R7. Enumeration Must Expose Persistence State -A live-only value can be read as current context, but deploying a live-only value -is risky because it may be an editor baseline or a transient state that would be -lost on refresh. +For each enumerated draft, global mode must be able to determine whether the +value exists in localStorage, in a live editor handle, or in both. -### R8. Reads must distinguish current context from deployable drafts +Global mode must not need to infer persistence state by reading private +`UserDraft` internals. -Global mode needs two read concepts: +### R8. Enumeration Must Expose Draft Origin -- current frontend-visible item: includes clean live editor baselines and - editor drafts, and is useful as context; -- deployable AI draft: external-originated and persisted. +For each enumerated draft, global mode must be able to determine whether the +draft is global-mode-originated or editor-originated. -The UserDraft API should provide enough metadata for a caller to implement both -views without guessing from the item value alone. +Global mode must not need to infer origin from the draft value shape, item path, +item kind, or localStorage key. -### R9. Clear must reset persisted and live state +### R9. Live-Only Values Must Not Be Deployable By Global Mode -Global mode delete/clear needs a method such as: +A frontend local draft must not be deployable by global mode unless it is known +to be global-mode-originated and persisted. -```ts -UserDraft.clear(itemKind, storagePath, opts) -``` +Live-only values may be read as current frontend-visible context, but they must +not be treated as deployable AI drafts. -The method must: +### R10. Current Context And Deployable Drafts Must Be Separatable -- remove the localStorage entry; -- reset any live handle for that key immediately; -- avoid re-persisting the cleared value via the live handle's autosave effect. +Global mode must be able to distinguish: -The existing `remove` method is not enough for global mode because it clears -localStorage only and can leave stale live state visible. +- current frontend-visible local state, which may include clean editor + baselines, editor-originated drafts, live-only values, and global mode drafts; +- deployable global mode draft state, which must include only persisted + global-mode-originated drafts. -### R10. Discard with fallback must remain available for editors +This distinction must be available without inspecting private `UserDraft` +internals. -Editors still need the ability to discard local changes and restore a deployed -baseline in memory: +### R11. Clearing A Global Mode Draft Must Clear Persisted And Live State -```ts -UserDraft.discard(itemKind, storagePath, fallback, opts) -``` +When global mode clears a frontend local draft, the draft must be removed from +localStorage and from any live editor handle for the same workspace, item kind, +and storage path. -`clear` should be a convenience wrapper for `discard(..., undefined)`, not a -replacement for editor discard behavior. +The cleared live state must not be re-persisted automatically as a side effect +of clearing. -### R11. Metadata updates must preserve ownership metadata +Clearing a global mode draft must not clear unrelated editor drafts or unrelated +workspace items. -`saveMeta` and handle-level `setMeta` must merge metadata instead of replacing -the entire metadata object. Updating `remoteRev` or `remoteDraftRev` must not -drop the external-origin marker. +### R12. Existing Discard Semantics Must Remain Available -Similarly, `saveExternal` must not drop existing remote freshness metadata. +Editor flows must retain the ability to discard local draft state while +restoring a provided fallback value in memory. -### R12. UserDraft must stay domain-generic +Global mode clearing requirements must not remove or weaken that editor +discard behavior. -UserDraft should not know about `WorkspaceItem`, scripts, flows, triggers, apps, -or global AI chat tools. It should expose generic draft entries and metadata. +### R13. Enumeration Must Be Robust To Editor Runtime Values -Mapping `UserDraftItemKind` to global mode workspace item types should live in a -global-mode adapter layer. +Draft enumeration must not fail because a live editor draft contains +runtime-only values that cannot be serialized into localStorage. -### R13. Backend DB drafts are outside UserDraft +Enumeration may omit runtime-only details, but it must preserve the draft data +needed for global mode to identify, read, list, and clear the draft. -Backend draft-only scripts, flows, and apps are not frontend UserDraft entries. -Global mode read/list tools must use backend draft-aware endpoints for those. +### R14. UserDraft Must Remain Domain-Agnostic -UserDraft should only represent local frontend drafts. The global-mode adapter -is responsible for merging: +`UserDraft` must remain independent of global mode workspace item semantics. -- backend deployed items; -- backend DB draft-only items; -- local UserDraft current context; -- local UserDraft external drafts. +It must not need to know about scripts, flows, apps, schedules, triggers, +resources, variables, deployment behavior, or chat tools. -### R14. Tests must cover live/persisted edge cases +Global mode-specific interpretation of item kinds and draft values must remain +outside `UserDraft`. -The UserDraft-only PR should include unit tests for: +### R15. Backend Drafts Must Remain Separate From UserDraft -- `saveExternal` marks origin and persists without a live handle; -- `saveExternal` updates and persists when a live handle exists; -- normal `save` does not mark origin; -- `list` reports `storagePath`, `source`, origin, and cloned value; -- `list` handles persisted-only, live-only, and both states; -- `clear` removes localStorage and clears live state; -- `discard` still restores a fallback baseline; -- metadata updates preserve origin; -- uncloneable live values do not make `list` throw; -- empty storage path plus value-level item path remains representable. +Backend draft-only scripts, flows, and apps are not frontend `UserDraft` +entries. -Global mode integration tests should live in the later global-mode PR, not in -the UserDraft-only PR. +Global mode must not require `UserDraft` to represent backend DB drafts. -## Proposed UserDraft-only PR Scope +Global mode must be able to combine backend draft-aware reads with frontend +`UserDraft` state without treating one storage system as the other. -The first PR should be limited to UserDraft service primitives and tests. +### R16. Cross-Workspace Isolation Must Be Preserved -### In scope +Draft reads, writes, enumeration, and clearing must remain scoped to the target +workspace. -- Add explicit external-write API. -- Add origin metadata to stored draft metadata. -- Enrich `UserDraft.list` entries with `storagePath`, `source`, and origin. -- Make `UserDraft.list` merge persisted and live entries safely. -- Add `UserDraft.clear`. -- Ensure metadata merge behavior preserves origin. -- Add focused `frontend/src/lib/userDraft.test.ts` coverage. +Global mode must not see, modify, deploy, or clear drafts from another +workspace. -### Out of scope +### R17. Item-Kind Boundaries Must Be Preserved -- No global chat tool wiring. -- No changes to global AI prompts. -- No global draft inspector route. -- No deploy behavior changes. -- No script/flow/app-specific adapter logic. -- No backend API changes. +Draft reads, writes, enumeration, and clearing must remain scoped to the target +item kind. -## Later Global Mode PR Scope +Global mode must not treat a draft of one item kind as a draft of another item +kind. -The follow-up global-mode PR can then: +### R18. Regression Coverage Is Required -- introduce a thin adapter that maps UserDraft entries to global - `WorkspaceItem`s; -- use external writes for global mode `write_*` tools; -- list external persisted drafts as deployable; -- read local current context without marking clean editor baselines as drafts; -- clear AI drafts through `UserDraft.clear`; -- merge backend items, backend draft-only items, editor context, and AI drafts - deterministically; -- add global chat unit and browser tests for read/list/write/deploy flows. +The UserDraft-only change must include tests covering: -## Acceptance Criteria - -The UserDraft-only PR is acceptable when: - -- existing editor behavior is unchanged; -- external-origin drafts can be identified without inspecting item shape; -- deployable vs current-context decisions can be made from list metadata; -- live handles and localStorage cannot diverge after external writes or clears; -- no global mode code needs private access to UserDraft internals. +- preservation of existing editor behavior; +- distinction between editor-originated and global-mode-originated drafts; +- persisted-only draft enumeration; +- live-only draft enumeration; +- combined persisted and live draft enumeration without duplicates; +- storage path exposure when storage path differs from item path; +- global-mode-originated writes becoming visible to live handles and + localStorage; +- clearing removing both persisted and live state; +- freshness metadata preservation; +- cross-workspace isolation; +- item-kind isolation; +- enumeration robustness when live draft values contain runtime-only data.