chore(storage,workspace): gates — coverage, catalogs, bilingual note

- Per-file 100% coverage across the five new packages (invariant
  companion suites, failure-injection negatives, lifecycle and
  malformed-medium branches).
- Canonical README Model Experience / Known Limitations sections; new
  storage/ and workspace/ group READMEs; packages/README.md rows (budget
  ceiling raised 760 → 790 for the two new groups).
- Cordis catalog/type-link registrations, service-role classification,
  and regenerated catalogs/graphs for the new services and events.
- Agent Note: English body + i18n pairing record; design-sketch fences
  opted out of doc-typecheck as ignore-check.
- Two exactOptionalPropertyTypes/discriminant fixes in new tests.

doc-sync (24 gates), typecheck, hygiene, and the five-package suite
(92 tests) all pass.
This commit is contained in:
imccyu
2026-07-24 22:49:57 +08:00
parent 80b3b6d917
commit 0559e0207e
33 changed files with 1333 additions and 58 deletions

View File

@@ -16,7 +16,19 @@ Session persistence is an optional peer resolved with `ctx.get`: absent, attach
## Model Experience
No model-visible surface: the package registers no tools, injects no prompts, and emits no context. Token and KV-cache cost are zero.
### Workspace records and session accounts
#### What the model sees
Nothing. `ctx.workspace` serves workspace records to host-side consumers only: the package registers no tools, injects no prompts, and writes no session events, so no request field ever carries this package's data.
#### Token effect
Zero direct tokens on every request.
#### KV Cache effect
Independent of live requests: the package never touches a request prefix, so it cannot invalidate provider cache reuse.
## Known Limitations and Deferred Work

View File

@@ -0,0 +1,56 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import InvariantService from '@deepseek-ai/dsh-invariants'
import type { DomainChanged } from '@deepseek-ai/dsh-domain'
import * as WorkspaceInvariant from '../src/invariant.ts'
import { WorkspaceId } from '../src/index.ts'
/** Boot the invariant service plus the companion over a stubbed registry knowing exactly `ids`. */
async function setup(ids: string[]): Promise<Context> {
const ctx = new Context()
await ctx.plugin(InvariantService)
ctx.provide('workspace', {
get: (id: WorkspaceId) => (ids.includes(id) ? { id } : undefined),
})
await ctx.plugin(WorkspaceInvariant)
return ctx
}
type ChangeLocation = Partial<Pick<DomainChanged, 'domain' | 'table' | 'key'>>
const put = (overrides?: ChangeLocation): DomainChanged => ({
domain: 'workspace',
table: 'workspaces',
key: 'w1',
operation: 'put',
value: {},
...overrides,
})
const deleted = (): DomainChanged => ({
domain: 'workspace',
table: 'workspaces',
key: 'w1',
operation: 'deleted',
})
describe('workspace cache/table invariant', () => {
it('accepts a put whose record has a cached entity and ignores foreign events', async () => {
const ctx = await setup(['w1'])
expect(() => { ctx.emit('domain/changed', put()) }).not.toThrow()
// Other domains and other tables are out of scope, whatever their shape.
expect(() => { ctx.emit('domain/changed', put({ domain: 'other', key: 'missing' })) }).not.toThrow()
expect(() => { ctx.emit('domain/changed', put({ table: 'other', key: 'missing' })) }).not.toThrow()
})
it('fails a deleted operation — this phase exposes no delete entry point', async () => {
const ctx = await setup(['w1'])
expect(() => { ctx.emit('domain/changed', deleted()) })
.toThrow(/no delete entry point/)
})
it('fails a put whose record the registry cache does not hold', async () => {
const ctx = await setup([])
expect(() => { ctx.emit('domain/changed', put()) }).toThrow(/diverged/)
})
})

View File

