Compare commits

..

8 Commits

12 changed files with 2350 additions and 1 deletions

View File

@ -46,6 +46,13 @@ official.notes/
"apiVersion": "1", "apiVersion": "1",
"description": "Markdown notes inside Verstak cases.", "description": "Markdown notes inside Verstak cases.",
"source": "official", "source": "official",
"localization": {
"defaultLocale": "en",
"locales": {
"en": "locales/en.json",
"ru": "locales/ru.json"
}
},
"provides": [ "provides": [
"workspace.notes", "workspace.notes",
"entity.note" "entity.note"
@ -85,6 +92,35 @@ official.notes/
} }
``` ```
### 3.1. Локализация
Локализуемый плагин объявляет собственные JSON-каталоги в `localization`.
Пути должны быть относительными, использовать `/` и оставаться внутри каталога
плагина. Все значения каталога — строки. Литеральные английские значения в
manifest остаются fallback, а переводы metadata используют стабильные ключи:
```text
manifest.name
manifest.description
contributions.views.<id>.title
contributions.commands.<id>.title
contributions.statusBarItems.<id>.label
```
Внутренний UI плагина получает текущий язык только через публичный API:
```js
const locale = api.i18n.getLocale();
const title = api.i18n.t('ui.title', undefined, 'Notes');
const unsubscribe = api.i18n.onDidChangeLocale(nextLocale => {
// Обновить текст без перемонтирования компонента и потери состояния.
});
```
Desktop поддерживает `system`, `en` и `ru`. В режиме `system` локали `ru-*`
выбирают русский язык, остальные — английский. Плагин отвечает за свои
переводы; desktop core не содержит тексты официальных плагинов.
## 4. Capabilities Instead Of Plugin Names ## 4. Capabilities Instead Of Plugin Names
Плагины не должны требовать конкретный плагин, если им нужна способность. Плагины не должны требовать конкретный плагин, если им нужна способность.
@ -306,4 +342,3 @@ official.notes-0.1.0.vpkg
- checksums/signature later. - checksums/signature later.
На первом этапе допустима ручная установка папкой в plugin directory. На первом этапе допустима ручная установка папкой в plugin directory.

View File

@ -41,6 +41,9 @@ Implemented:
- browser inbox local receiver and minimal official Browser Inbox plugin; - browser inbox local receiver and minimal official Browser Inbox plugin;
- sync server with device/user auth and operation push/pull; - sync server with device/user auth and operation push/pull;
- SDK manifest/types/schema coverage for current plugin APIs; - SDK manifest/types/schema coverage for current plugin APIs;
- persisted System/English/Russian application language selection, localized
desktop shell, public `api.i18n` plugin contract, and bilingual catalogs for
all official plugins;
- automated Go, frontend, official plugin, SDK, and real-sync smoke checks. - automated Go, frontend, official plugin, SDK, and real-sync smoke checks.
Known remaining gaps: Known remaining gaps:
@ -67,6 +70,8 @@ Known remaining gaps:
records conversions. Chunked large-file attachment capture remains future records conversions. Chunked large-file attachment capture remains future
work. work.
- Packaging/update/release workflow is not product-grade yet. - Packaging/update/release workflow is not product-grade yet.
- Browser extension UI localization is not yet migrated to the shared
multilingual product policy.
## 4. Implementation Phases ## 4. Implementation Phases

17
LICENSE Normal file
View File

@ -0,0 +1,17 @@
Verstak Documentation
Copyright (C) 2026 Verstak contributors
This program is free software: you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option) any
later version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
details.
The complete license text is available at:
https://www.gnu.org/licenses/agpl-3.0.html
SPDX-License-Identifier: AGPL-3.0-or-later

View File

@ -2,6 +2,10 @@
Сводные архитектурные артефакты платформы Верстак. Сводные архитектурные артефакты платформы Верстак.
> Первый публичный выпуск — alpha. Инструкции для сборки и упаковки каждого
> исполняемого компонента находятся в его README: Desktop, official plugins,
> browser extension и SDK должны собираться из одной release-линейки.
## Содержание ## Содержание
- [Product Vision](01_Product_Vision.md) — что остаётся неизменным - [Product Vision](01_Product_Vision.md) — что остаётся неизменным
@ -33,3 +37,8 @@
| `verstak-sync-server` | Сервер синхронизации | | `verstak-sync-server` | Сервер синхронизации |
| `verstak-browser-extension` | Расширение браузера | | `verstak-browser-extension` | Расширение браузера |
| `verstak-docs` | Документация | | `verstak-docs` | Документация |
## Лицензия
Copyright © 2026 Verstak contributors. Документация распространяется на
условиях [GNU AGPLv3 или новее](LICENSE).

View File

@ -0,0 +1,281 @@
# Platform Localization Implementation Plan
> **Execution note:** implement task-by-task in the current session. Do not use
> subagents. Use TDD for every production boundary and keep commits scoped to a
> single repository.
**Goal:** Add persisted System/Russian/English language selection to Verstak
Desktop, localize the shell and all official plugins, and establish the public
SDK contract used by future multilingual plugins.
**Architecture:** Desktop owns the application preference, locale resolution,
shell catalogs, safe plugin-catalog loading, and a generic runtime bridge. Each
plugin owns its manifest and UI catalogs and accesses them through
`api.i18n`. Manifest literals remain English fallbacks. Locale changes update
mounted UI without remounting plugin components.
**Tech stack:** Go/Wails backend tests, Svelte 4 and plain JavaScript frontend,
TypeScript SDK with Vitest, JSON Schema, browserless Node plugin smoke tests,
Playwright mocked-Wails E2E.
## Global Constraints
- `~/git/verstak` is UI reference only; do not copy its implementation.
- Core must not contain official plugin messages or depend on official plugin
IDs.
- Official and third-party plugins use the same manifest and runtime API.
- Store only `system`, `ru`, or `en`; default to `system`.
- Resolve `ru-*` system locales to `ru`, everything else to `en`.
- Do not remount plugins on locale changes or discard form/editor state.
- Preserve existing unrelated generated Wails binding changes in the desktop
worktree.
- Add tests before production changes and observe each focused test fail before
implementing its boundary.
---
## Task 1: SDK localization contract
**Files:**
- Modify `verstak-sdk/schemas/manifest.json`
- Modify `verstak-sdk/src/types.ts`
- Modify `verstak-sdk/src/plugin-api.ts`
- Modify `verstak-sdk/src/test-utils.ts`
- Modify `verstak-sdk/src/plugin-api.test.ts`
- Generated by build: `verstak-sdk/dist/*`
**Contract produced:**
```ts
interface PluginLocalizationConfig {
defaultLocale: string;
locales: Record<string, string>;
}
interface PluginI18nAPI {
getLocale(): 'ru' | 'en';
t(key: string, params?: Record<string, string | number>, fallback?: string): string;
onDidChangeLocale(listener: (locale: 'ru' | 'en') => void): Unsubscribe;
}
```
- [ ] Add failing schema/type/mock tests for a valid manifest localization
block, required default catalog, safe relative paths, API shape, fallback,
interpolation, and locale-change notification.
- [ ] Run `npm test -- --run src/plugin-api.test.ts` and confirm RED.
- [ ] Add `localization` to the JSON Schema and TypeScript manifest types.
- [ ] Add `i18n` to `VerstakPluginAPI` and configurable locale/messages to
`createMockPluginAPI`.
- [ ] Run focused tests GREEN.
- [ ] Run `npm test`, `npm run lint`, and `npm run build`.
- [ ] Commit only SDK source/schema/generated dist changes.
---
## Task 2: Desktop application language setting
**Files:**
- Modify `verstak-desktop/internal/core/appsettings/manager.go`
- Modify `verstak-desktop/internal/core/appsettings/manager_test.go`
- Modify `verstak-desktop/internal/api/app.go`
- Modify `verstak-desktop/internal/api/app_test.go`
**Behavior:** missing language becomes `system`; valid updates persist; invalid
updates return an error; unrelated settings remain unchanged.
- [ ] Add failing manager tests for default, reload, and preservation.
- [ ] Add failing API tests for `GetAppSettings` and accepted/rejected patches.
- [ ] Run focused Go tests and confirm RED.
- [ ] Add `Language` to `appsettings.Config`, default/load normalization, and
update handling without changing unrelated values.
- [ ] Expose and validate `language` in the Wails API.
- [ ] Run focused tests GREEN.
- [ ] Run `gofmt` on changed Go files and `go test ./internal/core/appsettings
./internal/api`.
---
## Task 3: Plugin localization metadata and safe catalog loading
**Files:**
- Modify `verstak-desktop/internal/core/plugin/plugin.go`
- Modify `verstak-desktop/internal/core/plugin/plugin_test.go`
- Modify `verstak-desktop/internal/api/app.go`
- Modify `verstak-desktop/internal/api/app_test.go`
- Generated Wails bindings are intentionally not regenerated over existing
user changes; frontend imports call the binding only after generation is
explicitly reconciled.
**Backend API:**
```go
GetPluginLocalization(pluginID, locale string) (map[string]string, string)
```
- [ ] Add failing manifest parsing tests for localization metadata.
- [ ] Add failing API tests for declared catalog reads, locale fallback input,
malformed JSON, non-string values, missing declarations, absolute paths,
backslashes, traversal, and containment.
- [ ] Run focused Go tests and confirm RED.
- [ ] Add localization manifest structs and the read-only safe catalog API.
- [ ] Run focused tests GREEN and `go test ./internal/core/plugin ./internal/api`.
---
## Task 4: Frontend localization service
**Files:**
- Add `verstak-desktop/frontend/src/lib/i18n/catalogs/en.js`
- Add `verstak-desktop/frontend/src/lib/i18n/catalogs/ru.js`
- Add `verstak-desktop/frontend/src/lib/i18n/index.js`
- Add `verstak-desktop/frontend/tests/i18n-test.mjs`
- Modify `verstak-desktop/frontend/src/main.js`
- Modify `verstak-desktop/frontend/src/lib/test/wails-mock.js`
- [ ] Add a failing standalone Node test for preference validation, system
locale resolution, interpolation, shell fallback, plugin catalog fallback,
subscription, and contribution-copy localization.
- [ ] Run `node frontend/tests/i18n-test.mjs` and confirm RED.
- [ ] Implement the framework-independent locale store and catalog cache.
- [ ] Initialize it from `GetAppSettings` before mounting Svelte.
- [ ] Extend the Wails mock with language state and plugin catalogs.
- [ ] Run the focused test GREEN and `npm run build`.
---
## Task 5: Runtime `api.i18n` bridge
**Files:**
- Modify `verstak-desktop/frontend/src/lib/plugin-host/VerstakPluginAPI.js`
- Modify `verstak-desktop/frontend/src/lib/plugin-host/PluginBundleHost.svelte`
- Modify `verstak-desktop/frontend/tests/plugin-api-contributions-test.mjs`
- Modify `verstak-desktop/frontend/tests/bundle-host-test.cjs`
- [ ] Extend bridge tests first to require `getLocale`, `t`, and disposable
`onDidChangeLocale` behavior.
- [ ] Confirm focused RED.
- [ ] Preload the plugin catalog before mount and expose the i18n service
through the public API.
- [ ] Keep locale subscriptions under existing API disposal cleanup.
- [ ] Translate host loading/error states through shell catalogs.
- [ ] Confirm bridge/bundle tests GREEN.
---
## Task 6: Language selector and shell migration
**Files:**
- Modify `verstak-desktop/frontend/src/App.svelte`
- Modify every Svelte file under `verstak-desktop/frontend/src/lib/shell/`
- Modify every Svelte file under
`verstak-desktop/frontend/src/lib/plugin-manager/`
- Modify `verstak-desktop/frontend/src/lib/plugin-host/PluginBundleHost.svelte`
- Modify `verstak-desktop/frontend/e2e/status-bar.spec.js`
- Add `verstak-desktop/frontend/e2e/localization.spec.js`
- Modify `verstak-desktop/frontend/tests/shell-source-contract-test.mjs` where
assertions intentionally depend on translated literals.
- [ ] Add failing E2E assertions for the language submenu/radio state,
persistence, Russian system locale, English fallback, and live shell update.
- [ ] Confirm focused Playwright RED.
- [ ] Add the language submenu to StatusBar and persist changes through
`UpdateAppSettings`.
- [ ] Replace shell user-facing literals with shell catalog keys, including
aria labels, tooltips, empty/loading/error/confirmation text.
- [ ] Localize copies of plugin manifests and contributions before display.
- [ ] Confirm focused E2E GREEN, standalone shell tests GREEN, and frontend
production build GREEN.
---
## Task 7: Official plugin localization declarations and catalog checks
**Files:**
- Modify every `verstak-official-plugins/plugins/*/plugin.json`
- Add `locales/en.json` and `locales/ru.json` under every official plugin
- Add `verstak-official-plugins/scripts/check-locales.mjs`
- Modify `verstak-official-plugins/scripts/check.sh`
- Modify relevant plugin smoke harness API mocks under
`verstak-official-plugins/scripts/`
- [ ] Add the locale checker first and confirm it fails for missing metadata
and catalogs.
- [ ] Add localization declarations to all 13 manifests.
- [ ] Add English and Russian manifest/contribution keys to all catalogs.
- [ ] Verify catalog parity, string-only values, safe paths, and required
derived manifest keys.
- [ ] Run `./scripts/check.sh` GREEN before migrating runtime UI strings.
---
## Task 8: Plain-JavaScript official plugin UI migration
**Files:**
- Modify `frontend/src/index.js` for activity, browser-inbox, default-editor,
file-preview, files, journal, notes, platform-test, search, secrets, todo, and
trash.
- Expand each plugin's `locales/en.json` and `locales/ru.json`.
- Modify focused `scripts/smoke-*-plugin.js` tests.
For each plugin, repeat independently:
- [ ] Add a failing bilingual rendering assertion and a locale-change state
preservation assertion to its existing smoke test.
- [ ] Add a local `t` helper backed only by `api.i18n`.
- [ ] Replace user-visible literals, attributes, empty/loading/error states,
confirmation prompts, and known validation messages with stable keys.
- [ ] Subscribe once at mount, update only rendered text/state, and unsubscribe
at unmount.
- [ ] Run the focused plugin smoke test GREEN before moving to the next plugin.
Do not translate IDs, paths, user data, logs, API labels in diagnostics, or
provider-returned values.
---
## Task 9: Svelte Sync plugin migration
**Files:**
- Modify `verstak-official-plugins/plugins/sync/frontend/src/SyncSettings.svelte`
- Modify `verstak-official-plugins/plugins/sync/frontend/src/SyncStatusBar.svelte`
- Modify `verstak-official-plugins/plugins/sync/frontend/src/index.js`
- Expand `verstak-official-plugins/plugins/sync/locales/en.json`
- Expand `verstak-official-plugins/plugins/sync/locales/ru.json`
- Modify Sync smoke/E2E tests as appropriate.
- [ ] Add failing English/Russian and live-change assertions.
- [ ] Bridge `api.i18n` into reactive Svelte state without remounting.
- [ ] Translate UI labels and known status/error chrome.
- [ ] Run Sync plugin build and focused tests GREEN.
---
## Task 10: Full verification and documentation
**Files:**
- Modify `verstak-docs/04_Plugin_System.md`
- Modify `verstak-docs/07_Full_Implementation_Roadmap.md`
- Modify other contract docs only where the implemented behavior requires it.
- [ ] Run SDK: `npm test`, `npm run lint`, `npm run build`.
- [ ] Run desktop backend: `go test -count=1 ./...` and `go vet ./...`.
- [ ] Run desktop standalone frontend tests, production build, and focused then
full `npm run test:e2e`.
- [ ] Run official plugins: `./scripts/check.sh` and all plugin builds.
- [ ] Run `git diff --check` in every changed repository.
- [ ] Inspect changed-file lists and ensure pre-existing Wails generated changes
are not staged or overwritten.
- [ ] Update docs to match only verified behavior.
- [ ] Commit scoped changes in each repository.
- [ ] Report exact verification labels. If native Wails/WebKit was not exercised,
state: `GUI behavior was not verified in the real desktop shell.`

