docs: remove missions directory

This commit is contained in:
Turtle
2026-07-25 14:36:31 +08:00
parent 154104e762
commit f7447b181d

View File

@@ -1,179 +0,0 @@
# Storage + Workspace 工程开发文档
> 施工范围5 个新包session 侧零 diff。规范正典[Agent Note](../../../.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.zh.md)——本文只写工程拆解(目录/文件、class 落位、teammate 分工、并行依赖),接口语义以 Note 为准,冲突时改这里不改 Note除非经用户拍板
> 门禁口径GUI 免门禁期同款——不随手写测试门禁,跑 typecheck/build 保证编译;测试文件按仓库惯例落位(包级 `tests/`、`.spec.ts`),红绿在 PR 窗口收口。
## 0. 总览
```
packages/storage/
storage/ dsh-storage 枢纽Storage service + BackendRegistry + StorageForms
storage-json/ dsh-storage-json JsonStorageBackendkv facet
storage-sqlite/ dsh-storage-sqlite SqliteStorageBackendkv facet
storage-domain/ dsh-storage-domain DomainFacility + Domain + KvTable + domain/changed
packages/workspace/
workspace/ dsh-workspace WorkspaceRegistry + WorkspaceEntity + workspaceDomainSpec
```
依赖与并行关系(→ = 依赖):
```
W1 storage(枢纽) ──→ W2a storage-json ──┐
└──→ W2b storage-sqlite ─┼──→ 集成冒烟W4 兼)
└──→ W3 domain ──────────┘
└──→ W4 workspace
```
- W1 先行(接口包是所有人的编译依赖),完成后 W2a/W2b/W3 **三线并行**W4 依赖 W3 的接口定型(不必等 json/sqlite 完工,可对着 W3 的类型先写,用内存假 backend 跑测试)。
- 每包的 package.json/tsconfig/README/invariant 伴生由该包 owner 自己配齐(模板照抄 `packages/session-persistence/session-persistence-sqlite/` 的形状)。
## 1. W1`dsh-storage`(枢纽)——主线程自做
量小且是全组编译根,主线程直接写,不派 teammate。
```
packages/storage/storage/
package.json # 无运行时依赖cordis peerDep + dev
tsconfig.json
src/index.ts # Storage service + apply + 全部导出
src/registry.ts # BackendRegistry
src/backend.ts # StorageBackend/KvFacet/KvUnitDescriptor/KvUnit 类型
src/error.ts # StorageError + code 联合
src/invariant.ts # 见下
tests/registry.spec.ts # registry/mount 套件
README.md
```
class/接口逐条(签名以 Note 为准,此处列实现要点):
| 成员 | 实现要点 |
| --- | --- |
| `class Storage extends Service` | `super(ctx, 'storage')``readonly backend = new BackendRegistry()``mount(form, facility)` 存入私有 `Map<keyof StorageForms, unknown>`,重复 → `StorageError('duplicate-mount')`,返回删除闭包;`get domain()` 从 map 取,缺 → `StorageError('form-not-mounted')` |
| `class BackendRegistry` | 私有 `Map<string, StorageBackend>``register` 重名 → `duplicate-backend`,返回 `() => map.delete(name)``get` 缺名 → `backend-not-found``names()` 返回数组拷贝 |
| `interface StorageForms {}` | 空接口 + JSDocmerge-extensible键 = 数据形式名) |
| `interface StorageBackend / KvFacet / KvUnitDescriptor / KvUnit` | 纯类型 + 契约 JSDoc七条契约写在 KvUnit 各方法 JSDoc 上——这是 backend 实现者的规范文本) |
| `class StorageError extends Error` | `constructor(code, message?, cause?)``name = 'StorageError'` |
| `const UNIT_NAME_RE = /^[a-z][a-z0-9_]*$/` | 导出descriptor 校验用backend open 时验fail loud |
| invariant | 枢纽自身无运行时不变量(纯注册表,无事件流/可变盘面),写"explained empty"(措辞照抄 sqlite 后端 invariant.ts 的 "No runtime invariant:" 模板) |
事件面:本包**无**事件(`domain/changed` 归 dsh-storage-domain
## 2. W2a`dsh-storage-json` —— teammate **json-backend**
```
packages/storage/storage-json/
src/index.ts # Config + apply + JsonStorageBackend
src/unit.ts # JsonKvUnit
src/atomic.ts # temp+fsync+rename 原子写(含 win32 分支)
src/format.ts # 文件格式 parse/serialize + malformed 检查
src/invariant.ts
tests/json-backend.spec.ts # 挂共享契约套件(见 §5+ json 特有文件肉眼格式、malformed
```
| class | 要点 |
| --- | --- |
| `Config` | schemastery`root: z.string().required()`JSDoc 说明为何无默认:防 cwd 散落,参照 session-persistence 措辞) |
| `class JsonStorageBackend implements StorageBackend` | `name='json'``kv = { open }`;持 `Map<unitName, JsonKvUnit>`(同名重复 open → 复用还是报错:**报错**unit 生命周期归调用方double-open 是 bug`close()` 逐 unit close幂等 |
| `class JsonKvUnit implements KvUnit` | 内存态 `{ version, global, tables: Map<string, Map<string, unknown>> }` 为权威;构造时读盘:文件缺失 = 空单元(不落盘),存在则 parse + 版本比对;每个写原语 = 改内存 → `writeAtomic(serialize())`**写不排队**(契约第 4 条:串行是调用方的事),但单次 writeAtomic 内部完整temp/fsync/renameclose 后操作 → `closed` |
| `atomic.ts` | `writeAtomic(path, data)`:同目录 temp 文件 + fsync + renamewin32 分支照抄 `session-persistence-jsonl/src/win32.ts` 的替换语义(先照抄,`log` facet 迁移期再提共享——Note 已记)|
| `format.ts` | `serialize(unit): string``JSON.stringify(…, null, 2)` + 尾换行);`parse(text): ParsedUnit`,缺 `unit` 头/结构不符 → `malformed-medium` |
| apply | `ctx.effect(() => { const d = ctx.storage.backend.register('json', backend); return async () => { d(); await backend.close() } })`inject: `['storage']` |
| invariant | 断言候选rename 发布后盘上文件必可 parse 回等价内存态(写后读回校验,仅测试态开启);若判断无运行时可断言关系则 explained empty |
## 3. W2b`dsh-storage-sqlite` —— teammate **sqlite-backend**
```
packages/storage/storage-sqlite/
src/index.ts # Config + apply + SqliteStorageBackend
src/unit.ts # SqliteKvUnit
src/schema.ts # SCHEMA_VERSION + openDatabase + DDL
src/invariant.ts
tests/sqlite-backend.spec.ts
```
| class | 要点 |
| --- | --- |
| `Config` | `path: z.string().required()``:memory:` 允许)+ `journalMode` 枚举 default 'wal' |
| `schema.ts` | `STORAGE_SQLITE_SCHEMA_VERSION = 1``openDatabase(config)` 照抄 session-persistence-sqlite 的序列mkdir 0o700 → wx 0o600 建文件 → PRAGMA foreign_keys → journal_mode → user_version 检查盖章/拒绝 → 建 `units`/`unit_globals`**先照抄不提共享 helper**Note 已记:提取放迁移期) |
| `class SqliteStorageBackend` | `name='sqlite'`;单 `DatabaseSync` 连接;`kv.open(descriptor)`:校验名字字符集 → `units` 行版本比对(无行则 INSERT 盖章)→ 按 descriptor.tables 逐张 `CREATE TABLE IF NOT EXISTS "u_<unit>_<table>"` → 返回 unit`close()` 关连接 |
| `class SqliteKvUnit` | 预编译语句(每表 upsert/delete/select-all + global upsert`loadAll` 全表 SELECT 组装;`putRecord` = `INSERT … ON CONFLICT(key) DO UPDATE`单语句原子无显式事务value `JSON.stringify`/parse |
| invariant | 断言候选STRICT 表 + user_version 与常量一致open 后检);或 explained empty |
## 4. W3`dsh-storage-domain` —— teammate **domain-layer**
```
packages/storage/storage-domain/
src/index.ts # Config + apply + DomainFacility
src/spec.ts # DomainSpec/defineDomain/domainTable + descriptorOf
src/domain.ts # DomainImpl + KvTableImpl + 写链
src/events.ts # domain/changed declaration merging
src/error.ts # DomainError
src/invariant.ts
tests/domain.spec.ts # 用内存假 backendtests/helpers/memory-backend.ts
```
| class | 要点 |
| --- | --- |
| `Config` | `backend: z.string().required()` + `routes: z.dict(z.string()).default({})` |
| `spec.ts` | `defineDomain` 恒等函数(编译期收窄)+ 名字/表名正则校验(违规 throwmisconfiguration fails loud`descriptorOf(spec)` 投影 |
| `class DomainFacility` | 持 `Map<domainName, DomainImpl>`already-open 检查);`open(spec)` 按 Note 六步实现zod 依赖在此包dependencies不是 peer |
| `class DomainImpl` | 写链 `chain: Promise<void>``enqueue<T>(job): Promise<T>` 私有方法,所有写走它);内存态 `Map<table, Map<key, value>>` + global每写链上 → 改内存 → unit 原语 await → `ctx.emit('domain/changed', …)`dispose`enqueue(noop)` 排空 → `unit.close()` |
| `class KvTableImpl<K,V>` | 读同步走内存;`update` fn 同步纯(类型上 `(current: V) => V`),缺 key → `missing-key``delete` 返回是否存在 |
| `events.ts` | 按 Note 全文(`@mode emit` + `@param``DomainChanged` 接口导出 |
| invariant | 断言候选(真不变量,建议做):**每次 `domain/changed` 事件的 value 必等于内存态当前值**(事件流 vs 可变数据的 owned relationship正合仓库 invariant 规范)|
| tests/helpers/memory-backend.ts | `MemoryStorageBackend`Map 实现 KvUnit宣称版本可注入——共享给 W4 用 |
## 5. 共享 backend 契约套件 —— domain-layer 兼写(或主线程)
```
packages/storage/storage/tests/contract.ts # export function runKvBackendContract(factory)
```
- 仿 `runPersistenceContract` 形状:`factory: () => Promise<{ backend, reopen(): Promise<StorageBackend> }>`,两后端 spec 文件各自 import 调用。
- 覆盖 Note 七条契约 + 版本拒绝 + close 幂等;"崩溃再 open"用 `reopen()`(新实例指向同一介质)模拟。
- 落在接口包 tests/ 下(不进 src不发布json/sqlite 的 devDependencies 指向 workspace 接口包即可复用。
## 6. W4`dsh-workspace` —— teammate **workspace-domain**
```
packages/workspace/workspace/
src/index.ts # apply + WorkspaceRegistryservice 挂 ctx.workspace
src/types.ts # WorkspaceId brand + Workspace 接口
src/spec.ts # workspaceRecord zod + workspaceDomainSpec
src/entity.ts # WorkspaceEntity不出包index.ts 不 re-export
src/paths.ts # realpathNormalize(path)
src/invariant.ts
tests/workspace.spec.ts # MemoryStorageBackend + 假 sessionPersistence stub
```
删除入口本期不存在registry 无 delete、entity 无关联清理——整套删除语义在 Agent Note 的 future work 节。)
| class | 要点 |
| --- | --- |
| `types.ts` | `WorkspaceId` brand + 工厂;`Workspace` 接口Note 签名照录JSDoc 齐全——这是对外契约) |
| `spec.ts` | `workspaceRecord`path/title/sessionIds/createdAt/updatedAt+ `workspaceDomainSpec = defineDomain({ name: 'workspace', version: 1, tables: { workspaces: … } })` |
| `paths.ts` | `realpathNormalize(p): Promise<string>`——`fs.realpath`ENOENT 原样抛create 的 reject 路径) |
| `class WorkspaceRegistry extends Service` | `super(ctx, 'workspace')`inject `['storage', 'sessionPersistence']`sessionPersistence optional`ctx.get()` 取,缺席时 attach 拒绝);`start()``ctx.storage.domain.open(workspaceDomainSpec)` + 重建 `Map<WorkspaceId, WorkspaceEntity>``create`realpath → resolveByPath 撞 → reject否则 `WorkspaceId(randomUUID())` + `table.put` + 建实体入缓存;`list()` 快照数组(过滤无效 sessionId 的投影在实体 getter 做);**无 delete 方法**future work与 session 级联一体落地) |
| `class WorkspaceEntity implements Workspace` | 构造持 registry/id/recordgetter 投影;`mutate(fn)` 私有:`table.update(id, r => stampUpdatedAt(fn(r)))` 后原地换 record`attachSession`:读 `sessionPersistence.list()` 找 header或 inspectcwd realpath ≠ path → reject幂等已在账 → no-op`detachSession` 摘账(不动 session 文件);`status()``fs.access(path)` |
| 一致性口径 | ①账指向的 session 查无:**投影过滤**getter 层)+ 下次 mutate 摘除;③双重账 load 检出 → throw④missing-dir 只反映在 status() |
| invariant | 断言候选:缓存实体集合与 domain 表 key 集合一致owned relationshipregistry 缓存 vs 权威盘面)|
## 7. Teammate 编成与节奏
| teammate | 包 | 开工条件 | 预估节奏 |
| --- | --- | --- | --- |
| (主线程) | W1 storage 枢纽 + §5 契约套件骨架 | 立即 | 首批落盘,随后进入 review/dispatcher 角色 |
| json-backend | W2a | W1 类型可编译即开工 | 分批落盘atomic/format 先行unit 次之,契约套件接入收尾 |
| sqlite-backend | W2b | 同上 | schema.ts 先行照抄源已指明unit 次之 |
| domain-layer | W3 + memory-backend helper | 同上 | spec/error 先行 → DomainImpl 写链 → 事件 → 契约套件(若主线程未完成则兼) |
| workspace-domain | W4 | W3 的 src 类型定型(不等其测试) | types/spec/paths 先行 → registry/entity → 测试 |
协作规矩(照 conventions分批落盘每批几分钟内、每批一句话回执产出零落盘超 5 分钟报告;不混 commit 别人的在途文件;代码注释一律英文且只写非显然契约;干完不 kill 保持待命。commit 纪律:`--no-verify`按包分刀W1 一刀 → W2a/W2b/W3 各一刀 → W4 一刀 → 测试/文档尾刀),文档(本文件 + Agent Note 增量)住顶刀。
## 8. 主线程验收清单(每包合入前)
- [ ] `pnpm run typecheck` 过(本期唯一硬门禁)
- [ ] 包结构齐package.json`@deepseek-ai/dsh-*`、ESM、cordis peerDep、README、invariant 伴生(真断言或 explained empty
- [ ] 接口与 Agent Note 一致;发现实现逼着改接口 → 停下来报主线程裁决(不擅改 Note
- [ ] 测试文件落位正确(包级 tests/、`.spec.ts`),能跑多少跑多少,红的记台账不追修
- [ ] session-persistence 包零 diff`git status` 检查线)