@@ -4,6 +4,7 @@ import { tmpdir } from 'node:os'
import { basename, join } from 'node:path'
import { Context } from 'cordis'
import Storage from '@deepseek-ai/dsh-storage'
import type { StorageBackend } from '@deepseek-ai/dsh-storage'
import { DomainFacility } from '@deepseek-ai/dsh-domain'
import type { DomainChanged } from '@deepseek-ai/dsh-domain'
import { SessionId } from '@deepseek-ai/dsh-session'
@@ -24,10 +25,11 @@ const header = (id: string, cwd?: string): SessionHeader =>
async function harness(options?: {
pool?: MemoryMediaPool
sessions?: SessionHeader[] | 'absent'
backend?: StorageBackend
}) {
const ctx = new Context()
await ctx.plugin(Storage)
ctx.storage.backend.register('memory', new MemoryStorageBackend(options?.pool))
ctx.storage.backend.register('memory', options?.backend ?? new MemoryStorageBackend(options?.pool))
ctx.storage.mount('domain', new DomainFacility(ctx, { backend: 'memory', routes: {} }))
let listed = options?.sessions === 'absent' ? undefined : options?.sessions ?? []
if (listed !== undefined) {
@@ -44,6 +46,36 @@ async function harness(options?: {
}
}
/** A memory backend whose next `putRecord` throws once when armed, for write-failure paths. */
function failingBackend(): { backend: StorageBackend; arm: () => void } {
const inner = new MemoryStorageBackend()
let failNext = false
return {
arm: () => { failNext = true },
backend: {
kv: {
open: async (descriptor) => {
const unit = await inner.kv.open(descriptor)
return {
loadAll: () => unit.loadAll(),
putRecord: async (table, key, value) => {
if (failNext) {
failNext = false
throw new Error('medium write failed (injected)')
}
return unit.putRecord(table, key, value)
},
deleteRecord: (table, key) => unit.deleteRecord(table, key),
setGlobal: value => unit.setGlobal(value),
close: () => unit.close(),
}
},
},
close: () => inner.close(),
},
}
}
/** A pool pre-stamped with one stored workspace record, simulating a prior run. */
function pooledRecord(id: string, record: WorkspaceRecord): MemoryMediaPool {
const pool = new MemoryMediaPool()
@@ -134,6 +166,25 @@ describe('WorkspaceRegistry.create', () => {
expect(await registry.resolveByPath(link)).toBe(workspace)
expect(await registry.resolveByPath(await makeDir('unowned'))).toBeUndefined()
})
it('rolls the entity cache back when the durable write fails, leaving the path free to retry', async () => {
const dir = await makeDir('rollback')
const { backend, arm } = failingBackend()
const { registry } = await harness({ backend })
arm()
await expect(registry.create(dir)).rejects.toThrow(/injected/)
expect(registry.list()).toEqual([])
const retried = await registry.create(dir)
expect(retried.path).toBe(dir)
})
it('rejects any table access before the registry has started', async () => {
const dir = await makeDir('unstarted')
const ctx = new Context()
// Constructed directly, Service.init never ran: no domain, no table.
const registry = new WorkspaceRegistry(ctx)
await expect(registry.create(dir)).rejects.toThrow(/not started/)
})
})
describe('Workspace.attachSession', () => {
@@ -235,7 +286,24 @@ describe('consistency projections', () => {
const id = WorkspaceId('00000000-0000-4000-8000-000000000002')
const pool = pooledRecord(id, record(dir, ['maybe']))
const { registry } = await harness({ pool, sessions: 'absent' })
expect(registry.get(id)!.sessionIds).toEqual(['maybe'])
const workspace = registry.get(id)!
expect(workspace.sessionIds).toEqual(['maybe'])
// Mutations must not prune either: unverifiable membership is kept as-is.
await workspace.setTitle('still-unverified')
expect(storedRecord(pool, id).sessionIds).toEqual(['maybe'])
})
it('prunes dead ids even when the triggering mutation is itself a no-op', async () => {
const dir = await makeDir('prune-on-noop')
const id = WorkspaceId('00000000-0000-4000-8000-000000000007')
const pool = pooledRecord(id, record(dir, ['ghost']))
const { registry, changes } = await harness({ pool, sessions: [] })
const workspace = registry.get(id)!
// Detaching an id that was never on the account changes nothing by
// itself, but the mutation slot still prunes the dead 'ghost' durably.
await workspace.detachSession(SessionId('never-there'))
expect(storedRecord(pool, id).sessionIds).toEqual([])
expect(changes).toHaveLength(1)
})
it('rejects startup over a medium accounting one session twice', async () => {
@@ -264,6 +332,20 @@ describe('consistency projections', () => {
})
})
describe('Workspace mutation failures', () => {
it('propagates a medium write failure from a mutation and keeps the old snapshot', async () => {
const dir = await makeDir('write-fail')
const { backend, arm } = failingBackend()
const { registry } = await harness({ backend })
const workspace = await registry.create(dir)
arm()
await expect(workspace.setTitle('lost')).rejects.toThrow(/injected/)
expect(workspace.title).toBe('write-fail')
await workspace.setTitle('kept')
expect(workspace.title).toBe('kept')
})
})
describe('Workspace.status', () => {
it('reports ok while the directory exists and missing-dir once it is gone, without mutating the record', async () => {
const dir = await makeDir('vanishing')
@@ -274,5 +356,8 @@ describe('Workspace.status', () => {
expect(await workspace.status()).toBe('missing-dir')
expect(workspace.path).toBe(dir)
expect(registry.get(workspace.id)).toBe(workspace)
// The path re-materializing as a non-directory is still missing-dir.
await writeFile(dir, 'now a file')
expect(await workspace.status()).toBe('missing-dir')
})
})