View File

@ -0,0 +1,240 @@
# Alpha Activity And Journal Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox syntax for tracking.
**Goal:** Make Activity a durable, background-maintained session log that safely proposes Journal entries without creating a Дело automatically.
**Architecture:** Desktop adds a plugin background-service lifecycle, unsubscribe-capable event bridge, and append-only Activity log. The Activity plugin owns session reconstruction and watermarks; Journal receives an explicit reviewed candidate and requires a destination for unassigned sessions.
**Tech Stack:** Go, Wails, Svelte, plain JavaScript plugin bundles, Go tests, Node smoke tests, Playwright.
## Global Constraints
- Activity raw events are retained for 60 days, 10,000 events, or 8 MiB, whichever is reached first.
- Raw activity uses append/compaction, not settings.json rewrites.
- Session scope is workspaceId plus path cache or unassigned.
- Point-event duration is capped at 10 minutes per adjacent pair; gap over 20 minutes starts another session.
- Accepted/dismissed watermarks consume only the reviewed slice.
- Journal save is always an explicit user action.
---
### Task 1: Add background services, event unsubscription, and an append-only Activity log
**Files:**
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/plugin/plugin.go
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/plugin/plugin_test.go
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/events/bus.go
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app.go
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app_test.go
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/storage/api.go
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/storage/api_test.go
- Create: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/plugin-host/BackgroundPluginHost.svelte
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/App.svelte
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/plugin-host/VerstakPluginAPI.js
- Modify: /home/mirivlad/git/verstak2/verstak-sdk/src/types.ts
- Modify: /home/mirivlad/git/verstak2/verstak-sdk/schemas/contributions.json
- Modify: /home/mirivlad/git/verstak2/verstak-sdk/schemas/manifest.json
- Modify: /home/mirivlad/git/verstak2/verstak-sdk/src/plugin-api.test.ts
**Interfaces:**
- Adds contributes.backgroundServices with id and component fields.
- Bus Subscribe returns subscription ID; Unsubscribe removes only that handler.
- Adds ActivityLogAppend, ActivityLogRead, ActivityLogDeleteWorkspace, and ActivityLogCompact backend methods.
- Background bundle registration exposes start(api) returning a cleanup function.
- [ ] **Step 1: Write failing manifest, bus, and storage tests**
Add a manifest validation test and isolated bus-unsubscribe test:
~~~go
subA := bus.Subscribe("browser.activity.domain", handlerA)
subB := bus.Subscribe("browser.activity.domain", handlerB)
bus.Unsubscribe("browser.activity.domain", subA)
bus.Publish(events.Event{Name: "browser.activity.domain"})
if gotA != 0 || gotB != 1 { t.Fatalf("wrong selective unsubscribe: %d %d", gotA, gotB) }
~~~
Add an append test asserting two appends create two NDJSON records and a
compaction test keeping only the newest record under a small test limit.
- [ ] **Step 2: Run red**
Run:
~~~bash
cd /home/mirivlad/git/verstak2/verstak-desktop
GOCACHE=/tmp/verstak-go-cache go test ./internal/core/plugin ./internal/core/events ./internal/core/storage ./internal/api -run 'Test.*(Background|Unsubscribe|ActivityLog)' -count=1
~~~
Expected: FAIL because subscriptions have no IDs and the log API does not exist.
- [ ] **Step 3: Implement the platform boundary**
Define:
~~~go
type ContributionBackgroundService struct {
ID string `json:"id"`
Component string `json:"component"`
}
type ActivityLogRecord struct {
ActivityID string `json:"activityId"`
Payload json.RawMessage `json:"payload"`
}
~~~
Append NDJSON under plugin-data/verstak.activity/events.ndjson using a locked
append. Compact to the declared retention limits by rewriting a temporary file.
Keep only compact candidate state in plugin data. BackgroundPluginHost loads
enabled plugins with a background contribution once at app startup and calls the
registered start function; it calls cleanup on disable/reload/destroy.
- [ ] **Step 4: Run green and commit**
Run:
~~~bash
GOCACHE=/tmp/verstak-go-cache go test ./internal/core/plugin ./internal/core/events ./internal/core/storage ./internal/api -count=1
git add internal/core/plugin internal/core/events internal/core/storage internal/api frontend/src/App.svelte frontend/src/lib/plugin-host
git commit -m "feat: add background activity runtime"
git -C /home/mirivlad/git/verstak2/verstak-sdk add src/types.ts schemas/contributions.json schemas/manifest.json src/plugin-api.test.ts
git -C /home/mirivlad/git/verstak2/verstak-sdk commit -m "feat: define plugin background services"
~~~
Expected: focused tests prove one subscription, selective cleanup, append, and compaction.
### Task 2: Rebuild Activity sessions and persistent candidate watermarks
**Files:**
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/activity/plugin.json
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/activity/frontend/src/index.js
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/activity/locales/en.json
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/activity/locales/ru.json
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/scripts/smoke-activity-plugin.js
**Interfaces:**
- Activity background service registers ActivityService.start(api).
- Event record includes activityId, scope, occurredAt, durationSeconds, and sessionId.
- Candidate contains sessionId, handledThrough, estimatedMinutes, dateSlices, and source activities.
- [ ] **Step 1: Write failing smoke scenarios**
Add three explicit scenarios:
~~~js
const nineMinutes = eventsAt('2026-07-12T10:00:00Z', '2026-07-12T10:09:00Z');
assert.equal(buildSessions(nineMinutes)[0].estimatedMinutes, 9);
const late = appendLateEvent(existingSession, eventAt('2026-07-12T09:58:00Z'));
assert.equal(late.sessionId, existingSession.sessionId);
assert.equal(candidateAfterDismiss.sourceActivityIds.includes('a'), false);
~~~
Also assert a 23:50 to 00:30 session has one session ID and date slices for both
local dates.
- [ ] **Step 2: Run red**
Run:
~~~bash
cd /home/mirivlad/git/verstak2/verstak-official-plugins
node scripts/smoke-activity-plugin.js
~~~
Expected: FAIL because Activity derives candidate IDs from first and last events.
- [ ] **Step 3: Implement background session state**
Replace view-mounted event recording with ActivityService.start(api). Persist
immutable generated session IDs and anchors in activity-state.json. Build scope
as either workspaceId plus root path or unassigned. Sum explicit browser
duration and only zero-duration adjacent point intervals, cap a point interval
at 10 minutes, split at 20 minutes, and cap a session at 120 minutes.
For accept or dismiss, store the ordered handledThrough watermark. New events
after the watermark require another 10 minutes before a new candidate appears;
late events at or before the watermark remain diagnostic only.
- [ ] **Step 4: Run green and commit**
Run:
~~~bash
node scripts/smoke-activity-plugin.js
./scripts/check.sh
git add plugins/activity scripts/smoke-activity-plugin.js
git commit -m "feat: persist activity sessions and watermarks"
~~~
Expected: session, late-event, dismissal, browser-duration, and midnight smoke tests pass.
### Task 3: Review candidates safely in Journal and expose sessions in UI
**Files:**
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/activity/frontend/src/index.js
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/journal/frontend/src/index.js
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/journal/locales/en.json
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/journal/locales/ru.json
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/scripts/smoke-journal-plugin.js
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/e2e/activity.spec.js
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/e2e/todo.spec.js
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/test/wails-mock.js
**Interfaces:**
- Journal candidate request accepts destinationWorkspaceId only for unassigned scope.
- Saved Journal entry stores sourceSessionId and handledThrough in addition to activity IDs.
- Activity UI has Sessions default and Events diagnostic view.
- [ ] **Step 1: Write failing E2E and smoke assertions**
Require an unassigned candidate to show a destination selector and reject Save
until an active workspace ID is selected:
~~~js
await page.getByRole('button', { name: /review/i }).click();
await expect(page.getByText(/choose.*дело/i)).toBeVisible();
await expect(page.getByRole('button', { name: /save/i })).toBeDisabled();
~~~
Require the mock WritePluginSetting path to retain sourceSessionId after reload.
Require Clear Activity and Journal Delete to show cancellation-safe confirmation
before changing stored rows.
- [ ] **Step 2: Run red**
Run:
~~~bash
cd /home/mirivlad/git/verstak2/verstak-desktop/frontend
npm run test:e2e -- activity.spec.js todo.spec.js
~~~
Expected: FAIL because the current mock discards settings and Journal only accepts path-scoped candidates.
- [ ] **Step 3: Implement review UI and local dates**
Render domain rows as hostname plus duration without URLs. Keep raw event
details in the Events tab. For unassigned scope, list active workspace IDs and
require a selection before handing the candidate to Journal. Preselect the
largest date slice, show both date slices, use local date conversion, and save
the session watermark with the Journal entry. Show a visible action if Journal
is disabled instead of silently dropping the request. Put a confirm/cancel
dialog in front of Clear Activity and Journal Delete; cancelling leaves the
append log, candidate state, and entry list untouched.
- [ ] **Step 4: Run green and commit**
Run:
~~~bash
cd /home/mirivlad/git/verstak2/verstak-official-plugins && node scripts/smoke-activity-plugin.js && node scripts/smoke-journal-plugin.js
cd /home/mirivlad/git/verstak2/verstak-desktop/frontend && npm run test:e2e -- activity.spec.js todo.spec.js
git -C /home/mirivlad/git/verstak2/verstak-official-plugins add plugins/activity plugins/journal scripts/smoke-activity-plugin.js scripts/smoke-journal-plugin.js
git -C /home/mirivlad/git/verstak2/verstak-official-plugins commit -m "feat: review activity sessions in journal"
git -C /home/mirivlad/git/verstak2/verstak-desktop add frontend/e2e/activity.spec.js frontend/e2e/todo.spec.js frontend/src/lib/test/wails-mock.js
git -C /home/mirivlad/git/verstak2/verstak-desktop commit -m "test: cover activity journal review flow"
~~~
Expected: candidate-to-Journal, local date, and persisted mock workflows pass.

