docs: define durable alpha workspace identity

This commit is contained in:
mirivlad 2026-07-12 16:42:05 +08:00
parent f59af601d6
commit 139b2a67b7
1 changed files with 122 additions and 35 deletions

View File

@ -12,6 +12,38 @@ The user-visible Russian term is **Дело**. Existing platform and storage nam
such as `workspaceRootPath` remain internal compatibility details in this such as `workspaceRootPath` remain internal compatibility details in this
tranche. tranche.
## Durable identity of a Дело
Every managed Дело has an immutable UUID v4 `workspaceId`. It is the identity for
relations; `workspaceRootPath` is only the current or historical filesystem
address and presentation label. Inbox assignments, domain bindings, Activity
events/sessions/candidates, Journal source references, and Overview state store
`workspaceId` as their primary relation key and retain a path only as a cached
display value.
The UUID lives in a small immutable marker inside the case folder,
`<Дело>/.verstak/workspace.json`, and is also indexed in Desktop metadata. The
inside-folder marker survives a Desktop rename, Trash move, restore, and an
external filesystem rename. It prevents a newly created folder with the same
path from inheriting old links.
On first alpha startup, each writable legacy or externally created top-level
case without a marker receives a new UUID and its path-keyed relation data is
migrated to that UUID. If a folder is not writable, it remains viewable but
cannot be selected as a durable relation target until its marker can be
created. If two active folders contain the same UUID (for example after a
filesystem copy), Desktop shows an identity-repair action and does not
automatically attach Inbox, binding, or Activity data to either duplicate.
The repair UI asks which folder retains the existing identity; it generates a
new marker UUID for the other folder and leaves old relations with the retained
identity. It never silently merges the two folders' histories.
Workspace lifecycle events carry both `workspaceId` and current path. A rename
updates only the path cache. Trash and restore retain the same ID and use the
trash ID only to match the particular trash operation. When a folder is removed
outside Desktop, its relations retain the UUID and become unavailable; a new
folder at the old path has a newly generated UUID and cannot take them over.
This document supersedes conflicting decisions in the following older narrow This document supersedes conflicting decisions in the following older narrow
designs: designs:
@ -171,9 +203,11 @@ The existing local pairing token gates the endpoint; the token is never placed
in Activity storage or UI. in Activity storage or UI.
The receiver annotates a valid activity with an existing exact hostname-to-Дело The receiver annotates a valid activity with an existing exact hostname-to-Дело
binding when one exists. Bindings remain explicit: `client.example.com` does binding when one exists. A binding stores `workspaceId` as identity and the
not imply `example.com` or the reverse. This is deliberately different from current root path as display cache. Bindings remain explicit:
the exclusion-list suffix rule. Unbound activity is stored as global activity. `client.example.com` does not imply `example.com` or the reverse. This is
deliberately different from the exclusion-list suffix rule. Unbound activity is
stored with the explicit `unassigned` session scope.
## Activity and Journal ## Activity and Journal
@ -201,9 +235,18 @@ Activity presents chronological sessions rather than a raw log by default. A
browser activity session displays, for example, `admin.client-site.ru · 1 ч browser activity session displays, for example, `admin.client-site.ru · 1 ч
32 мин`; it exposes no hidden URL/title data. 32 мин`; it exposes no hidden URL/title data.
A logical session is scoped to one existing Дело and is split only by a A logical session has one explicit scope:
different Дело, a 20-minute idle gap, or 120 minutes of total session span. It
is not split merely because midnight passes. A session is ready when either: ```text
{ kind: "workspace", workspaceId, workspaceRootPath } | { kind: "unassigned" }
```
An unassigned session is a normal temporal scope, not an exception. It is shown
in Activity and may open Journal review, where the user must choose an existing
active Дело. That one-time choice does not create a binding. A workspace session
is split only by a different workspace ID, a transition to/from unassigned, a
20-minute idle gap, or 120 minutes of total session span. It is not split
merely because midnight passes. A session is ready when either:
- meaningful events in its session cover at least ten minutes and include at - meaningful events in its session cover at least ten minutes and include at
least two events; or least two events; or
@ -212,10 +255,33 @@ is not split merely because midnight passes. A session is ready when either:
`workspace.selected`, `file.opened`, and `note.opened` are diagnostic context, `workspace.selected`, `file.opened`, and `note.opened` are diagnostic context,
not meaningful work by themselves. not meaningful work by themselves.
Each logical session receives a stable `sessionId` derived from its case and Duration is normative rather than the wall-clock span from first to last event.
first ordered event; the service stores that ID on each appended event and Sort meaningful events by `{occurredAt, activityId}` within a session:
preserves the session-start metadata through log compaction. It persists an
ordered handled watermark - A browser-domain record contributes its explicit validated
`durationSeconds`.
- A file/note event has no inherent duration. For each adjacent pair of
zero-duration meaningful events, add `min(time difference, 10 minutes)` only
when no explicit-duration browser record lies between that pair in the
ordered session.
- A gap over 20 minutes has already split the session, so it contributes no
implicit duration. The first and final standalone point event each contribute
zero seconds.
- Candidate duration is the sum of explicit browser duration and these implicit
point-event intervals, capped at the 120-minute session maximum. It is never
inferred from the overall first-to-last span.
Thus a note saved at 10:00 and a file changed at 10:09 estimate nine minutes;
the same events at 10:00 and 10:50 form separate zero-duration sessions rather
than a fictional 50-minute block.
Each logical session receives a newly generated immutable UUID `sessionId` at
creation and stores an immutable anchor `{scope, firstSeenAt, firstActivityId}`.
The service stores that ID on each appended event and preserves the anchor
through log compaction. A late event may join a same-scope session only when it
falls within that session's 20-minute boundary; otherwise it starts a new
session. It never recomputes an existing session ID or anchor. Each session
persists an ordered handled watermark
`{ occurredAt, activityId }` and optional state for the latest reviewed slice. `{ occurredAt, activityId }` and optional state for the latest reviewed slice.
A candidate contains only source events after that watermark: A candidate contains only source events after that watermark:
@ -272,14 +338,17 @@ A capture remains one canonical record with independent fields:
```text ```text
globalState: active | archived globalState: active | archived
workspaceRef: active | trashed | unavailable | orphaned workspaceId: optional immutable UUID of assigned Дело
workspaceRootPath: optional active or historical Дело path workspaceState: unassigned | active | trashed | unavailable | orphaned
workspaceRootPath: optional current or historical Дело path cache
workspaceTrashId: optional stable trash identity workspaceTrashId: optional stable trash identity
``` ```
Existing records migrate to `globalState: active`; their current assignment is Existing records migrate to `globalState: active`. An assignment whose current
retained. The current test vault may be discarded, but the migration is path resolves to an active marker receives that marker's `workspaceId`; an
non-destructive for an alpha user vault. unresolved legacy path becomes `unavailable` rather than attaching to a future
folder of the same name. The current test vault may be discarded, but the
migration is non-destructive for an alpha user vault.
### Actions ### Actions
@ -288,8 +357,8 @@ non-destructive for an alpha user vault.
- **Убрать из общих входящих** changes `globalState` to `archived`. It never - **Убрать из общих входящих** changes `globalState` to `archived`. It never
removes the assignment, so an assigned capture remains available from its removes the assignment, so an assigned capture remains available from its
Дело. Дело.
- **Открепить от дела** removes only the `workspaceRef` and - **Открепить от дела** clears `workspaceId`, `workspaceRootPath`, and
`workspaceRootPath`. If the capture is `workspaceTrashId`, and sets `workspaceState` to `unassigned`. If the capture is
still globally active it returns to the global Inbox; otherwise it remains in still globally active it returns to the global Inbox; otherwise it remains in
archive. archive.
- **Удалить везде** removes the canonical capture and its assignment. It is a - **Удалить везде** removes the canonical capture and its assignment. It is a
@ -305,27 +374,29 @@ non-destructive for an alpha user vault.
overwrites silently: the dialog offers an explicit `name (2).url` alternative overwrites silently: the dialog offers an explicit `name (2).url` alternative
or cancellation. Read-only/error cases leave the capture unchanged and show or cancellation. Read-only/error cases leave the capture unchanged and show
the failure. Success publishes the normal safe file/link Activity event; the the failure. Success publishes the normal safe file/link Activity event; the
written file opens through the existing user-initiated OS-default-browser link is opened through the dedicated URL-opening behaviour below, not by
file action. relying on a Linux `.url` file association.
Bulk actions operate only on the visibly filtered capture set, show the count, Bulk actions operate only on the visibly filtered capture set, show the count,
and use archive rather than permanent deletion. Every permanent deletion and and use archive rather than permanent deletion. Every permanent deletion and
Journal deletion has a cancellation-safe confirmation dialog. Journal deletion has a cancellation-safe confirmation dialog.
When a Дело is renamed, the Inbox service migrates its capture assignments and When a Дело is renamed, the Inbox service updates the path cache of captures
exact domain bindings from the old root to the new root. It does not migrate a and exact domain bindings with that `workspaceId`; no relation is keyed by the
binding that has been manually changed to another Дело during that operation. old root path. It does not update a binding that has been manually changed to a
different workspace ID during that operation.
When a Дело goes to Trash, assignments and bindings become `trashed` with the When a Дело goes to Trash, assignments and bindings for its `workspaceId`
Desktop trash ID. They remain visible as unavailable historical context but are become `trashed` with the Desktop trash ID. They remain visible as unavailable
not used for automatic routing. A restore event carrying that trash ID restores historical context but are not used for automatic routing. A restore event
the assignment/binding, including a restored path changed by a collision. A carrying that trash ID restores the same ID's assignment/binding, including a
permanent trash purge changes captures to unassigned and changes bindings to a restored path changed by a collision. A permanent trash purge changes captures
visible `orphaned` state that the user can reassign or remove. Activity remains to unassigned and changes bindings to a visible `orphaned` state that the user
historical and labels the case as deleted. If a case disappears by external can reassign or remove. Activity remains historical and labels the case as
filesystem change, active references become `unavailable`, automatic routing is deleted. If a case disappears by external filesystem change, active references
disabled, and the user must explicitly reassign or remove them; a newly created become `unavailable`, automatic routing is disabled, and the user must
folder with the same name never steals those references. explicitly reassign or remove them; a newly created folder with the same name
has a different `workspaceId` and never steals those references.
### Archive and filters ### Archive and filters
@ -337,6 +408,16 @@ to `active`. Archive supports a visible filtered bulk restore with a count and
confirmation. A capture assigned to a Дело remains visible in that Дело's Inbox confirmation. A capture assigned to a Дело remains visible in that Дело's Inbox
regardless of its global archive state, with an Archive badge. regardless of its global archive state, with an Archive badge.
### Opening saved links on Linux
The platform adds a user-initiated `urls.openExternal` capability and API. It
accepts only a validated HTTP(S) URL and opens that URL through the system
browser opener (`xdg-open` in the Linux alpha); it never passes a `.url` file
path to the opener. Browser Inbox uses this capability for **Open link** and
for a saved `.url` after parsing and validating its `URL=` value. The Files
surface recognizes a valid `.url` file and uses the same URL-opening path. This
does not depend on desktop file-association support for InternetShortcut files.
## Alpha interface ## Alpha interface
- Use Russian product labels consistently: **Дела**, **Входящие**, - Use Russian product labels consistently: **Дела**, **Входящие**,
@ -350,6 +431,9 @@ regardless of its global archive state, with an Archive badge.
active unprocessed Inbox records, and existing urgent Todos, in that order active unprocessed Inbox records, and existing urgent Todos, in that order
within their respective priority. An Inbox record is new/needs attention within their respective priority. An Inbox record is new/needs attention
while it is active and unprocessed, without an arbitrary age cutoff. while it is active and unprocessed, without an arbitrary age cutoff.
- Todo rows are included only when a loaded plugin exposes the `todo.workspace`
capability. A missing or disabled Todo plugin is not an Overview error and
contributes no placeholder rows.
- **Continue work** shows at most four distinct case-scoped entities from the - **Continue work** shows at most four distinct case-scoped entities from the
last 14 days: unfinished Todo, unprocessed capture, and the most recent last 14 days: unfinished Todo, unprocessed capture, and the most recent
note/file/Journal entity. Every item carries `lastMeaningfulAt`; items sort note/file/Journal entity. Every item carries `lastMeaningfulAt`; items sort
@ -398,11 +482,14 @@ Automated checks must cover:
- Desktop activity receiver authentication, canonical normalization, - Desktop activity receiver authentication, canonical normalization,
validation, idempotency, binding, and event publication; validation, idempotency, binding, and event publication;
- append-only Activity log retention/compaction, background subscription - append-only Activity log retention/compaction, background subscription
lifecycle, handled watermarks, across-midnight review, Journal handoff, lifecycle, UUID workspace/unassigned session scopes, point-event duration
local dates, case-scoped clear, and missing-Journal feedback; calculation, immutable session IDs/late events, handled watermarks,
across-midnight review, Journal handoff, local dates, case-scoped clear, and
missing-Journal feedback;
- assigning, archiving, restoring, unlinking, permanent deletion, `.url` - assigning, archiving, restoring, unlinking, permanent deletion, `.url`
naming/collision/readonly behaviour, filtered bulk operations, rename, trash naming/collision/readonly behaviour, filtered bulk operations, rename, trash
restore/purge, and external-workspace unavailability; restore/purge, external-workspace unavailability, duplicate-workspace-ID
repair, and direct Linux URL opening without `.url` association;
- normal and debug-mode plugin tab labels; - normal and debug-mode plugin tab labels;
- the corrected frontend Wails mock, deterministic Overview limits/scoping, - the corrected frontend Wails mock, deterministic Overview limits/scoping,
plus the end-to-end flows activity-to-Journal and Inbox-to-Дело. plus the end-to-end flows activity-to-Journal and Inbox-to-Дело.