docs(storage): align version rules with review findings
This commit is contained in:
1 parent
a5ce98cb82
commit
b412dc04ca
6 files changed
+22
-20
No files matched your search
+6
-6
@@ -12,17 +12,17 @@
|
||||
en: 34e5bde1535a2bb1
|
||||
zh: 559deb25b2ec50ba
|
||||
/agent-note-domain-kv-storage-capability-seam-and-the-workspace-entity/proposal/dsh-storage-the-storage-hub:
|
||||
en: 191afc01a150e4a0
|
||||
zh: da3fbc33875a4f60
|
||||
en: 3bd546f3d6e4f6db
|
||||
zh: 9ecb7bddb0dfa344
|
||||
/agent-note-domain-kv-storage-capability-seam-and-the-workspace-entity/proposal/dsh-storage-json:
|
||||
en: a18ec1e534a5488b
|
||||
zh: 2f5beb445e30554b
|
||||
/agent-note-domain-kv-storage-capability-seam-and-the-workspace-entity/proposal/dsh-storage-sqlite:
|
||||
en: 6a26dd7ef357db74
|
||||
zh: 951b31136e2415f9
|
||||
en: db257f86fdb85f7b
|
||||
zh: b4961cd3f56da716
|
||||
/agent-note-domain-kv-storage-capability-seam-and-the-workspace-entity/proposal/dsh-domain-the-domain-data-form:
|
||||
en: 2ee24f36cd93f7dd
|
||||
zh: 45c83bd745de41f9
|
||||
en: de98570d018f8131
|
||||
zh: 3187b550128fef78
|
||||
/agent-note-domain-kv-storage-capability-seam-and-the-workspace-entity/proposal/future-work-session-side-deletion-design-settled-not-implemented-this-phase:
|
||||
en: a3824326587b6539
|
||||
zh: 9736e3060bd57651
|
||||
|
||||
@@ -44,9 +44,9 @@ A backend is one **medium owner** (a file-tree root / one db file) exposing prim
|
||||
The backend contract (asserted clause by clause by the shared conformance suite, one suite for both backends):
|
||||
|
||||
1. `open` creates when the medium holds nothing (lazy materialization allowed: may defer to the first write, but `loadAll` must immediately serve empty tables); loads when the medium exists.
|
||||
2. A stored version ≠ descriptor.version → `StorageError('version-mismatch')`; no migration, no rebuild.
|
||||
2. Unit-wide checks accept `descriptor.version` and the owner's `compatibleVersions`; other stamps reject with `StorageError('version-mismatch')`. Ordinary compatible reads preserve saved stamps and values. [Domain versioning](../../../../docs/subsystems/storage.md#domain-versioning) defines write stamping and JSON per-record exceptions.
|
||||
3. Durability: after a write primitive resolves, a process crash followed by a re-open must observe the write in `loadAll`.
|
||||
4. The backend does not promise write ordering within a unit — **the caller serializes**; the backend only guarantees each single call is atomic (JSON whole-file replace / SQLite single statement).
|
||||
4. The backend does not promise write ordering within a unit — **the caller serializes**; each mutation atomically publishes its data and current stamp (JSON whole-file replacement / SQLite transaction when advancing the stamp).
|
||||
5. `deleteRecord` is idempotent; `putRecord` overwrites.
|
||||
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')`.
|
||||
@@ -88,8 +88,8 @@ CREATE TABLE IF NOT EXISTS "u_<unit>_<table>" (
|
||||
key TEXT PRIMARY KEY, value TEXT NOT NULL) STRICT; -- value = record JSON document
|
||||
```
|
||||
|
||||
- Unit versions live in `units` rows; a descriptor mismatch → `version-mismatch`. Row granularity is document-per-row, preserving precise per-key durable updates (the path left open for high-frequency point-update tables like the session sidecar); when query needs appear, JSON1 reads the value column directly.
|
||||
- Write primitives are single statements and thus atomic; no cross-statement transactions needed (the domain layer has no cross-table transactions, see the out-of-scope list).
|
||||
- Unit versions live in `units` rows; an unaccepted version rejects with `version-mismatch`. Row granularity is document-per-row, preserving precise per-key durable updates (the path left open for high-frequency point-update tables like the session sidecar); when query needs appear, JSON1 reads the value column directly.
|
||||
- Current-version write primitives use single statements. The first mutation of an accepted older unit commits its data and current stamp in one transaction. This does not add cross-table domain transactions.
|
||||
|
||||
### `dsh-domain`: the domain data form
|
||||
|
||||
@@ -117,6 +117,7 @@ export interface DomainTableSpec<K extends string, V> { readonly valueSchema: Zo
|
||||
export interface DomainSpec {
|
||||
readonly name: string // ^[a-z][a-z0-9_]*$
|
||||
readonly version: number
|
||||
readonly compatibleVersions?: readonly number[]
|
||||
readonly global?: DomainGlobalSpec<unknown>
|
||||
readonly tables: Record<string, DomainTableSpec<string, unknown>>
|
||||
}
|
||||
@@ -158,7 +159,7 @@ Rules:
|
||||
- **Single-level mapping**: key → record, no nested tables; hierarchical needs use composite keys or fields inside the value. The two backends stay isomorphic as a result (one JSON object level ↔ one SQLite row).
|
||||
- **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).
|
||||
- **Version compatibility**: the domain owner declares readable older versions through `compatibleVersions` and decodes historical values with its current schemas. Storage does not convert record values; [domain versioning](../../../../docs/subsystems/storage.md#domain-versioning) defines stamp publication and mixed-generation requirements.
|
||||
- **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/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)
|
||||
|
||||
+6
-5
@@ -44,9 +44,9 @@ host 侧唯一的持久化面是 session 事件日志(`packages/session/sessio
|
||||
后端约定(共享约定测试逐条断言,两后端同套件):
|
||||
|
||||
1. `open` 对不存在的介质创建(懒物化允许:可延迟到首写,但 `loadAll` 立即可用返回空表);对已存在介质载入。
|
||||
2. 介质上版本 ≠ descriptor.version → `StorageError('version-mismatch')`,不迁移不重建。
|
||||
2. 单元级检查接受 `descriptor.version` 和拥有方声明的 `compatibleVersions`;其他版本戳以 `StorageError('version-mismatch')` 拒绝。普通兼容读取保留已存版本戳和值。[领域版本](../../../../docs/subsystems/storage.zh.md#domain-versioning)定义写入盖戳和 JSON 逐记录例外。
|
||||
3. 持久性:写原语 resolve 后进程崩溃再 open,`loadAll` 必须反映该写入。
|
||||
4. 后端不承诺 unit 内写并发序——**调用方负责串行**;后端只保证单次调用原子(JSON 整文件替换 / SQLite 单语句)。
|
||||
4. 后端不承诺 unit 内写并发序——**调用方负责串行**;每次修改原子发布其数据和当前版本戳(JSON 整文件替换 / SQLite 推进版本戳时使用事务)。
|
||||
5. `deleteRecord` 幂等;`putRecord` 覆写。
|
||||
6. 任意字符串 key / 任意 JSON 值安全(key 不进文件路径,结构性质)。
|
||||
7. `close` 幂等;close 后任何操作 → `StorageError('closed')`。
|
||||
@@ -88,8 +88,8 @@ CREATE TABLE IF NOT EXISTS "u_<unit>_<table>" (
|
||||
key TEXT PRIMARY KEY, value TEXT NOT NULL) STRICT; -- value = record JSON document
|
||||
```
|
||||
|
||||
- unit 版本存 `units` 行,descriptor 不符 → `version-mismatch`。行粒度 document-per-row,保住按 key 精确落盘更新(为 session sidecar 这类高频点更新大表留路);查询需求出现时 JSON1 直查 value 列。
|
||||
- 写原语单语句即原子,无跨语句事务需求(domain 层无跨表事务,见不做清单)。
|
||||
- unit 版本存 `units` 行,未接受的版本以 `version-mismatch` 拒绝。行粒度 document-per-row,保住按 key 精确落盘更新(为 session sidecar 这类高频点更新大表留路);查询需求出现时 JSON1 直查 value 列。
|
||||
- 当前版本的写原语使用单条语句。对已接受旧单元的首次修改在同一事务中提交其数据和当前版本戳。这不增加跨表领域事务。
|
||||
|
||||
### `dsh-domain`:领域数据形式
|
||||
|
||||
@@ -117,6 +117,7 @@ export interface DomainTableSpec<K extends string, V> { readonly valueSchema: Zo
|
||||
export interface DomainSpec {
|
||||
readonly name: string // ^[a-z][a-z0-9_]*$
|
||||
readonly version: number
|
||||
readonly compatibleVersions?: readonly number[]
|
||||
readonly global?: DomainGlobalSpec<unknown>
|
||||
readonly tables: Record<string, DomainTableSpec<string, unknown>>
|
||||
}
|
||||
@@ -158,7 +159,7 @@ export interface KvTable<K extends string, V> {
|
||||
- **一级 mapping**:key → 记录,不做嵌套表;层级需求用复合 key 或值内字段。两后端因此同构(JSON object 一层 ↔ SQLite 一行)。
|
||||
- **记录是纯数据**:可直接 JSON 序列化的不可变 POJO;`get`/`entries` 返回值不得原地改(TypeScript readonly 投影,不做运行时冻结)。带行为的领域对象属于消费方包。
|
||||
- **写串行**:域内一条 promise 链,`put`/`delete`/`update`/`global.set` 全排队;`update` 的 fn 在链上执行,并发不交错。不做 active-record(取出可变对象自动落盘——落盘时机不可控,与整域原子覆写冲突)。
|
||||
- **版本 fail loud**:盘上版本与 spec 不符直接报错,不迁移不重建(数据不可再生,pre-release 拒绝旧格式)。
|
||||
- **版本兼容**:领域拥有方通过 `compatibleVersions` 声明可读的旧版本,并用当前 schema 解码历史值。Storage 不转换记录值;[领域版本](../../../../docs/subsystems/storage.zh.md#domain-versioning)定义版本戳发布和混合代际要求。
|
||||
- **变更事件**:每次写落盘 resolve 后 emit `domain/changed`(`@mode emit`),逐条发、不带旧值(对齐仓库"新快照 + 操作判别"惯例,范本 `goal/changed`);payload `DomainChanged` 是 put/deleted 判别联合——域名 + 表名 + key(global 变更两者为 `''`)+ operation,put 支带新快照 value、deleted 支无 value(`packages/storage/storage-domain/src/events.ts`)。此为下期 RPC 推帧的事件源。错误词汇 `DomainError`,码表:`already-open` / `facet-unsupported` / `invalid-record`(带 `{ table, key }`)/ `missing-key` / `closed`。
|
||||
|
||||
### Future work:session 侧删除(设计定案,本期不实施)
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
en: fa4613279da360c5
|
||||
zh: 09e7a7d63bd1774a
|
||||
/storage/domain-versioning:
|
||||
en: 7191e32c0b23b38f
|
||||
zh: 15fb47f2e9fccb1e
|
||||
en: 6197f1ddf7325d2c
|
||||
zh: 349d7398c5c8cbec
|
||||
/storage/the-open-domain:
|
||||
en: 43de223f07ce8094
|
||||
zh: da371991af788fdb
|
||||
|
||||
@@ -99,7 +99,7 @@ interface DomainSpec {
|
||||
|
||||
`version` names the current read/write format. `compatibleVersions` lists older versions whose records and global value the owner can decode with its current schemas; only nonnegative integers below `version` are allowed. Without that declaration, reads stay exact-version. Acceptance does not weaken record validation or permit a newer unknown version. Each row's saved fields must determine its meaning unambiguously; a unit stamp cannot distinguish different meanings of identical fields in mixed rows.
|
||||
|
||||
Ordinary compatible opens and reads do not restamp or normalize saved values. Whole-unit JSON publishes the current stamp and changed data in one atomic file replacement; SQLite commits its first updated unit stamp and mutation in one transaction, then uses ordinary statements. Deleting a missing record does not advance the stamp. The stamp identifies an accepted format, not completion of value normalization; readers must accept mixed generations. A reader rejects an unsupported unit stamp.
|
||||
Ordinary compatible opens and reads do not restamp or normalize saved values. Whole-unit JSON publishes the current stamp and changed data in one atomic file replacement; SQLite commits its first updated unit stamp and mutation in one transaction, then uses ordinary statements. Deleting a missing record does not advance the stamp. The stamp identifies an accepted format, not completion of value normalization; readers must accept mixed generations. A backend with a unit-wide stamp rejects an unsupported unit version.
|
||||
|
||||
Serialization failure before publication or successful SQLite rollback preserves the previous data and stamp. An I/O failure after JSON replacement, or a failure of SQLite rollback itself, can leave the saved outcome unknown despite rejection. Stop using and close the backend and its domains, then reopen to inspect saved state before deciding whether to retry. The backend READMEs define these failure cases.
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ interface DomainSpec {
|
||||
|
||||
`version` 表示当前读写格式。`compatibleVersions` 列出拥有方可用当前 schema 解码其记录和全局值的旧版本;只允许小于 `version` 的非负整数。不声明兼容版本时,读取仍要求精确版本。接受旧版本不放宽记录校验,也不允许更新的未知版本。每行已存字段必须能无歧义地确定其含义;单元版本戳无法在混合行中区分相同字段的不同含义。
|
||||
|
||||
普通兼容打开和读取不会重新盖戳或规范化已存值。整单元 JSON 用一次原子文件替换发布当前版本戳和修改后的数据;SQLite 将首次单元版本更新与修改放在同一事务中提交,此后使用普通语句。删除不存在的记录不会推进版本戳。版本戳标识可接受格式,不代表值规范化已经完成;读取器必须接受混合代际。读取器会拒绝不支持的单元版本戳。
|
||||
普通兼容打开和读取不会重新盖戳或规范化已存值。整单元 JSON 用一次原子文件替换发布当前版本戳和修改后的数据;SQLite 将首次单元版本更新与修改放在同一事务中提交,此后使用普通语句。删除不存在的记录不会推进版本戳。版本戳标识可接受格式,不代表值规范化已经完成;读取器必须接受混合代际。采用单元级版本戳的后端会拒绝不支持的单元版本。
|
||||
|
||||
发布前的序列化失败或成功的 SQLite 回滚会保留原有数据与版本戳。JSON 替换后的 I/O 失败或 SQLite 回滚本身失败时,即使操作被拒绝,已存结果也可能未知。必须停止使用并关闭后端及其领域,然后重新打开检查已存状态,再决定是否重试。各后端 README 定义了这些失败情况。
|
||||
|
||||
|
||||
Reference in new issue
Block a user