View File

@ -0,0 +1,216 @@
# Alpha Browser Domain Activity Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox syntax for tracking.
**Goal:** Add explicitly consented, privacy-minimal active-tab domain timing to the browser extension and deliver immutable activity batches to Desktop.
**Architecture:** Manual captures remain unchanged. A new tracker stores mutable accumulators, immutable pending batches, and acknowledged IDs locally; Desktop accepts a separate authenticated activity endpoint and emits browser.activity.domain.
**Tech Stack:** Plain WebExtension JavaScript, Node tests, Go HTTP handler tests, shared JSON vectors.
## Global Constraints
- Passive tracking defaults to disabled and requires explicit informed consent.
- Track only normalized hostname plus duration for an active tab in a focused window.
- Never send URL, title, text, page content, keystrokes, or history.
- A pending batch is byte-for-byte immutable; acknowledge only its own ID.
- Discard ambiguous or negative clock deltas and gaps over 10 minutes.
- Use hostname-normalization-v1 for extension, Desktop receiver, exclusions, and bindings.
---
### Task 1: Define and test canonical hostname normalization
**Files:**
- Create: /home/mirivlad/git/verstak2/verstak-sdk/schemas/hostname-normalization-v1.json
- Create: /home/mirivlad/git/verstak2/verstak-browser-extension/shared/hostname.js
- Create: /home/mirivlad/git/verstak2/verstak-browser-extension/shared/hostname-normalization-v1.json
- Create: /home/mirivlad/git/verstak2/verstak-browser-extension/scripts/test-hostname.js
- Create: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/hostname/normalize.go
- Create: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/hostname/normalize_test.go
- Create: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/hostname/testdata/hostname-normalization-v1.json
**Interfaces:**
- Produces JavaScript normalizeHostnameV1(value, mode).
- Produces Go hostname.NormalizeV1(value string, mode Mode) (string, error).
- Mode is URLSource for HTTP(S) URLs and BareHost for binding/exclusion input.
- [ ] **Step 1: Write the shared vector corpus and failing tests**
Include vectors for Unicode/punycode, trailing dot, port stripping/rejection,
IPv4, bracketed IPv6, localhost, one-label internal names, malformed labels,
and an overlong name:
~~~json
[
{"mode":"bare","input":"пример.рф.","want":"xn--e1afmkfd.xn--p1ai"},
{"mode":"url","input":"https://Example.COM:8443/a","want":"example.com"},
{"mode":"bare","input":"example.com:8443","error":"port"},
{"mode":"bare","input":"[::1]","want":"::1"}
]
~~~
- [ ] **Step 2: Run red**
Run:
~~~bash
cd /home/mirivlad/git/verstak2/verstak-browser-extension
node scripts/test-hostname.js
cd /home/mirivlad/git/verstak2/verstak-desktop
GOCACHE=/tmp/verstak-go-cache go test ./internal/core/hostname -count=1
~~~
Expected: both fail because the normalizers do not exist.
- [ ] **Step 3: Implement both normalizers**
Use the browser URL parser plus ASCII IDNA conversion in JavaScript. Use
net/url, net/netip, and golang.org/x/net/idna.Lookup.ToASCII in Go. Store
ASCII A-labels, lowercase names, no ports or IPv6 brackets, and reject malformed
or over-limit values.
- [ ] **Step 4: Run green and commit**
Run:
~~~bash
cd /home/mirivlad/git/verstak2/verstak-browser-extension && npm test
cd /home/mirivlad/git/verstak2/verstak-desktop && GOCACHE=/tmp/verstak-go-cache go test ./internal/core/hostname -count=1
cmp /home/mirivlad/git/verstak2/verstak-sdk/schemas/hostname-normalization-v1.json /home/mirivlad/git/verstak2/verstak-browser-extension/shared/hostname-normalization-v1.json
cmp /home/mirivlad/git/verstak2/verstak-sdk/schemas/hostname-normalization-v1.json /home/mirivlad/git/verstak2/verstak-desktop/internal/core/hostname/testdata/hostname-normalization-v1.json
git -C /home/mirivlad/git/verstak2/verstak-sdk add schemas/hostname-normalization-v1.json
git -C /home/mirivlad/git/verstak2/verstak-sdk commit -m "feat: define hostname normalization vectors"
git -C /home/mirivlad/git/verstak2/verstak-browser-extension add shared/hostname.js shared/hostname-normalization-v1.json scripts/test-hostname.js
git -C /home/mirivlad/git/verstak2/verstak-browser-extension commit -m "feat: normalize activity hostnames"
git -C /home/mirivlad/git/verstak2/verstak-desktop add internal/core/hostname
git -C /home/mirivlad/git/verstak2/verstak-desktop commit -m "feat: normalize browser hostnames"
~~~
Expected: every copied corpus passes in both implementations.
### Task 2: Add a separate authenticated Desktop activity receiver
**Files:**
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/browserreceiver/receiver.go
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/browserreceiver/receiver_test.go
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app.go
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app_test.go
**Interfaces:**
- Accepts POST /activities with BrowserDomainActivity.
- Emits browser.activity.domain with normalized hostname, duration, ID, and optional workspaceId.
- Returns status accepted and activityId for first delivery and duplicate delivery.
- [ ] **Step 1: Write failing handler tests**
Create a valid immutable request and then retry the same ID:
~~~go
payload := "{\"schemaVersion\":1,\"activityId\":\"a-1\",\"hostname\":\"пример.рф\",\"durationSeconds\":300,\"startedAt\":\"2026-07-12T10:00:00Z\",\"endedAt\":\"2026-07-12T10:05:00Z\"}"
req := httptest.NewRequest(http.MethodPost, "/activities", strings.NewReader(payload))
req.Header.Set("X-Verstak-Receiver-Token", "pair-token")
~~~
Assert invalid tokens, URL-like hostnames, zero or oversize duration, and a
duplicate ID do not publish a second event.
- [ ] **Step 2: Run red**
Run:
~~~bash
cd /home/mirivlad/git/verstak2/verstak-desktop
GOCACHE=/tmp/verstak-go-cache go test ./internal/core/browserreceiver -run Activity -count=1
~~~
Expected: FAIL because /activities is not routed.
- [ ] **Step 3: Implement the bounded idempotent route**
Add BrowserDomainActivity, a 30-day bounded receiver ID cache, and
handleActivity which invokes hostname.NormalizeV1. Do not reuse capture types
or persistence. In App, resolve an active exact hostname binding to workspaceId
and a path cache before Activity receives the event.
- [ ] **Step 4: Run green and commit**
Run:
~~~bash
GOCACHE=/tmp/verstak-go-cache go test ./internal/core/browserreceiver ./internal/api -count=1
git add internal/core/browserreceiver/receiver.go internal/core/browserreceiver/receiver_test.go internal/api/app.go internal/api/app_test.go
git commit -m "feat: receive browser domain activity"
~~~
Expected: authentication, normalization, idempotency, and binding tests pass.
### Task 3: Build the opt-in extension tracker and immutable delivery queue
**Files:**
- Create: /home/mirivlad/git/verstak2/verstak-browser-extension/shared/activity-tracker.js
- Create: /home/mirivlad/git/verstak2/verstak-browser-extension/scripts/test-activity-tracker.js
- Modify: /home/mirivlad/git/verstak2/verstak-browser-extension/shared/api.js
- Modify: /home/mirivlad/git/verstak2/verstak-browser-extension/shared/background.js
- Modify: /home/mirivlad/git/verstak2/verstak-browser-extension/shared/popup/popup.html
- Modify: /home/mirivlad/git/verstak2/verstak-browser-extension/shared/popup/popup.js
- Modify: /home/mirivlad/git/verstak2/verstak-browser-extension/shared/popup/popup.css
- Modify: /home/mirivlad/git/verstak2/verstak-browser-extension/shared/locales/en.json
- Modify: /home/mirivlad/git/verstak2/verstak-browser-extension/shared/locales/ru.json
- Modify: /home/mirivlad/git/verstak2/verstak-browser-extension/chromium/manifest.json
- Modify: /home/mirivlad/git/verstak2/verstak-browser-extension/firefox/manifest.json
**Interfaces:**
- Persists activeAccumulator, pendingBatches, and acknowledgedIds under verstak.activityTracker.
- Produces sendActivity(receiverUrl, token, immutablePayload).
- Settings include passiveActivityEnabled false and excludedDomains empty.
- [ ] **Step 1: Write failing state-machine tests**
Cover consent, active-tab-only timing, long-gap discard, and the A/B batch case:
~~~js
tracker.addElapsed('example.com', 600, start, end);
const batchA = tracker.freeze('example.com');
tracker.addElapsed('example.com', 300, laterStart, laterEnd);
assert.equal(tracker.pendingBatches[0].payload.durationSeconds, 600);
tracker.acknowledge(batchA.activityId);
assert.equal(tracker.activeAccumulator['example.com'].durationSeconds, 300);
~~~
- [ ] **Step 2: Run red**
Run:
~~~bash
cd /home/mirivlad/git/verstak2/verstak-browser-extension
node scripts/test-activity-tracker.js
~~~
Expected: FAIL because the state machine is absent.
- [ ] **Step 3: Implement tracker, disclosure, and listeners**
Implement the tracker as pure functions. In background.js, register
tabs.onActivated, tabs.onUpdated, windows.onFocusChanged, idle.onStateChanged,
and a five-minute alarm only while opted in. Set idle detection to 600 seconds.
On a gap over 600 seconds, clock rollback, lock, or browser startup, reset the
active checkpoint without adding time. Retry frozen batches oldest-first and
remove only matching IDs.
Show an unchecked Russian/English consent explanation in settings. Add windows,
alarms, and idle permissions to both manifests. Keep manual capture queue and
popup actions unchanged.
- [ ] **Step 4: Run green and commit**
Run:
~~~bash
npm test
node scripts/test-activity-tracker.js
git add shared chromium/manifest.json firefox/manifest.json scripts/test-activity-tracker.js
git commit -m "feat: add opt-in browser domain tracker"
~~~
Expected: tracker unit tests and existing extension tests pass.

