docs: tighten global ai userdraft requirements

This commit is contained in:
centdix
2026-05-20 15:55:50 +02:00
parent f7d987a1be
commit 3d142eded7
+137 -211
View File
@@ -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.