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
+16 -4
View File
@@ -1,6 +1,6 @@
# @deepseek-ai/dsh-domain
Domain data form for the DeepSeek Harness storage hub: mounts `ctx.storage.domain`, opening schema-validated KV domains over configured storage backends. A domain is declared once with `defineDomain` (zod record schemas, `z.infer`-derived types), opened through `DomainFacility.open`, and served from authoritative in-memory state — reads are synchronous, writes serialize on one per-domain chain, land durably on the routed backend, then emit `domain/changed`.
Domain data form for the DeepSeek Harness storage hub: mounts `ctx.storage.domain`, opening schema-validated KV domains over configured storage backends. A domain is declared once with `defineDomain` (zod record schemas, `z.infer`-derived types), opened through `DomainFacility.open`, and served from authoritative in-memory state — reads are synchronous, writes serialize on one per-domain chain, reach durability on the routed backend first, then update memory and emit `domain/changed`.
Design rationale, open semantics, and the storage/domain layer split live in the [Agent Note](../../../.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.zh.md).
@@ -13,9 +13,21 @@ Design rationale, open semantics, and the storage/domain layer split live in the
## 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.
### Durable domain state
#### What the model sees
Nothing. The package registers no tools, injects no prompts, and appends no session events; it stores non-session data (workspace records, future session sidecars) behind `ctx.storage.domain` and emits only the in-process `domain/changed` event, which reaches a model only if a consumer package renders it through its own documented surface.
#### Token effect
Zero. No text from this package enters any model request.
#### KV Cache effect
Independent: domain reads and writes never touch request prefixes, so nothing here can invalidate provider cache reuse.
## Known Limitations and Deferred Work
- Single-process only: `domain/changed` is an in-process event; cross-process observation (GUI reconnect) is deferred to the revision pattern noted in the Agent Note's non-goals.
- No cross-table transactions, secondary indexes, or multi-segment keys; triggers and rework points are tabled in the Agent Note.
- **Single-process change visibility** — `domain/changed` is an in-process event; a second host process or a reconnecting GUI observes no changes until the cross-process revision pattern deferred in the Agent Note lands.
- **No cross-table transactions, secondary indexes, or multi-segment keys** — each write touches one record; triggers and rework points for these extensions are tabled in the Agent Note's deferred-work list.
+5 -3
View File
@@ -147,7 +147,9 @@ export class DomainImpl {
* @param ctx - Context that carries `domain/changed` emissions.
* @param spec - The domain declaration.
* @param unit - The opened backend unit; this instance owns its lifecycle.
* @param records - Validated per-table records from the unit's `loadAll`.
* @param records - Validated records from the unit's `loadAll`, one entry
* per declared table (empty maps included) — the facility builds it from
* the spec, so the entry set IS the table set.
* @param globalValue - Validated stored global, or the spec's `initial`
* when the medium held none; `undefined` when the spec declares no global.
*/
@@ -166,8 +168,8 @@ export class DomainImpl {
assertReadable: () => this.assertReadable(),
emitChanged: (change) => this.ctx.emit('domain/changed', change),
}
for (const table of Object.keys(spec.tables)) {
this.tables.set(table, new KvTableImpl(host, table, records.get(table) ?? new Map()))
for (const [table, tableRecords] of records) {
this.tables.set(table, new KvTableImpl(host, table, tableRecords))
}
if (spec.global !== undefined) {
this.globalValue = globalValue
+6 -2
View File
@@ -119,8 +119,10 @@ export class DomainFacility {
? spec.global.initial
: parseRecord(spec.name, '', '', () => spec.global!.schema.parse(snapshot.global))
const domain = new DomainImpl(this.ctx, spec, unit, tables, globalValue)
this.domains.set(spec.name, domain)
// The open-domain table entry is itself the effect: registration and
// the drain-then-unlist teardown live in one closure.
this.ctx.effect(() => {
this.domains.set(spec.name, domain)
return async () => {
// Drain before unlisting: writes landing during the drain still
// emit domain/changed, and the domain must stay resolvable (the
@@ -139,7 +141,9 @@ export class DomainFacility {
throw error
}
} catch (error) {
if (!this.domains.has(spec.name)) this.reserved.delete(spec.name)
// Any failure means the effect never registered (nothing can throw
// after it), so releasing the name reservation is unconditional.
this.reserved.delete(spec.name)
throw error
}
}
@@ -76,6 +76,35 @@ describe('DomainFacility.open', () => {
await expect(facility.open(spec)).rejects.toMatchObject({ code: 'facet-unsupported' })
})
it('falls back to the default backend when no route table is configured', async () => {
// A second, unmounted facility whose config omits `routes` entirely
// (exactOptionalPropertyTypes forbids an explicit undefined). Opening
// emits no events, so the mounted facility's invariant never consults it.
const { ctx } = await harness()
const routeless = new DomainFacility(ctx, { backend: 'memory' })
await expect(routeless.open(bareSpec)).resolves.toBeDefined()
})
it('treats a table key the backend omitted from loadAll as empty', async () => {
// A sparse backend: loadAll omits declared table keys entirely instead of
// returning them as empty objects.
const { ctx, facility } = await harness({ config: { backend: 'sparse' } })
ctx.storage.backend.register('sparse', {
kv: {
open: async () => ({
loadAll: async () => ({ tables: {}, global: null }),
putRecord: async () => {},
deleteRecord: async () => {},
setGlobal: async () => {},
close: async () => {},
}),
},
close: async () => {},
})
const domain = await facility.open(bareSpec)
expect(domain.table('rows').size).toBe(0)
})
it('rejects stored records that fail their schema, naming table and key', async () => {
const pool = new MemoryMediaPool()
{
@@ -112,6 +141,33 @@ describe('DomainFacility.open', () => {
})
})
describe('plugin apply', () => {
it('mounts the facility as ctx.storage.domain through the plugin effect', async () => {
const ctx = new Context()
await ctx.plugin(Storage)
ctx.storage.backend.register('memory', new MemoryStorageBackend())
const DomainPlugin = await import('../src/index.ts')
const fiber = await ctx.plugin(DomainPlugin, { backend: 'memory' })
expect(ctx.storage.domain).toBeInstanceOf(DomainFacility)
await fiber.dispose()
expect(() => ctx.storage.form('domain')).toThrow(/not mounted/)
})
})
describe('table and snapshot reads', () => {
it('serves entries, keys, and size as stable snapshots; unknown table names throw', async () => {
const { facility } = await harness()
const domain = await facility.open(spec)
const table = domain.table('items')
await table.put('a', { label: 'x', count: 1 })
await table.put('b', { label: 'y', count: 2 })
expect(table.size).toBe(2)
expect([...table.keys()].sort()).toEqual(['a', 'b'])
expect(new Map(table.entries()).get('a')).toEqual({ label: 'x', count: 1 })
expect(() => domain.table('nope' as never)).toThrow(/declares no table/)
})
})
describe('KvTable writes', () => {
it('serializes concurrent updates on one key without losing increments', async () => {
const { facility } = await harness()
@@ -0,0 +1,91 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { z } from 'zod'
import Storage from '@deepseek-ai/dsh-storage'
import InvariantService, { InvariantError } from '@deepseek-ai/dsh-invariants'
import * as DomainInvariantCompanion from '@deepseek-ai/dsh-domain/invariant'
import { DomainFacility, defineDomain, domainTable } from '../src/index.ts'
import type { DomainChanged } from '../src/events.ts'
import { MemoryStorageBackend } from './helpers/memory-backend.ts'
const itemSchema = z.object({ n: z.number() })
type Item = z.infer<typeof itemSchema>
const spec = defineDomain({
name: 'inv',
version: 1,
global: { schema: itemSchema, initial: { n: 0 } },
tables: { rows: domainTable<string, Item>(itemSchema) },
})
async function setup() {
const ctx = new Context()
await ctx.plugin(Storage)
await ctx.plugin(InvariantService, { enabled: true })
await ctx.plugin(DomainInvariantCompanion)
ctx.storage.backend.register('memory', new MemoryStorageBackend())
const facility = new DomainFacility(ctx, { backend: 'memory', routes: {} })
ctx.storage.mount('domain', facility)
return { ctx, facility }
}
const invariantViolation = expect.objectContaining<Partial<InvariantError>>({
code: 'INVARIANT',
packageName: '@deepseek-ai/dsh-domain',
})
describe('domain change-event invariants', () => {
it('accepts every write shape emitted by the real write paths', async () => {
const { facility } = await setup()
const domain = await facility.open(spec)
const rows = domain.table('rows')
await rows.put('a', { n: 1 })
await rows.update('a', (current) => ({ n: current.n + 1 }))
await expect(rows.delete('a')).resolves.toBe(true)
await domain.global.set({ n: 5 })
})
it('rejects an event for a domain that is not open', async () => {
const { ctx } = await setup()
expect(() => ctx.emit('domain/changed', {
domain: 'ghost', table: 'rows', key: 'a', operation: 'put', value: { n: 1 },
})).toThrow(invariantViolation)
})
it('rejects a put event whose value is not the in-memory record', async () => {
const { ctx, facility } = await setup()
const domain = await facility.open(spec)
await domain.table('rows').put('a', { n: 1 })
expect(() => ctx.emit('domain/changed', {
domain: 'inv', table: 'rows', key: 'a', operation: 'put', value: { n: 999 },
})).toThrow(invariantViolation)
})
it('rejects a deletion event while the record is still in memory', async () => {
const { ctx, facility } = await setup()
const domain = await facility.open(spec)
await domain.table('rows').put('a', { n: 1 })
expect(() => ctx.emit('domain/changed', {
domain: 'inv', table: 'rows', key: 'a', operation: 'deleted',
})).toThrow(invariantViolation)
})
it('rejects a global event whose value is not the in-memory global', async () => {
const { ctx, facility } = await setup()
await facility.open(spec)
expect(() => ctx.emit('domain/changed', {
domain: 'inv', table: '', key: '', operation: 'put', value: { n: 42 },
})).toThrow(invariantViolation)
})
it('tolerates operations outside the closed union without failing falsely', async () => {
const { ctx, facility } = await setup()
const domain = await facility.open(spec)
await domain.table('rows').put('a', { n: 1 })
// Merge-hostile input: the closed union's satisfies-never default arm is
// unreachable in typed code; an untyped emit must not crash the check.
expect(() => ctx.emit('domain/changed', {
domain: 'inv', table: 'rows', key: 'a', operation: 'exotic',
} as unknown as DomainChanged)).not.toThrow()
})
})