View File

@ -0,0 +1,229 @@
# Alpha Browser Inbox Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox syntax for tracking.
**Goal:** Preserve a manually captured link when it leaves the global Inbox, give Browser Inbox a usable archive, and make links durable across case lifecycle changes.
**Architecture:** A capture remains one canonical record. Its global archive state is independent of its workspaceId relation; domain bindings use the same stable identity. Browser Inbox owns capture UI and migration, while Desktop exposes a narrow URL-open capability for user-initiated links.
**Tech Stack:** Plain JavaScript plugin bundle, Go Wails API, Node smoke tests, Playwright.
## Global Constraints
- Only manual extension actions create Browser Inbox captures.
- Captures have at most one optional case assignment in this alpha.
- Remove from global Inbox archives; only Delete everywhere is permanent.
- Assignment and binding use workspaceId, never path as identity.
- Create .url files through files.write and never overwrite a name silently.
- URL opening is direct HTTP(S) opening, not a Linux .url association.
---
### Task 1: Migrate Browser Inbox records and implement archive state
**Files:**
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/browser-inbox/frontend/src/index.js
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/browser-inbox/locales/en.json
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/browser-inbox/locales/ru.json
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/scripts/smoke-browser-inbox-plugin.js
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app.go
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app_test.go
**Interfaces:**
- Canonical capture fields: globalState, workspaceId, workspaceState, workspaceRootPath, workspaceTrashId.
- Domain binding value: workspaceId plus workspaceRootPath cache plus state.
- Mutations: archive, restore, assign, unassign, deleteEverywhere, migrate.
- [ ] **Step 1: Write failing migration and archive smoke tests**
Use a legacy path assignment and assert it resolves to a stable ID. Test the
original data-loss regression:
~~~js
await inbox.assignWorkspace('capture-1', activeWorkspace);
await inbox.archiveCapture('capture-1');
assert.equal(globalRows().some(row => row.captureId === 'capture-1'), false);
assert.equal(workspaceRows(activeWorkspace.workspaceId).some(row => row.captureId === 'capture-1'), true);
await inbox.restoreCapture('capture-1');
assert.equal(globalRows().some(row => row.captureId === 'capture-1'), true);
~~~
- [ ] **Step 2: Run red**
Run:
~~~bash
cd /home/mirivlad/git/verstak2/verstak-official-plugins
node scripts/smoke-browser-inbox-plugin.js
~~~
Expected: FAIL because archive and assignment are currently one mutable path field.
- [ ] **Step 3: Implement canonical state and idempotent migration**
Normalize every capture on read. Set legacy captures globalState active. Resolve
legacy paths through Desktop identities: active marker gives workspaceId,
otherwise set workspaceState unavailable. Store global captures once; workspace
views filter the canonical collection by workspaceId rather than duplicate
storage keys. Reject permanent deletion without an explicit confirm token from
the UI.
- [ ] **Step 4: Run green and commit**
Run:
~~~bash
node scripts/smoke-browser-inbox-plugin.js
./scripts/check.sh
git -C /home/mirivlad/git/verstak2/verstak-official-plugins add plugins/browser-inbox scripts/smoke-browser-inbox-plugin.js
git -C /home/mirivlad/git/verstak2/verstak-official-plugins commit -m "feat: preserve archived browser inbox captures"
git -C /home/mirivlad/git/verstak2/verstak-desktop add internal/api/app.go internal/api/app_test.go
git -C /home/mirivlad/git/verstak2/verstak-desktop commit -m "feat: migrate browser inbox relations"
~~~
Expected: migration, archive, restore, and assignment tests pass.
### Task 2: Handle rename, Trash, restore, purge, and archive UI
**Files:**
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/browser-inbox/frontend/src/index.js
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/scripts/smoke-browser-inbox-plugin.js
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/e2e/browser-inbox.spec.js
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/test/wails-mock.js
**Interfaces:**
- Receives workspace.renamed, workspace.trashed, workspace.restored, and workspace.purged.
- Status filter values are active, archive, all.
- Archive bulk action is restore; bulk permanent delete is not offered.
- [ ] **Step 1: Write failing lifecycle and UI tests**
Assert a rename changes only path cache, trash disables routing, restore restores
the same workspace ID, purge makes captures unassigned and bindings orphaned:
~~~js
await emitWorkspaceEvent('workspace.trashed', { workspaceId: 'w-1', trashId: 't-1' });
assert.equal(capture.workspaceState, 'trashed');
assert.equal(domainBinding('client.example').state, 'trashed');
await emitWorkspaceEvent('workspace.purged', { workspaceId: 'w-1', trashId: 't-1' });
assert.equal(capture.workspaceState, 'unassigned');
assert.equal(domainBinding('client.example').state, 'orphaned');
~~~
In Playwright, select Archive, restore one visible item, and bulk-restore only
the filtered archive rows. Also click Delete everywhere for an assigned capture,
assert the dialog names its assigned Дело, cancel, and assert that the global
and assigned rows still exist.
- [ ] **Step 2: Run red**
Run:
~~~bash
cd /home/mirivlad/git/verstak2/verstak-desktop/frontend
npm run test:e2e -- browser-inbox.spec.js
~~~
Expected: FAIL because the current UI has only processed filters and destructive Clear.
- [ ] **Step 3: Implement lifecycle and labels**
Subscribe once from the Browser Inbox background service. Update path caches on
rename only when workspaceId matches. On external unavailable, do not route to
the same path again. Add Active, Archive, and All filters; make search apply
inside the selected filter; show Archive badge inside a case; add Restore to
Inbox and visible filtered bulk restore. Rename Clear to an archive action with
count confirmation. Keep Delete everywhere separate and require a dialog that
names the assigned Дело; cancellation must perform no mutation.
- [ ] **Step 4: Run green and commit**
Run:
~~~bash
cd /home/mirivlad/git/verstak2/verstak-official-plugins && node scripts/smoke-browser-inbox-plugin.js
cd /home/mirivlad/git/verstak2/verstak-desktop/frontend && npm run test:e2e -- browser-inbox.spec.js
git -C /home/mirivlad/git/verstak2/verstak-official-plugins add plugins/browser-inbox scripts/smoke-browser-inbox-plugin.js
git -C /home/mirivlad/git/verstak2/verstak-official-plugins commit -m "feat: add browser inbox archive lifecycle"
git -C /home/mirivlad/git/verstak2/verstak-desktop add frontend/e2e/browser-inbox.spec.js frontend/src/lib/test/wails-mock.js
git -C /home/mirivlad/git/verstak2/verstak-desktop commit -m "test: cover browser inbox archive lifecycle"
~~~
Expected: archive filters, restore, rename, Trash, restore, and purge tests pass.
### Task 3: Save and open durable links without file-association reliance
**Files:**
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/permissions/registry.go
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app.go
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app_test.go
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/plugin-host/VerstakPluginAPI.js
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/browser-inbox/plugin.json
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/browser-inbox/frontend/src/index.js
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/scripts/smoke-browser-inbox-plugin.js
**Interfaces:**
- Permission and API: urls.openExternal(url string) for user-initiated HTTP(S) URLs only.
- SaveLinkInCase(captureId, workspaceId, filename) creates Links/name.url with InternetShortcut URL field.
- Collision response offers a proposed unique filename and never overwrites.
- [ ] **Step 1: Write failing API and plugin tests**
Add tests for invalid URL, readonly failure, unique filename, collision, and
direct opener argument:
~~~go
if errStr := app.OpenExternalURL("browser.plugin", "https://example.test/a"); errStr != "" {
t.Fatal(errStr)
}
if got := opener.Arguments[0]; got != "https://example.test/a" {
t.Fatalf("opened %q, want URL not .url path", got)
}
~~~
Add plugin smoke assertions for a title-derived name, Link fallback, and
proposal Link (2).url after a collision.
- [ ] **Step 2: Run red**
Run:
~~~bash
cd /home/mirivlad/git/verstak2/verstak-desktop
GOCACHE=/tmp/verstak-go-cache go test ./internal/api -run 'Test.*OpenExternalURL' -count=1
cd /home/mirivlad/git/verstak2/verstak-official-plugins
node scripts/smoke-browser-inbox-plugin.js
~~~
Expected: FAIL because only file-path external opening exists.
- [ ] **Step 3: Implement safe save and open**
Register urls.openExternal as dangerous. Validate HTTP(S), invoke the existing
OS opener with the URL string, and expose api.urls.openExternal. Browser Inbox
requests that permission, creates Links through files.createFolder and
files.writeText using:
~~~text
[InternetShortcut]
URL=https://example.test/
~~~
Sanitize a 96-character filename stem. On collision show an editable dialog
with proposed name (2).url and Cancel; leave capture state unchanged on errors.
When opening a saved link, parse and validate URL= before invoking the URL API.
- [ ] **Step 4: Run green and commit**
Run:
~~~bash
cd /home/mirivlad/git/verstak2/verstak-desktop && GOCACHE=/tmp/verstak-go-cache go test ./internal/api -count=1
cd /home/mirivlad/git/verstak2/verstak-official-plugins && node scripts/smoke-browser-inbox-plugin.js
git -C /home/mirivlad/git/verstak2/verstak-desktop add internal/core/permissions internal/api frontend/src/lib/plugin-host
git -C /home/mirivlad/git/verstak2/verstak-desktop commit -m "feat: open external URLs safely"
git -C /home/mirivlad/git/verstak2/verstak-official-plugins add plugins/browser-inbox scripts/smoke-browser-inbox-plugin.js
git -C /home/mirivlad/git/verstak2/verstak-official-plugins commit -m "feat: save browser inbox links safely"
~~~
Expected: URL API tests and link save/collision smoke tests pass.

