Files
deepseek-harness/missions/tasks/20260724-storage-workspace/dev-plan.md
imccyu f5506cf35f refactor(storage): rename dsh-domain to dsh-storage-domain
The bare 'domain' name was too generic for a published package. The
directory moves to packages/storage/storage-domain, the package becomes
@deepseek-ai/dsh-storage-domain, and the plugin/invariant names follow;
the ctx surface (ctx.storage.domain), the domain/changed event, and all
runtime behavior are unchanged. References, catalogs, graphs, and the
bilingual design note move together.
2026-07-25 11:08:04 +08:00

14 KiB
Raw Blame History

Storage + Workspace 工程开发文档

施工范围:5 个新包,session 侧零 diff。规范正典:Agent Note——本文只写工程拆解(目录/文件、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    JsonStorageBackend(kv facet)
  storage-sqlite/  dsh-storage-sqlite  SqliteStorageBackend(kv 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 {} 空接口 + JSDoc(merge-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/rename);close 后操作 → closed
atomic.ts writeAtomic(path, data):同目录 temp 文件 + fsync + rename;win32 分支照抄 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        # 用内存假 backend(tests/helpers/memory-backend.ts)
class 要点
Config backend: z.string().required() + routes: z.dict(z.string()).default({})
spec.ts defineDomain 恒等函数(编译期收窄)+ 名字/表名正则校验(违规 throw,misconfiguration 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 + WorkspaceRegistry(service 挂 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/record;getter 投影;mutate(fn) 私有:table.update(id, r => stampUpdatedAt(fn(r))) 后原地换 record;attachSession:读 sessionPersistence.list() 找 header(或 inspect),cwd realpath ≠ path → reject;幂等(已在账 → no-op);detachSession 摘账(不动 session 文件);status():fs.access(path)
一致性口径 ①账指向的 session 查无:投影过滤(getter 层)+ 下次 mutate 摘除;③双重账 load 检出 → throw;④missing-dir 只反映在 status()
invariant 断言候选:缓存实体集合与 domain 表 key 集合一致(owned relationship:registry 缓存 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 检查线)