From 25a4a2063b262b639580b8d524513b5fa2ed9261 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Fri, 24 Jul 2026 23:26:41 +0800 Subject: [PATCH] docs(storage): replace design-sketch fences with prose in the bilingual note CI's static lane counts opted-out ts fences repo-wide and the note's ignore-check sketches tipped the ratio past 50%. The hub/backend/event shapes those fences sketched are all shipped code now, so the sections point at the owning source files (src/index.ts, src/backend.ts, src/error.ts, src/events.ts) instead of restating signatures; both language sides move together and the pairing record is re-confirmed. --- ...-domain-kv-storage-and-workspace.i18n.yaml | 4 +- ...6-07-24-domain-kv-storage-and-workspace.md | 92 +------------------ ...7-24-domain-kv-storage-and-workspace.zh.md | 92 +------------------ 3 files changed, 10 insertions(+), 178 deletions(-) diff --git a/.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.i18n.yaml b/.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.i18n.yaml index 9e25572a8f..1ef199c028 100644 --- a/.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.i18n.yaml +++ b/.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -2026-07-24-domain-kv-storage-and-workspace.md: 96299afae94245c1441643407a21a8fab0efcad5 -2026-07-24-domain-kv-storage-and-workspace.zh.md: 67921a4eec4d322aefc705042b6356b156755a7f +2026-07-24-domain-kv-storage-and-workspace.md: f6b74b656270f4791fd9369438235c7fc1b1edb8 +2026-07-24-domain-kv-storage-and-workspace.zh.md: f6649fc0c387d9c1240d45efda4b4a0ab9372486 diff --git a/.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.md b/.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.md index 96299afae9..f6b74b6562 100644 --- a/.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.md +++ b/.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.md @@ -33,65 +33,11 @@ Dependency direction: `dsh-workspace` → `dsh-domain` → `dsh-storage` ← the ### `dsh-storage`: the storage hub -A pure registration hub, no IO of its own, no Config. - -```ts ignore-check -declare module 'cordis' { interface Context { storage: Storage } } - -export class Storage extends Service { - constructor(ctx: Context) // super(ctx, 'storage') - readonly backend: BackendRegistry - /** Domain data form; present once dsh-domain is loaded. Unmounted access → throw. */ - readonly domain: DomainFacility - /** Mount a data-form facility. Returns the disposer. Duplicate mount → throw. */ - mount(form: K, facility: StorageForms[K]): () => void -} - -/** Merge-extensible map of data forms; dsh-domain merges `domain: DomainFacility`. */ -export interface StorageForms {} - -export class BackendRegistry { - /** Register a named backend. Returns the disposer (unregisters). Duplicate name → throw. */ - register(name: string, backend: StorageBackend): () => void - /** Resolve by name. Unknown → throw StorageError('backend-not-found'). */ - get(name: string): StorageBackend - names(): string[] -} -``` +A pure registration hub, no IO of its own, no Config. The `Storage` service mounts at `ctx.storage` with two faces: `backend` (a `BackendRegistry`: `register(name, backend)` returns the disposer, duplicate names throw; `get(name)` throws `backend-not-found` for unknown names) and data-form mounting (`mount(form, facility)` over the merge-extensible `StorageForms` map, into which `dsh-domain` merges the `domain` key; unmounted access throws `form-not-mounted`). The signature text lives in `packages/storage/storage/src/index.ts` and `src/registry.ts`. **Multiple backends stay mounted side by side**; which backend serves a domain is `dsh-domain`'s configuration (below), never a global either-or. Disposer semantics = remove the name from the table; closing the backend itself belongs to the backend package's effect closure, unregister first then close. -A backend is one **medium owner** (a file-tree root / one db file) exposing primitives through **data-shape facets** — only `kv` this phase; the session migration adds `log` (see the migration section). A facet is an optional member: absence means the backend cannot serve that shape, and resolution fails loud: - -```ts ignore-check -export interface StorageBackend { - readonly name: string - readonly kv?: KvFacet // 迁移期扩展:readonly log?: LogFacet - /** Drain in-flight writes and release the medium. Idempotent. */ - close(): Promise -} - -export interface KvFacet { - /** Open (create or load) one unit. Version mismatch / malformed medium → throw. */ - open(descriptor: KvUnitDescriptor): Promise -} - -export interface KvUnitDescriptor { - readonly name: string // ^[a-z][a-z0-9_]*$,兼作文件名/SQL 表名段 - readonly version: number - readonly tables: readonly string[] // 同字符集约束 - readonly hasGlobal: boolean -} - -/** One opened unit. Values are opaque JSON to this layer. */ -export interface KvUnit { - loadAll(): Promise<{ tables: Record>; global: unknown | null }> - putRecord(table: string, key: string, value: unknown): Promise - deleteRecord(table: string, key: string): Promise // missing key = no-op - setGlobal(value: unknown): Promise - close(): Promise // idempotent -} -``` +A backend is one **medium owner** (a file-tree root / one db file) exposing primitives through **data-shape facets** — only `kv` this phase; the session migration adds `log` (see the migration section). A facet is an optional member: absence means the backend cannot serve that shape, and resolution fails loud. The `kv` facet's primitive surface: `open(descriptor)` (descriptor = name/version/table list/global flag, with names and table names restricted to `^[a-z][a-z0-9_]*$` doubling as file-name and SQL-identifier segments) returns a unit exposing `loadAll` / `putRecord` / `deleteRecord` (missing key is a no-op) / `setGlobal` / `close` (idempotent); values are opaque JSON to the backend. The normative text (with per-method JSDoc) is `packages/storage/storage/src/backend.ts`. The backend contract (asserted clause by clause by the shared conformance suite, one suite for both backends): @@ -103,12 +49,7 @@ The backend contract (asserted clause by clause by the shared conformance suite, 6. Any string key / any JSON value is safe (keys never reach file paths, a structural property). 7. `close` is idempotent; any operation after close → `StorageError('closed')`. -```ts ignore-check -export type StorageErrorCode = - | 'backend-not-found' | 'form-not-mounted' | 'duplicate-backend' | 'duplicate-mount' - | 'version-mismatch' | 'malformed-medium' | 'closed' -export class StorageError extends Error { readonly code: StorageErrorCode } -``` +The error vocabulary is `StorageError` with a code discriminant: `backend-not-found` / `form-not-mounted` / `duplicate-backend` / `duplicate-mount` / `version-mismatch` / `malformed-medium` / `closed` (`packages/storage/storage/src/error.ts`). ### `dsh-storage-json` @@ -216,32 +157,7 @@ Rules: - **Records are plain data**: immutable, directly JSON-serializable POJOs; values returned by `get`/`entries` must not be mutated in place (TypeScript readonly projection, no runtime freezing). Behavior-carrying domain objects belong to consumer packages. - **Serialized writes**: one promise chain per domain; `put`/`delete`/`update`/`global.set` all queue on it; `update`'s fn runs on the chain, so concurrency cannot interleave. No active-record (pulling out a mutable object that auto-persists — uncontrollable persist timing, in conflict with the whole-unit atomic-rewrite model). - **Version fails loud**: a stored version differing from the spec throws outright; no migration, no rebuild (the data is not regenerable; pre-release rejects old formats). -- **Change events**: emitted after each write's durability resolves, one per record, no old value (matching the repository's "new snapshot + operation discriminant" convention, template `goal/changed`); this is next phase's RPC push-frame event source: - -```ts ignore-check -declare module 'cordis' { - interface Events { - /** - * A domain record or global changed (post-durability). - * @mode emit - * @param change - domain, table ('' for global), key ('' for global), - * operation, and the new snapshot (absent for deletions). - */ - 'domain/changed'(change: DomainChanged): void - } -} -export interface DomainChanged { - readonly domain: string - readonly table: string - readonly key: string - readonly operation: 'put' | 'deleted' - readonly value?: unknown -} - -export type DomainErrorCode = - | 'already-open' | 'facet-unsupported' | 'invalid-record' | 'missing-key' | 'closed' -export class DomainError extends Error { readonly code: DomainErrorCode } -``` +- **Change events**: after each write's durability resolves, emit `domain/changed` (`@mode emit`), one per record, no old value (matching the repository's "new snapshot + operation discriminant" convention, template `goal/changed`); the payload `DomainChanged` is a put/deleted discriminated union — domain + table + key (both `''` for global changes) + operation, with the put branch carrying the new snapshot value and the deleted branch carrying none (`packages/storage/domain/src/events.ts`). This is next phase's RPC push-frame event source. The error vocabulary is `DomainError`, codes: `already-open` / `facet-unsupported` / `invalid-record` (with `{ table, key }`) / `missing-key` / `closed`. ### Future work: session-side deletion (design settled, not implemented this phase) diff --git a/.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.zh.md b/.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.zh.md index 67921a4eec..f6649fc0c3 100644 --- a/.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.zh.md +++ b/.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.zh.md @@ -33,65 +33,11 @@ host 侧唯一的持久化面是 session 事件日志(`packages/session-persis ### `dsh-storage`:存储枢纽 -纯注册枢纽,自身不做 IO,无 Config。 - -```ts ignore-check -declare module 'cordis' { interface Context { storage: Storage } } - -export class Storage extends Service { - constructor(ctx: Context) // super(ctx, 'storage') - readonly backend: BackendRegistry - /** Domain data form; present once dsh-domain is loaded. Unmounted access → throw. */ - readonly domain: DomainFacility - /** Mount a data-form facility. Returns the disposer. Duplicate mount → throw. */ - mount(form: K, facility: StorageForms[K]): () => void -} - -/** Merge-extensible map of data forms; dsh-domain merges `domain: DomainFacility`. */ -export interface StorageForms {} - -export class BackendRegistry { - /** Register a named backend. Returns the disposer (unregisters). Duplicate name → throw. */ - register(name: string, backend: StorageBackend): () => void - /** Resolve by name. Unknown → throw StorageError('backend-not-found'). */ - get(name: string): StorageBackend - names(): string[] -} -``` +纯注册枢纽,自身不做 IO,无 Config。`Storage` service 挂 `ctx.storage`,两个面:`backend`(`BackendRegistry`:`register(name, backend)` 返回 disposer、重名 throw;`get(name)` 未知名 throw `backend-not-found`)与数据形式挂载(`mount(form, facility)` 配 merge-extensible 的 `StorageForms` map,`dsh-domain` merge 进 `domain` 键;未挂载访问 throw `form-not-mounted`)。签名正文见 `packages/storage/storage/src/index.ts` 与 `src/registry.ts`。 **多后端同时挂载**;域→后端的选择是 `dsh-domain` 的配置(见下),不是全局二选一。disposer 语义 = 从表中摘名;后端自身的 close 由后端包的 effect 闭包负责,顺序先摘名后 close。 -一个后端是一个**介质 owner**(一棵文件树 root / 一个 db 文件),通过**数据形状 facet** 暴露原语——本期只有 `kv`;session 迁移期加 `log`(见迁移节)。facet 是可选成员,缺席即该后端不支持该形状,解析时 fail loud: - -```ts ignore-check -export interface StorageBackend { - readonly name: string - readonly kv?: KvFacet // 迁移期扩展:readonly log?: LogFacet - /** Drain in-flight writes and release the medium. Idempotent. */ - close(): Promise -} - -export interface KvFacet { - /** Open (create or load) one unit. Version mismatch / malformed medium → throw. */ - open(descriptor: KvUnitDescriptor): Promise -} - -export interface KvUnitDescriptor { - readonly name: string // ^[a-z][a-z0-9_]*$,兼作文件名/SQL 表名段 - readonly version: number - readonly tables: readonly string[] // 同字符集约束 - readonly hasGlobal: boolean -} - -/** One opened unit. Values are opaque JSON to this layer. */ -export interface KvUnit { - loadAll(): Promise<{ tables: Record>; global: unknown | null }> - putRecord(table: string, key: string, value: unknown): Promise - deleteRecord(table: string, key: string): Promise // missing key = no-op - setGlobal(value: unknown): Promise - close(): Promise // idempotent -} -``` +一个后端是一个**介质 owner**(一棵文件树 root / 一个 db 文件),通过**数据形状 facet** 暴露原语——本期只有 `kv`;session 迁移期加 `log`(见迁移节)。facet 是可选成员,缺席即该后端不支持该形状,解析时 fail loud。`kv` facet 的原语面:`open(descriptor)`(descriptor = 名字/版本/表名清单/有无 global,名字与表名限 `^[a-z][a-z0-9_]*$` 兼作文件名与 SQL 表名段)返回 unit,unit 提供 `loadAll` / `putRecord` / `deleteRecord`(缺 key 为 no-op)/ `setGlobal` / `close`(幂等);值对后端是不透明 JSON。规范正文(含逐方法 JSDoc)在 `packages/storage/storage/src/backend.ts`。 backend 契约(共享契约测试逐条断言,两后端同套件): @@ -103,12 +49,7 @@ backend 契约(共享契约测试逐条断言,两后端同套件): 6. 任意字符串 key / 任意 JSON 值安全(key 不进文件路径,结构性质)。 7. `close` 幂等;close 后任何操作 → `StorageError('closed')`。 -```ts ignore-check -export type StorageErrorCode = - | 'backend-not-found' | 'form-not-mounted' | 'duplicate-backend' | 'duplicate-mount' - | 'version-mismatch' | 'malformed-medium' | 'closed' -export class StorageError extends Error { readonly code: StorageErrorCode } -``` +错误词汇是带 code 判别的 `StorageError`,码表:`backend-not-found` / `form-not-mounted` / `duplicate-backend` / `duplicate-mount` / `version-mismatch` / `malformed-medium` / `closed`(`packages/storage/storage/src/error.ts`)。 ### `dsh-storage-json` @@ -216,32 +157,7 @@ export interface KvTable { - **记录是纯数据**:可直接 JSON 序列化的不可变 POJO;`get`/`entries` 返回值不得原地改(TypeScript readonly 投影,不做运行时冻结)。带行为的领域对象属于消费者包。 - **写串行**:域内一条 promise 链,`put`/`delete`/`update`/`global.set` 全排队;`update` 的 fn 在链上执行,并发不交错。不做 active-record(取出可变对象自动落盘——落盘时机不可控,与整域原子覆写冲突)。 - **版本 fail loud**:盘上版本与 spec 不符直接报错,不迁移不重建(数据不可再生,pre-release 拒绝旧格式)。 -- **变更事件**:每次写落盘 resolve 后 emit,逐条发、不带旧值(对齐仓库"新快照 + 操作判别"惯例,范本 `goal/changed`);此为下期 RPC 推帧的事件源: - -```ts ignore-check -declare module 'cordis' { - interface Events { - /** - * A domain record or global changed (post-durability). - * @mode emit - * @param change - domain, table ('' for global), key ('' for global), - * operation, and the new snapshot (absent for deletions). - */ - 'domain/changed'(change: DomainChanged): void - } -} -export interface DomainChanged { - readonly domain: string - readonly table: string - readonly key: string - readonly operation: 'put' | 'deleted' - readonly value?: unknown -} - -export type DomainErrorCode = - | 'already-open' | 'facet-unsupported' | 'invalid-record' | 'missing-key' | 'closed' -export class DomainError extends Error { readonly code: DomainErrorCode } -``` +- **变更事件**:每次写落盘 resolve 后 emit `domain/changed`(`@mode emit`),逐条发、不带旧值(对齐仓库"新快照 + 操作判别"惯例,范本 `goal/changed`);payload `DomainChanged` 是 put/deleted 判别联合——域名 + 表名 + key(global 变更两者为 `''`)+ operation,put 支带新快照 value、deleted 支无 value(`packages/storage/domain/src/events.ts`)。此为下期 RPC 推帧的事件源。错误词汇 `DomainError`,码表:`already-open` / `facet-unsupported` / `invalid-record`(带 `{ table, key }`)/ `missing-key` / `closed`。 ### Future work:session 侧删除(设计定案,本期不实施)