View File

@ -0,0 +1,207 @@
# Alpha Shell UX Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox syntax for tracking.
**Goal:** Make the Desktop overview scoped and actionable, hide diagnostic plugin IDs by default, and remove alpha UI strings that expose implementation noise.
**Architecture:** The shell consumes stable workspace IDs and plugin capabilities rather than hard-coded namespaces. Overview builds deterministic case-scoped projections; debug display is a presentation preference plus a session-only command-line override.
**Tech Stack:** Svelte, Wails app settings, Go tests, Playwright E2E, existing i18n catalogs.
## Global Constraints
- Default UI labels are Russian and use Дела, Входящие, Активности, Журнал.
- A normal tab never shows a plugin ID.
- Settings Debug Show plugin IDs defaults false; Desktop --debug enables it only for that run.
- Overview never leaks an unscoped event into every case.
- Todo entries are included only if todo.workspace capability is loaded.
- No action opens or creates a Дело implicitly.
---
### Task 1: Add effective debug display setting and human plugin tab labels
**Files:**
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/main.go
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/appsettings/manager.go
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/appsettings/manager_test.go
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app.go
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app_test.go
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/shell/WorkspaceHost.svelte
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/plugin-manager/PluginManager.svelte
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/i18n/catalogs/en.js
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/i18n/catalogs/ru.js
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/e2e/plugin-api-bridge.spec.js
**Interfaces:**
- Persisted app setting showPluginIds bool defaults false.
- GetAppSettings returns effectiveShowPluginIds and UpdateAppSettings accepts showPluginIds.
- Runtime --debug sets effectiveShowPluginIds true without persisting it.
- [ ] **Step 1: Write failing app-setting and E2E tests**
Add a Go test proving the setting preserves theme and language, plus a browser
test proving normal mode has no raw ID and debug mode has one:
~~~js
await expect(page.locator('[role="tab"]').filter({ hasText: 'verstak.' })).toHaveCount(0);
await mockAppSettings({ showPluginIds: true });
await expect(page.locator('[role="tab"]').filter({ hasText: 'verstak.default-editor' })).toHaveCount(1);
~~~
- [ ] **Step 2: Run red**
Run:
~~~bash
cd /home/mirivlad/git/verstak2/verstak-desktop
GOCACHE=/tmp/verstak-go-cache go test ./internal/core/appsettings ./internal/api -run 'Test.*PluginIds' -count=1
cd frontend && npm run test:e2e -- plugin-api-bridge.spec.js
~~~
Expected: FAIL because no display preference exists.
- [ ] **Step 3: Implement presentation-only debug state**
Parse --debug before Wails startup and keep it in App runtime state. Merge it
with the persisted bool when returning effective settings. Render the localized
manifest title normally; append the plugin ID only when effectiveShowPluginIds
is true. Add the Debug settings checkbox and localized explanatory copy.
- [ ] **Step 4: Run green and commit**
Run:
~~~bash
GOCACHE=/tmp/verstak-go-cache go test ./internal/core/appsettings ./internal/api -count=1
cd frontend && npm run test:e2e -- plugin-api-bridge.spec.js
git add main.go internal/core/appsettings internal/api frontend/src frontend/e2e/plugin-api-bridge.spec.js
git commit -m "feat: hide plugin IDs outside debug mode"
~~~
Expected: normal and debug labels are both verified.
### Task 2: Implement deterministic case-scoped Overview
**Files:**
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/shell/TodaySurface.svelte
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/shell/WorkspaceHost.svelte
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/i18n/catalogs/en.js
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/i18n/catalogs/ru.js
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/e2e/ux-today.spec.js
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/e2e/ux-p0.spec.js
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/test/wails-mock.js
**Interfaces:**
- Every overview row has workspaceId and lastMeaningfulAt.
- Needs attention maximum is five; Continue work maximum is four; Recent changes maximum is eight.
- Hidden recommendation state is keyed by entity ID and invalidated by later meaningful change.
- [ ] **Step 1: Write failing scope and ranking tests**
Add fixture events for two workspace IDs plus an unscoped event:
~~~js
await seedOverview({
selectedWorkspaceId: 'client-a',
events: [
{ activityId: 'a', workspaceId: 'client-a', type: 'note.saved', occurredAt: now },
{ activityId: 'b', workspaceId: 'client-b', type: 'file.changed', occurredAt: now },
{ activityId: 'global', type: 'workspace.selected', occurredAt: now }
]
});
await expect(page.getByText(/client-b/i)).toHaveCount(0);
await expect(page.locator('[data-overview-section="continue"] [data-overview-item]')).toHaveCount(4);
~~~
Also assert no selection shows only the select/create prompt and up to five
unassigned active Inbox captures.
- [ ] **Step 2: Run red**
Run:
~~~bash
cd /home/mirivlad/git/verstak2/verstak-desktop/frontend
npm run test:e2e -- ux-today.spec.js ux-p0.spec.js
~~~
Expected: FAIL because rowsFor currently accepts untagged events for every workspace.
- [ ] **Step 3: Implement exact overview projections**
Replace path-only filtering with workspaceId equality. Build:
1. Needs attention: ready Journal candidates, active unprocessed captures, then
urgent Todo rows only if todo.workspace capability exists; take five.
2. Continue work: distinct entities updated in 14 days; sort by
lastMeaningfulAt descending and the documented type tie-break; take four.
3. Recent changes: note save/create, file create/rename or last change per
file, saved link, and Journal create in seven days; take eight.
Persist Hide recommendation without changing underlying data. Use localized
empty states and remove English technical strings.
- [ ] **Step 4: Run green and commit**
Run:
~~~bash
npm run test:e2e -- ux-today.spec.js ux-p0.spec.js
git add src/lib/shell/TodaySurface.svelte src/lib/shell/WorkspaceHost.svelte src/lib/i18n frontend/e2e/ux-today.spec.js frontend/e2e/ux-p0.spec.js src/lib/test/wails-mock.js
git commit -m "feat: make overview case-scoped and actionable"
~~~
Expected: limits, scope, empty state, and recommendation hide tests pass.
### Task 3: Verify alpha UX as a user sees it
**Files:**
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/e2e/ux-followup.spec.js
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/scripts/check-gui.sh
**Interfaces:**
- E2E fixture uses effectiveShowPluginIds false by default.
- GUI checker accepts a --debug-labels case.
- [ ] **Step 1: Write the final failing alpha scenarios**
Add a scenario that creates a capture, assigns it, archives it globally, opens
the assigned case, and verifies the capture is still present. Add a session
candidate path to Journal and assert normal tab labels are human-readable.
~~~js
await archiveGlobalCapture('capture-1');
await openWorkspace('client-a');
await expect(page.getByTestId('browser-capture-capture-1')).toBeVisible();
await expect(page.getByRole('tab', { name: /verstak\./i })).toHaveCount(0);
~~~
- [ ] **Step 2: Run red**
Run:
~~~bash
npm run test:e2e -- ux-followup.spec.js
~~~
Expected: FAIL until the prior plans are integrated.
- [ ] **Step 3: Add deterministic GUI smoke commands**
Extend check-gui.sh to start an isolated test vault, run normal and --debug
screens, and save screenshots under frontend/e2e-results. The script must not
read or mutate the user's normal vault.
- [ ] **Step 4: Run green and commit**
Run:
~~~bash
npm run test:e2e -- ux-followup.spec.js
GOCACHE=/tmp/verstak-go-cache ./scripts/check-gui.sh
git add frontend/e2e/ux-followup.spec.js scripts/check-gui.sh
git commit -m "test: cover first alpha UX smoke flow"
~~~
Expected: E2E and isolated GUI smoke pass.

View File

@ -0,0 +1,216 @@
# Alpha Workspace Identity Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox syntax for tracking.
**Goal:** Give every managed Дело a durable UUID identity and publish lifecycle events that let alpha features follow a case across rename, Trash, restore, and path reuse.
**Architecture:** The filesystem remains the source of truth for whether a case folder exists. A UUID marker inside each case is the relation identity; Desktop metadata indexes that marker and paths remain presentation caches.
**Tech Stack:** Go, Wails bindings, JSON metadata, Go tests.
## Global Constraints
- A new Дело is created only by an explicit user action.
- workspaceId is UUID v4; a path is never relation identity.
- The marker is .verstak/workspace.json inside the case.
- Existing writable cases migrate without data loss; non-writable cases are view-only.
- Use TDD and make one commit per task.
---
### Task 1: Store and resolve durable workspace identities
**Files:**
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/workspace/manager.go
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/workspace/manager_test.go
**Interfaces:**
- Produces Workspace.ID string and Metadata.WorkspaceID string.
- Produces func (m *Manager) EnsureWorkspaceIdentity(name string) (WorkspaceIdentity, error).
- Produces func (m *Manager) ListWorkspaceIdentities() ([]WorkspaceIdentity, error).
- [ ] **Step 1: Write the failing identity tests**
Add tests proving creation writes a UUID marker, rename retains it, and a newly
created folder after external removal gets another UUID:
~~~go
ws, err := m.CreateWorkspace("Clients", "default")
if err != nil { t.Fatal(err) }
first, err := m.EnsureWorkspaceIdentity(ws.Name)
if err != nil || first.WorkspaceID == "" { t.Fatalf("identity = %+v, %v", first, err) }
if err := m.RenameWorkspace("Clients", "Clients-2026"); err != nil { t.Fatal(err) }
renamed, _ := m.EnsureWorkspaceIdentity("Clients-2026")
if renamed.WorkspaceID != first.WorkspaceID { t.Fatal("rename changed workspace ID") }
~~~
- [ ] **Step 2: Run the focused test to verify it fails**
Run:
~~~bash
cd /home/mirivlad/git/verstak2/verstak-desktop
GOCACHE=/tmp/verstak-go-cache go test ./internal/core/workspace -run 'Test.*Workspace.*Identity' -count=1
~~~
Expected: FAIL because WorkspaceIdentity and EnsureWorkspaceIdentity do not exist.
- [ ] **Step 3: Write the minimal marker implementation**
Add these types and use them in CreateWorkspace, GetWorkspaceMetadata, and
workspace listing:
~~~go
type WorkspaceIdentity struct {
WorkspaceID string `json:"workspaceId"`
RootPath string `json:"rootPath"`
State string `json:"state"`
}
const workspaceIdentityRelativePath = ".verstak/workspace.json"
func (m *Manager) EnsureWorkspaceIdentity(name string) (WorkspaceIdentity, error) {
// Validate the folder, read the in-folder marker, and atomically create
// a UUID-v4 marker only when absent and writable.
}
~~~
Write the same ID to central metadata only as an index. Never recover an ID from
stale path-keyed metadata when the in-folder marker is absent.
- [ ] **Step 4: Run green and commit**
Run:
~~~bash
GOCACHE=/tmp/verstak-go-cache go test ./internal/core/workspace -count=1
git add internal/core/workspace/manager.go internal/core/workspace/manager_test.go
git commit -m "feat: add durable workspace identities"
~~~
Expected: tests pass and the commit contains only workspace identity changes.
### Task 2: Expose identity and lifecycle events through Desktop
**Files:**
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app.go
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app_test.go
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/App.svelte
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/wailsjs/go/api/App.d.ts
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/wailsjs/go/models.ts
**Interfaces:**
- Produces ListWorkspaceIdentities() ([]workspace.WorkspaceIdentity, string).
- Lifecycle payloads include workspaceId and workspaceRootPath; Trash also includes trashId.
- Produces workspace.restored and workspace.purged events.
- [ ] **Step 1: Add failing API tests**
Extend TestWorkspaceAPIPublishesLifecycleEvents:
~~~go
if got := received["workspace.created"]["workspaceId"]; got == "" {
t.Fatal("workspace.created must include workspaceId")
}
if got := received["workspace.renamed"]["workspaceId"]; got != createdID {
t.Fatalf("rename ID = %v, want %s", got, createdID)
}
~~~
Add restore and purge tests around RestoreVaultTrash and DeleteVaultTrash that
assert the original UUID and trash ID are emitted.
- [ ] **Step 2: Run red**
Run:
~~~bash
GOCACHE=/tmp/verstak-go-cache go test ./internal/api -run 'TestWorkspaceAPI.*Lifecycle' -count=1
~~~
Expected: FAIL because lifecycle payloads only identify a path.
- [ ] **Step 3: Publish complete identity lifecycle**
Resolve the marker before publishing create, rename, selected, and trashed
events. Detect workspace Trash metadata in RestoreVaultTrash and
DeleteVaultTrash, then publish:
~~~go
map[string]interface{}{
"operation": "restore",
"workspaceId": identity.WorkspaceID,
"workspaceRootPath": restoredRoot,
"trashId": trashID,
}
~~~
Change App.svelte workspace-node conversion to preserve workspace.id while using
rootPath only for selection and display.
- [ ] **Step 4: Run green and commit**
Run:
~~~bash
GOCACHE=/tmp/verstak-go-cache go test ./internal/api ./internal/core/workspace -count=1
git add internal/api/app.go internal/api/app_test.go frontend/src/App.svelte frontend/wailsjs/go/api/App.d.ts frontend/wailsjs/go/models.ts
git commit -m "feat: publish workspace identity lifecycle"
~~~
Expected: focused API and workspace tests pass.
### Task 3: Migrate legacy references and repair duplicate IDs
**Files:**
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/workspace/manager.go
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/workspace/manager_test.go
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app.go
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app_test.go
**Interfaces:**
- Produces RepairWorkspaceIdentity(keepRootPath, regenerateRootPath string) string.
- Identity states are active, unavailable, or duplicate.
- Later Inbox and Activity plans consume workspaceId through ListWorkspaceIdentities.
- [ ] **Step 1: Write failing duplicate and legacy tests**
Create two folders with copied marker data and assert neither is an active
relation target. Create a legacy path-keyed capture fixture and assert a
resolved path gets an ID while a missing path becomes unavailable:
~~~go
if errStr := app.RepairWorkspaceIdentity("Original", "Copied"); errStr != "" {
t.Fatal(errStr)
}
if originalID == copiedID { t.Fatal("repair must issue a new ID") }
~~~
- [ ] **Step 2: Run red**
Run:
~~~bash
GOCACHE=/tmp/verstak-go-cache go test ./internal/core/workspace ./internal/api -run 'Test.*(Duplicate|Legacy).*Workspace' -count=1
~~~
Expected: FAIL because copied markers are accepted as ordinary folders.
- [ ] **Step 3: Implement deterministic repair**
Group active roots by marker UUID during identity listing. Mark a group with
more than one root duplicate and exclude it from assignment targets.
RepairWorkspaceIdentity verifies the duplicate, keeps the first marker, creates
a new marker for the second root, and moves no relation data.
- [ ] **Step 4: Run green and commit**
Run:
~~~bash
GOCACHE=/tmp/verstak-go-cache go test ./internal/core/workspace ./internal/api -count=1
git add internal/core/workspace/manager.go internal/core/workspace/manager_test.go internal/api/app.go internal/api/app_test.go
git commit -m "feat: repair duplicate workspace identities"
~~~
Expected: legacy, path-reuse, and duplicate-ID tests pass.

View File

@ -0,0 +1,384 @@
# Platform Localization Design
## Context
Verstak v2 currently renders most desktop shell and official plugin text in
English. Application settings do not store a language, the frontend plugin API
has no localization contract, and plugin manifests expose only literal English
metadata. The sync server already has separate Russian and English catalogs and
is outside the scope of this desktop milestone. The browser extension also has
its own lifecycle and is explicitly deferred to a later milestone.
The first localization milestone covers:
- the `verstak-desktop` shell and core-owned screens;
- every plugin in `verstak-official-plugins`, including `platform-test`;
- the public SDK and manifest contract required by future third-party plugins;
- Russian and English;
- a persisted language selector in the desktop settings menu.
The existing `~/git/verstak` project remains a visual reference only. Its code
and localization architecture are not inputs to this implementation.
## Product Decisions
- The stored language preference is `system`, `ru`, or `en`.
- A new installation defaults to `system`.
- System locales beginning with `ru` resolve to Russian; all other system
locales resolve to English.
- The settings menu offers System, Russian, and English choices.
- Changing the preference updates the running UI without restarting the app.
- The preference is installation-local and is not stored in or synchronized
with a vault.
- Browser extension localization is a separate follow-up.
- Sync server localization remains independent from the desktop preference.
## Alternatives Considered
### Recommended: platform contract with plugin-owned catalogs
The shell owns shell translations. Each plugin owns its catalogs and declares
them in its manifest. The public plugin API exposes the resolved locale,
translation, and locale-change subscription.
Trade-offs:
- preserves repository and plugin ownership boundaries;
- gives third-party plugins the same mechanism as official plugins;
- requires coordinated SDK, desktop, manifest, and plugin changes;
- avoids teaching desktop core the UI vocabulary of official plugins.
### One desktop-wide catalog
Desktop would contain translations for shell and all official plugins.
Trade-offs:
- initially faster;
- couples core releases to plugin text and IDs;
- prevents independently distributed plugins from owning their localization;
- contradicts the dynamic plugin architecture.
### Independent dictionaries without a platform API
Every plugin would read a global value or DOM event and implement translation
on its own.
Trade-offs:
- small initial runtime change;
- inconsistent fallback and interpolation behavior;
- no stable SDK contract for third-party plugins;
- makes live locale changes and manifest metadata unreliable.
## Chosen Architecture
Use the platform contract with plugin-owned catalogs.
Localization is a UI platform service, not a user feature plugin. Desktop owns
language preference resolution, catalog loading, shell translation, and the
generic runtime bridge. It does not own plugin message content.
The data flow is:
```text
app config language preference
|
v
desktop locale store -----> shell catalogs
|
+------------------> localized manifest contributions
|
+------------------> api.i18n for each plugin
|
v
plugin-owned catalogs
```
## Application Setting
Add `Language string` to the desktop application config and expose it as
`language` through `GetAppSettings` and `UpdateAppSettings`.
Accepted persisted values:
```text
system
ru
en
```
Rules:
- missing or empty values load as `system`;
- unknown values are rejected by the update API and never persisted;
- an existing config requires no schema migration beyond applying the missing
default;
- updating language must not reset `devMode`, theme, workbench preferences, or
other unrelated settings;
- `system` is resolved in the frontend from `navigator.languages` or
`navigator.language`, with `en` as the safe fallback.
The settings menu renders language names in their own language so the user can
recover from an accidental switch:
```text
System / Системный
English
Русский
```
The active option is visibly marked and uses menu radio semantics.
## Desktop Localization Service
Add one small frontend localization module that owns:
- the stored preference;
- the resolved locale (`ru` or `en`);
- the English and Russian shell catalogs;
- loaded plugin catalogs keyed by plugin ID and locale;
- a subscription API for Svelte shell components and plugin bridges;
- named parameter interpolation.
The shell-facing API is synchronous after initialization:
```js
getLanguagePreference()
getLocale()
t(key, params?, fallback?)
setLanguagePreference(preference)
subscribe(listener)
loadPluginCatalog(pluginId, localizationConfig)
translatePlugin(pluginId, key, params?, fallback?)
```
Application startup loads the persisted preference before the main interactive
shell is shown. Catalog-loading failures do not prevent startup.
Parameter interpolation supports named placeholders such as `{count}`. Missing
parameters leave the placeholder visible so catalog mistakes are detectable.
Catalog strings are always assigned through existing safe text rendering APIs;
translation values are not treated as HTML.
## Plugin Manifest Contract
Add an optional top-level manifest field:
```json
{
"localization": {
"defaultLocale": "en",
"locales": {
"en": "locales/en.json",
"ru": "locales/ru.json"
}
}
}
```
The field belongs at the top level because manifest metadata and contributions
can be localized even when a plugin has no frontend bundle.
Validation rules:
- `defaultLocale` and locale map keys use lower-case supported locale tags;
- the default locale must have a declared catalog;
- catalog paths must be plugin-relative safe paths;
- absolute paths, traversal, backslashes, and paths outside the plugin root are
rejected;
- localization is optional, preserving compatibility with existing and
third-party plugins.
Catalog files are flat JSON string maps. Official plugins provide both `en`
and `ru` catalogs. The English manifest literals stay in place as readable
fallbacks and for compatibility with hosts that do not implement localization.
## Manifest And Contribution Keys
Desktop derives stable keys rather than embedding translation tokens in fields.
Reserved keys:
```text
manifest.name
manifest.description
contributions.views.<id>.title
contributions.commands.<id>.title
contributions.settingsPanels.<id>.title
contributions.sidebarItems.<id>.title
contributions.fileActions.<id>.title
contributions.noteActions.<id>.title
contributions.contextMenuEntries.<id>.title
contributions.searchProviders.<id>.label
contributions.statusBarItems.<id>.label
contributions.workspaceItems.<id>.title
```
The complete set follows contribution fields defined by the SDK schema. IDs
are used verbatim after the contribution type prefix. Missing keys retain the
literal manifest value.
The desktop localizes copies returned to frontend presentation code; it does
not mutate the backend registry or change IDs, handlers, capabilities, or
permission logic.
## Plugin Runtime API
Extend `VerstakPluginAPI` with:
```ts
i18n: {
getLocale(): 'ru' | 'en';
t(key: string, params?: Record<string, string | number>, fallback?: string): string;
onDidChangeLocale(listener: (locale: 'ru' | 'en') => void): Unsubscribe;
}
```
The desktop preloads a plugin catalog before mounting its component. `t` is
therefore synchronous during rendering. The API automatically disposes locale
subscriptions with the rest of the plugin API.
Official plugin components subscribe once when mounted and rerender only their
text when the locale changes. Locale changes must not clear form values,
selection, dirty editor state, loaded data, or navigation state. The host does
not solve localization by unmounting and remounting plugin components.
Plugins without localization metadata continue to work. Their `t` calls return
the supplied fallback or key, and their literal manifest metadata remains
visible.
## Catalog Loading Boundary
Desktop exposes a dedicated read-only backend method for a declared plugin
catalog. It locates the enabled/discovered plugin, selects only a path declared
by its localization manifest, validates containment within the plugin root, and
returns a parsed string map or an error.
Do not repurpose arbitrary plugin settings or make localization catalogs
writable at runtime. Do not give plugins access to another plugin's catalog.
Catalog errors are isolated:
- malformed selected-locale catalog: fall back to default locale;
- missing default catalog: use manifest literals and key/fallback values;
- one broken plugin catalog: report a diagnostic without affecting other
plugins or the shell.
## Fallback Rules
Shell translation fallback:
```text
resolved locale -> English catalog -> explicit fallback -> key
```
Plugin translation fallback:
```text
resolved locale -> plugin default locale -> explicit fallback -> key
```
Manifest/contribution fallback:
```text
resolved locale -> plugin default locale -> original manifest literal
```
Russian and English catalogs must have the same keys in CI for the shell and
official plugins. Third-party plugins may provide only their default locale.
## Scope Of Translated UI
The first milestone translates:
- vault onboarding and shell navigation;
- status bar, settings menu, command palette, global search chrome, dialogs,
plugin host states, and Plugin Manager;
- all normal, empty, loading, confirmation, warning, and known validation
states in official plugins;
- official plugin names, descriptions, contribution titles, and labels;
- accessibility labels and tooltips owned by shell or official plugins.
Arbitrary backend, operating-system, filesystem, network, and third-party error
messages remain in their source language. The surrounding user-facing prefix
and recovery instruction are translated. Stable known error codes may receive
localized messages in later milestones.
User data, filenames, workspace names, provider values, logs, identifiers, API
names, and developer diagnostics are never translated.
## Testing Strategy
Use TDD for each production change.
### SDK
- manifest schema accepts valid localization declarations;
- schema rejects unsafe or incomplete declarations;
- TypeScript types expose localization metadata and `api.i18n`;
- mock API implements locale reads, translations, and subscriptions;
- interpolation and fallback behavior are covered.
### Desktop backend
- missing language defaults to `system`;
- all three accepted values persist and reload;
- invalid values are rejected;
- language updates preserve unrelated settings;
- catalog reads accept only declared safe paths;
- malformed, missing, and traversal catalog cases fail safely.
### Desktop frontend
- system locale resolution maps `ru-*` to `ru` and others to `en`;
- shell fallback and interpolation are deterministic;
- language menu shows and persists the selected option;
- changing language updates visible shell text without reload;
- plugin API receives the locale and a working catalog;
- contribution labels change without changing contribution identity;
- missing plugin catalogs preserve English manifest literals.
### Official plugins
- every official manifest declares English and Russian catalogs;
- catalog key parity is checked;
- every manifest/contribution localization key is present;
- focused smoke tests exercise English and Russian rendering;
- a mounted component reacts to a locale change without losing its state.
### End-to-end
- first launch with a mocked Russian system locale renders Russian;
- first launch with another locale renders English;
- English -> Russian -> System selection persists through reload;
- shell and representative plain-JS and Svelte plugins switch together;
- plugin enable/disable and failed-plugin isolation still work.
## Rollout Order
1. SDK manifest/types/mock contract.
2. Desktop setting and catalog security boundary.
3. Desktop localization service and language menu.
4. Desktop shell catalog migration.
5. Official plugin manifests, catalogs, and runtime UI migration.
6. Full unit, smoke, build, and mocked-Wails E2E verification.
7. Real Wails/WebKit GUI smoke when the native environment is available.
The work remains split into small commits per repository. Existing unrelated
changes, including generated Wails bindings already present in the desktop
worktree, must not be included.
## Follow-up Work
After this localization milestone:
1. add browser-extension `_locales` catalogs and its independent language
behavior;
2. introduce stable workspace IDs so renames do not orphan plugin scopes;
3. scope sync blobs by tenant/vault and make operation-sequence writes
transactional;
4. make concurrent settings updates lossless;
5. remove remaining hard-coded official plugin IDs and schemas from shell;
6. reduce drift between E2E mock bundles and real plugin packages;
7. include whole-workspace entries in global Trash where appropriate.

View File

@ -0,0 +1,510 @@
# First Alpha Product UX Design
## Status and scope
This is the approved product-UX tranche for the first public alpha. It covers
the Desktop application, official Activity, Journal and Browser Inbox plugins,
and the browser extension. Release packaging, public repository documentation,
licensing, and sync-server hardening are specified separately in the
`alpha-release` tranche.
The user-visible Russian term is **Дело**. Existing platform and storage names
such as `workspaceRootPath` remain internal compatibility details in this
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
designs:
- `2026-06-29-activity-worklog-suggestions-design.md` for candidate lifecycle
and background availability;
- `2026-06-29-browser-inbox-domain-binding-design.md` for the relation between
bindings, global Inbox, and browser activity;
- `2026-07-11-platform-localization-design.md` only where it would leave
developer-facing plugin IDs visible in the normal user interface.
## Product decisions
- A new Дело is created **only** by an explicit user action. Browser Inbox,
Activity, and Journal never create a case, candidate case, or implicit case.
- A manual extension send is a Browser Inbox capture. Passive browsing activity
is never an Inbox capture.
- Passive browser activity records only a normalized hostname and measured
duration. It never records a page URL, title, selection, page content,
keystrokes, or browsing history.
- Passive browser tracking is disabled on a new installation. It starts only
after explicit, informed consent in extension settings.
- Passive browser activity counts only the active tab in a focused browser
window. Background tabs and unfocused browser windows contribute no time.
- Activity can suggest a Journal record, but the user reviews and saves it.
Journal entries are never created automatically.
- An unassigned activity can be saved only after the user picks an existing
Дело. Picking it for a single Journal entry does not silently create a domain
binding.
- The first alpha retains one optional Дело assignment per Browser Inbox
capture, matching current product semantics. It does not introduce sharing a
capture between several cases.
## Browser extension: manual capture and passive activity
### Manual capture
The existing explicit popup/context-menu actions remain the only way to create
`browser.capture.*` events and Browser Inbox records. Their protocol and
retry queue remain separate from passive activity data.
### Domain activity tracker
Passive tracking is an opt-in extension setting, `passiveActivityEnabled`, with
a default of `false`. The first extension settings view includes a concise
consent card and an unchecked switch. It states that the feature sends only a
normalized hostname and duration, and explicitly states that it does **not**
collect or send URLs, titles, page content, selections, keystrokes, or browser
history. The same explanation remains next to the switch after onboarding.
Until the user enables that switch, the extension does not subscribe to
tracking events, create activity state, or send activity records. Disabling it
stops tracking immediately, clears only the mutable unflushed accumulator, and
leaves already acknowledged Desktop activity unchanged. The user can either
retry or discard already-created pending batches through an explicit settings
action; disabling tracking never silently loses them.
When enabled, the extension uses a persisted tracker:
1. When an HTTP(S) page becomes the active tab of a focused browser window,
start timing its normalized lowercase hostname.
2. On tab activation, hostname change, window focus loss, browser idle/lock,
or a five-minute alarm, write a checkpoint and calculate elapsed time. The
idle detection threshold is explicitly ten minutes; a locked state pauses
immediately.
3. Ignore browser-internal pages, invalid URLs, and excluded hostnames. An
exclusion `youtube.com` matches that hostname and every subdomain; the same
rule applies to `x.com`.
4. On a flush, freeze one record per hostname as a `browser.activity.domain`
batch. The payload contains a schema version, idempotency ID, observed
period bounds, hostname, and `durationSeconds`; it contains no URL-like
field.
The persisted state has three distinct parts:
```text
activeAccumulator: mutable, unsent duration and bounds by hostname
pendingBatches: immutable payloads, keyed by idempotency ID
acknowledgedIds: bounded recent acknowledgement IDs
```
Flushing copies a hostname's current accumulator into a new immutable pending
batch with a newly generated ID, then clears only that copied accumulator. New
time for the same hostname accumulates in `activeAccumulator` and can become a
later batch. It never mutates an already sent batch. Retries send the stored
payload byte-for-byte. A successful acknowledgement removes only the matching
pending batch and records its ID; it cannot remove newer time. Pending batches
are sent oldest first. `acknowledgedIds` is a 30-day bounded LRU set used to
handle replayed acknowledgement messages safely; Desktop maintains its own
idempotency store as the authority.
The tracker uses timestamp arithmetic but applies a conservative ambiguity
limit. A checkpoint contributes elapsed time only if wall-clock time is
monotonic and the gap from the previous trustworthy checkpoint is no more than
ten minutes. A negative clock delta, a gap over ten minutes, an idle/locked
state, or a browser startup after a crash discards the ambiguous interval and
establishes a fresh baseline. WebExtensions have no portable system
suspend/resume event, so the first post-suspend observation is deliberately
handled by this gap rule; platform idle/lock events add an earlier pause where
available. This can undercount ambiguous work but cannot turn an overnight
sleep or a clock change into working time. A browser startup never carries an
active interval across process death; pending batches and accumulated completed
intervals are retained.
The extension persists timestamps and the three state parts in local extension
storage, so a Manifest V3 service-worker restart can continue safely. It sets
the `idle` API detection interval to ten minutes. Its manifest gains the
`windows`, `alarms`, and `idle` permissions needed for this flow; existing tab
access is retained. Firefox uses the equivalent WebExtension events when
available and otherwise applies the same ten-minute checkpoint limit.
The settings page gets an **Excluded domains** list. It accepts one hostname per
item through the canonical normalization below, and explains that a hostname
excludes its subdomains. The default list is empty. Adding an exclusion stops
the matching active measurement immediately and discards only its mutable
unflushed time; immutable pending batches remain available for the user's
explicit retry or discard decision.
### Canonical hostname normalization
Every extension event, exclusion, and Desktop domain binding uses
`hostname-normalization-v1`. It is a normative shared contract, not an
implementation detail of one component:
1. An activity source must be an HTTP(S) URL. Its port, user info, path, query,
and fragment are discarded before hostname normalization. A binding or
exclusion must be a bare hostname; schemes, paths, queries, fragments, and
ports are rejected.
2. Trim surrounding whitespace, lowercase, and remove exactly one DNS root
trailing dot. `example.com.` therefore becomes `example.com`.
3. Convert DNS names using non-transitional UTS #46 / IDNA lookup processing to
an ASCII A-label. The canonical stored and compared form of `пример.рф` is
its punycode A-label; the settings UI may render the Unicode display form.
4. IPv4 addresses are accepted in canonical dotted-decimal form. IPv6 literals
are accepted (bracketed only at URL/settings input), stored without brackets
in canonical lower-case RFC 5952 form, and never include a port. `localhost`
and syntactically valid single-label internal names are also accepted.
5. Reject empty values, malformed IP literals, invalid labels, empty labels,
labels over 63 ASCII bytes, DNS names over 253 ASCII bytes, and any input
that fails URL/IDNA parsing. Invalid activity is not counted; invalid
settings input gets an inline validation error and is not saved.
The SDK owns a versioned `hostname-normalization-v1` test-vector corpus. Desktop
and the browser extension vendor the byte-identical corpus in their tests; the
coordinated `build-all` verification checks its hash. Go and JavaScript have
separate implementations, but both must pass every vector, including trailing
dots, Unicode/punycode equivalence, IPv4, IPv6, localhost, internal names,
ports, malformed input, and excessive lengths.
### Desktop receiver
Desktop exposes an authenticated activity receiver separate from the capture
receiver. It normalizes and validates a hostname with
`hostname-normalization-v1`, validates a positive bounded duration, ISO time
fields, schema version, and idempotency ID before it publishes
`browser.activity.domain`. Invalid and duplicate records do not enter Activity.
The existing local pairing token gates the endpoint; the token is never placed
in Activity storage or UI.
The receiver annotates a valid activity with an existing exact hostname-to-Дело
binding when one exists. A binding stores `workspaceId` as identity and the
current root path as display cache. Bindings remain explicit:
`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
### Background processing
Official plugins gain a lifecycle-safe background-service contribution. It is
loaded when the plugin host starts, not only while the plugin view is mounted.
The Activity service subscribes once to public events, normalizes and persists
them, rebuilds candidates, and releases subscriptions on host teardown.
The platform provides unsubscribe-capable event subscriptions. Command
registration is owned by the background service so commands remain available
without opening the Activity view.
Raw Activity data is not held in `settings.json`. The Desktop storage layer
provides a lifecycle-safe, plugin-scoped append-only event log for
`verstak.activity` under plugin data. Appending one event does not rewrite the
whole log. Plugin settings retain only compact preferences; plugin data retains
candidate watermarks and indexes. Appends and compaction are serialized by the
storage layer, preventing lost updates.
### Candidate rules
Activity presents chronological sessions rather than a raw log by default. A
browser activity session displays, for example, `admin.client-site.ru · 1 ч
32 мин`; it exposes no hidden URL/title data.
A logical session has one explicit scope:
```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
least two events; or
- it contains one or more browser-domain records totaling at least ten minutes.
`workspace.selected`, `file.opened`, and `note.opened` are diagnostic context,
not meaningful work by themselves.
Duration is normative rather than the wall-clock span from first to last event.
Sort meaningful events by `{occurredAt, activityId}` within a session:
- 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.
A candidate contains only source events after that watermark:
- **Accepting** a candidate stores its `sessionId` and handled watermark on the
Journal entry. Later activity can create a candidate only for the additional
interval after that watermark and only when that new interval reaches the
normal threshold.
- **Dismissing** consumes the current candidate slice through its watermark.
`A+B` therefore cannot reappear as `A+B+C`; only qualifying new work after
`B` may later be suggested.
- A dismissal is not a permanent ban on future work in the same logical
session. At least ten new meaningful minutes are required before it can be
suggested again.
An event arriving late at or before a handled watermark remains in the raw
diagnostic log but never reopens an accepted or dismissed candidate slice.
An across-midnight logical session remains one candidate. The review displays
its time apportioned by local date, preselects the date containing the largest
share of the duration (ties use the start date), and lets the user choose the
Journal date. This preserves a 23:5000:30 session instead of losing its first
ten minutes to an artificial threshold.
The Journal review action opens the existing journal editor with candidate
duration, date, and a concise domain/event summary. The user may edit all
content and must explicitly save. For global/unassigned activity, the review
requires choosing one existing Дело first. Missing or disabled Journal support
produces a visible, actionable message rather than failing silently.
Activity has two clear views:
- **Сессии** — default, with candidate cards and activity summaries;
- **События** — a secondary diagnostic stream for technical event inspection.
### Retention and deletion
The alpha retains raw Activity events for at most 60 days, 10,000 events, or
8 MiB of log data, whichever limit is reached first. On append and at service
startup, bounded compaction removes the oldest entries until all limits hold;
it does not block the UI. Candidate state is pruned once its session and every
related Journal source watermark are older than 60 days.
Saving a Journal entry does not delete its source Activity events. The Journal
entry retains the compact source session/watermark reference after raw-event
retention removes those events. **Clear activity for this Дело** removes only
that case's raw events, sessions, and candidate state after confirmation.
**Clear all activity** is a separate global confirmed operation.
## Browser Inbox lifecycle
### Record state
A capture remains one canonical record with independent fields:
```text
globalState: active | archived
workspaceId: optional immutable UUID of assigned Дело
workspaceState: unassigned | active | trashed | unavailable | orphaned
workspaceRootPath: optional current or historical Дело path cache
workspaceTrashId: optional stable trash identity
```
Existing records migrate to `globalState: active`. An assignment whose current
path resolves to an active marker receives that marker's `workspaceId`; an
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
- **Assign to Дело** retains the capture in global Inbox and shows it in the
selected Дело's Inbox.
- **Убрать из общих входящих** changes `globalState` to `archived`. It never
removes the assignment, so an assigned capture remains available from its
Дело.
- **Открепить от дела** clears `workspaceId`, `workspaceRootPath`, and
`workspaceTrashId`, and sets `workspaceState` to `unassigned`. If the capture is
still globally active it returns to the global Inbox; otherwise it remains in
archive.
- **Удалить везде** removes the canonical capture and its assignment. It is a
separate destructive action and requires confirmation that names the affected
Дело when assigned.
- **Сохранить ссылку в деле** is available only for a valid HTTP(S) capture URL.
It opens a small save dialog with an editable proposed filename derived from
the capture title, then its hostname, then `Link`. The stem is sanitized for
cross-platform forbidden characters and control characters, limited to 96
characters, and receives `.url`. The file uses InternetShortcut format and
is created through the plugin `files.write` capability under the case's
`Links/` directory, creating that directory first. A name collision never
overwrites silently: the dialog offers an explicit `name (2).url` alternative
or cancellation. Read-only/error cases leave the capture unchanged and show
the failure. Success publishes the normal safe file/link Activity event; the
link is opened through the dedicated URL-opening behaviour below, not by
relying on a Linux `.url` file association.
Bulk actions operate only on the visibly filtered capture set, show the count,
and use archive rather than permanent deletion. Every permanent deletion and
Journal deletion has a cancellation-safe confirmation dialog.
When a Дело is renamed, the Inbox service updates the path cache of captures
and exact domain bindings with that `workspaceId`; no relation is keyed by the
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 for its `workspaceId`
become `trashed` with the Desktop trash ID. They remain visible as unavailable
historical context but are not used for automatic routing. A restore event
carrying that trash ID restores the same ID's assignment/binding, including a
restored path changed by a collision. A permanent trash purge changes captures
to unassigned and changes bindings to a visible `orphaned` state that the user
can reassign or remove. Activity remains historical and labels the case as
deleted. If a case disappears by external filesystem change, active references
become `unavailable`, automatic routing is disabled, and the user must
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
Global Browser Inbox has **Active**, **Archive**, and **All** status filters;
Active is the default. Search applies within the chosen filter, so Archive is
searchable deliberately through Archive or All rather than unexpectedly
appearing in the normal queue. **Restore to Inbox** changes `globalState` back
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
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
- Use Russian product labels consistently: **Дела**, **Входящие**,
**Активности**, and **Журнал**. User-facing dates use the local time zone.
- With a selected Дело, the overview reads only records explicitly scoped to
that Дело; an unscoped global event never leaks into every case. With no
selected Дело, it shows a short prompt to select/create one plus up to five
active unassigned Inbox records; it does not fabricate a blended continuation
feed.
- **Needs attention** shows at most five entries: ready Journal candidates,
active unprocessed Inbox records, and existing urgent Todos, in that order
within their respective priority. An Inbox record is new/needs attention
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
last 14 days: unfinished Todo, unprocessed capture, and the most recent
note/file/Journal entity. Every item carries `lastMeaningfulAt`; items sort
descending by that value, with an unprocessed capture, Todo, Journal, note,
then file as deterministic ties. Multiple `file.changed` events for the same
entity collapse to one item. Opening an item does not mark it complete. Its
overflow action **Hide recommendation** stores a non-destructive
entity/event dismissal; a later meaningful change to that entity makes it
eligible again.
- **Recent changes** shows at most eight distinct case-scoped records from the
last seven days. Included events are note save/create, file create/rename or
last change per file, saved browser link, and Journal create. Technical
selection/open events are excluded. The empty state says that there were no
changes in that period.
- Empty states give a next action, not an empty pane.
- Clear, delete, and archive actions name their scope and consequences.
Plugin IDs are diagnostic information. Normal tabs display the manifest's
human title, such as `Заметки` or `Изображение`, never
`verstak.default-editor.notes-markdown` or `verstak.file-preview.image`.
Add a persisted **Settings → Debug → Show plugin IDs** preference, defaulting
to false. The Desktop application-specific `--debug` command-line argument
enables the same display only for that run. When either is active, show the ID
adjacent to the human title and in diagnostic errors. Neither mode alters data,
plugin permissions, or release behaviour.
## Error handling and privacy
- A failed extension delivery keeps the accumulated hostname time locally and
reports the pending-batch count and a non-blocking retry state in extension
settings; it never falls back to Browser Inbox capture.
- Receiver authentication/validation errors provide a safe status to the
extension without echoing the pairing token or untrusted payload.
- Activity never manufactures a case from missing assignment data.
- User-visible errors explain the failed action and offer the next safe action.
## Verification
Automated checks must cover:
- browser tracker explicit consent/disable behaviour, focused-tab-only
accounting, exclusions, canonical hostname vectors, immutable pending
batches, acknowledgement-only reset, retry persistence, payload privacy,
negative clock changes, long gaps, restart, lock, and suspend/resume;
- Desktop activity receiver authentication, canonical normalization,
validation, idempotency, binding, and event publication;
- append-only Activity log retention/compaction, background subscription
lifecycle, UUID workspace/unassigned session scopes, point-event duration
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`
naming/collision/readonly behaviour, filtered bulk operations, rename, trash
restore/purge, external-workspace unavailability, duplicate-workspace-ID
repair, and direct Linux URL opening without `.url` association;
- normal and debug-mode plugin tab labels;
- the corrected frontend Wails mock, deterministic Overview limits/scoping,
plus the end-to-end flows activity-to-Journal and Inbox-to-Дело.
Manual GUI smoke testing verifies Russian normal-mode labels, hidden plugin
IDs, candidate review, Inbox preservation, and extension domain exclusions in
the Linux Desktop build.
## Out of scope
- automatic creation of a Дело;
- background-tab or browser-history tracking;
- URL/content/title collection for passive activity;
- shared/multi-case captures;
- automated Journal saving, billing, or time-sheet generation;
- browser-extension localization and a general analytics product;
- release packaging, licensing, public GitHub README, and sync-server release
security, which belong to the next alpha-release design.