@x1a0f3n9/dsh-storage 0.1.5-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DeepSeek
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,6 @@
1
+ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
2
+ # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
+ # after editing either side, bring the other along and re-record with:
4
+ # pnpm run verify-translation-pairing --write packages/storage/storage/README.md
5
+ README.md: edb833ad29d38df3225edad6296096ddbd69107f
6
+ README.zh.md: 1b39857099687a208f4644408ff9a9a3936a5847
package/README.md ADDED
@@ -0,0 +1,142 @@
1
+ ---
2
+ description: "Storage hub (ctx.storage) for compositions and maintainers choosing, mounting, or debugging named storage backends and data-form facilities."
3
+ kind: "package-reference"
4
+ ---
5
+
6
+ # @x1a0f3n9/dsh-storage
7
+
8
+ English | [中文](README.zh.md)
9
+
10
+ ## Summary
11
+
12
+ Use `dsh-storage` to keep typed application data durable without adding it to session history. Mount it with a supported storage medium and domain configuration, then callers can access records through the public `ctx.storageDomain` API. Choose it for workspace records, session sidecars, or other application state that must survive restarts without becoming session events. It is available only to host code and has no model-visible effect; compositions that do not need such data can omit it.
13
+
14
+ ## Table of Contents
15
+
16
+ - [Use this package](#use-this-package)
17
+ - [Understand the implementation](#understand-the-implementation)
18
+ - [Further Exploration](#further-exploration)
19
+ - [Model Experience](#model-experience)
20
+ - [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
21
+ - [Dev Note](#dev-note)
22
+
23
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## Use this package
27
+
28
+ Use this package to give a composition durable, non-session storage: mount it together with backend and domain-form packages, and host-side packages read and write validated records through `ctx.storageDomain`. The hub itself adds nothing observable — it is the meeting point that makes the family work, and everything below is what a composition gets from it.
29
+
30
+ ### When to use it
31
+
32
+ Mount the hub whenever any package in the composition persists data that is not a session event log — workspace records, session sidecars. It is required by the domain form and both shipped backends, so the storage rows of a composition are `storage` plus a backend plus `storage-domain`. Skip the whole group when nothing stores such data; the agent loop never needs it.
33
+
34
+ ### A minimal composition
35
+
36
+ ```yaml
37
+ - name: '@x1a0f3n9/dsh-storage'
38
+ - name: '@x1a0f3n9/dsh-storage-json'
39
+ config:
40
+ root: /var/lib/dsh/data
41
+ - name: '@x1a0f3n9/dsh-storage-domain'
42
+ config:
43
+ backend: json
44
+ ```
45
+
46
+ With these rows, the `json` backend registers itself and the `domain` data form mounts; a consumer such as `dsh-workspace` then opens its domain over the routed backend and reads and writes records through `ctx.storageDomain`. Several backends can stay mounted side by side; which backend serves which domain is the domain form's configuration, never a hub-wide choice.
47
+
48
+ ### What you get
49
+
50
+ - A mounted backend resolves by name, so a composition with both shipped backends can route each domain to either medium by configuration.
51
+ - A mounted data form resolves as `ctx.storage.<form>`; the domain form is additionally served directly as `ctx.storageDomain`.
52
+ - Misconfiguration fails loud with a stable `StorageError` code instead of silently deferring: an unknown backend name, a form read before its owner mounts, or a duplicate registration all throw.
53
+
54
+ ### Failures and recovery
55
+
56
+ - `backend-not-found` — the domain form routes to a backend that is not mounted; add the backend package. The form waits for every configured backend to register, so row order is not a failure mode.
57
+ - `form-not-mounted` — a consumer reads `ctx.storage.domain` before `dsh-storage-domain` loads; mount the domain row before the consumer.
58
+ - `duplicate-backend` / `duplicate-mount` — the same name or form registers twice; that is a composition bug and fails loud.
59
+
60
+ -----
61
+
62
+ <a id="understand-the-implementation"></a>
63
+ ## Understand the implementation
64
+
65
+ <details>
66
+ <summary>Implementation internals — click to expand</summary>
67
+
68
+ The hub is a pure registration table with two faces, designed so backends and data forms stay replaceable without the hub knowing their internals.
69
+
70
+ ### Design concept
71
+
72
+ - **Backends own media, data forms own semantics.** The hub never performs IO; it only holds the name → backend table and the form-name → facility map. Backend packages register their medium owner, data-form packages mount their facility, and neither needs the other's details.
73
+ - **Multiple backends stay side by side.** Which backend serves which consumer is the consumer's configuration (the domain form's route table), never a hub-global either-or.
74
+ - **Registration and mounting are effects.** `register()` and `mount()` return disposers; disposal removes only that registration's contribution, and does not close the backend — the owning plugin closes it after unregistering.
75
+ - **Activation cannot race registration.** Each backend plugin also publishes a lifecycle-only service key (`storage.backend.<name>`); form providers inject those keys, so the domain form activates only after every configured backend registers, while callers still resolve backends by name through the hub.
76
+
77
+ ### The backend contract
78
+
79
+ [`src/backend.ts`](src/backend.ts) is the normative contract for backend implementers, checked clause by clause by the shared conformance suite in `tests/contract.ts`. A backend owns exactly one medium and exposes optional data-shape facets; `kv` is the only facet, and opening a unit yields a versioned, globally-singleton schema handle whose single calls are atomic and durable once resolved. Unit and table names must match `UNIT_NAME_RE`; record keys are arbitrary strings that never reach file paths. The unit does not serialize concurrent writes — ordering belongs to the caller — and a stored version differing from the descriptor rejects `version-mismatch` (no migration).
80
+
81
+ ### Source map
82
+
83
+ | File | Role |
84
+ |---|---|
85
+ | [`src/index.ts`](src/index.ts) | Plugin entry: `Storage` service, form mounting, `StorageForms` map |
86
+ | [`src/registry.ts`](src/registry.ts) | `BackendRegistry`: name → backend table, registration disposers |
87
+ | [`src/backend.ts`](src/backend.ts) | The backend contract: facets, units, `UNIT_NAME_RE` |
88
+ | [`src/error.ts`](src/error.ts) | `StorageError` codes shared by the hub and every backend |
89
+ | — | No runtime invariant companion is published; the hub is a pure registration table (names → backends, forms → facilities) whose consistency is fully enforced at the call sites (duplicate/missing entries fail loud synchronously); it owns no event stream or mutable medium to cross-check. |
90
+ | [`tests/contract.ts`](tests/contract.ts) | The shared conformance suite run against each backend |
91
+
92
+ </details>
93
+
94
+ -----
95
+
96
+ <a id="further-exploration"></a>
97
+ ## Further Exploration
98
+
99
+ Read these pages when the hub's view is not enough: the subsystem reference is the authoritative contract, and the Agent Note records the family design and its deferred work.
100
+
101
+ - [Storage subsystem](../../../docs/subsystems/storage.md) — the backend contract, domain semantics, change events, and generated API.
102
+ - [Storage package map](../README.md) — the family's packages and their repository position.
103
+ - [domain KV storage Agent Note](../../../.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.md) — the design behind the hub, the domain form, and the session-backend migration.
104
+
105
+ -----
106
+
107
+ <a id="model-experience"></a>
108
+ ## Model Experience
109
+
110
+ ### Backend and form registrations
111
+
112
+ #### What the model sees
113
+
114
+ Nothing. `ctx.storage` is a host-side registration table: the hub registers no tools, injects no prompts, and writes no session events, so no request field ever carries this package's data.
115
+
116
+ #### Token effect
117
+
118
+ Zero direct tokens on every request.
119
+
120
+ #### KV Cache effect
121
+
122
+ Independent of live requests: the hub never touches a request prefix, so it cannot invalidate provider cache reuse.
123
+
124
+ ## Known Limitations and Deferred Work
125
+
126
+ <a id="known-limitations-and-deferred-work"></a>
127
+
128
+
129
+ These limits define what the hub cannot do. They are current package constraints, not a task backlog.
130
+
131
+ - **`kv` is the only data shape** — a backend implements one facet; the `log` facet for session event logs is deferred to the session-backend migration ([Agent Note](../../../.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.md)).
132
+ - **Forms resolve lazily** — reading `ctx.storage.domain` before the domain plugin mounts throws `form-not-mounted`; assemblies order plugins accordingly rather than silently deferring.
133
+
134
+ <a id="dev-note"></a>
135
+ ### Dev Note
136
+
137
+ <details>
138
+ <summary>Working context for maintainers — click to expand</summary>
139
+
140
+ None.
141
+
142
+ </details>
package/README.zh.md ADDED
@@ -0,0 +1,142 @@
1
+ ---
2
+ description: "存储枢纽(ctx.storage):面向选择、挂载或排查具名存储后端与数据形式设施的组合方与维护者。"
3
+ kind: "package-reference"
4
+ ---
5
+
6
+ # @x1a0f3n9/dsh-storage
7
+
8
+ [English](README.md) | 中文
9
+
10
+ ## 概述
11
+
12
+ 使用 `dsh-storage` 持久保存类型化应用数据,而不将其加入会话历史。将它与受支持的存储介质和领域配置一同挂载后,调用方即可通过公共 `ctx.storageDomain` API 访问记录。工作区记录、会话伴随数据或其他必须在重启后保留且不应成为会话事件的应用状态适合使用它。它仅供宿主代码使用,对模型没有可见影响;无需此类数据的组合可以省略它。
13
+
14
+ ## 目录
15
+
16
+ - [使用本包](#use-this-package)
17
+ - [理解实现](#understand-the-implementation)
18
+ - [进一步探索](#further-exploration)
19
+ - [模型体验](#model-experience)
20
+ - [已知限制与延期工作](#known-limitations-and-deferred-work)
21
+ - [开发备注](#dev-note)
22
+
23
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## 使用本包
27
+
28
+ 使用本包为组合提供持久的非会话存储:把它与后端和数据形式包一起挂载,宿主包即可通过 `ctx.storageDomain` 读写经过校验的记录。枢纽自身不增加任何可观察行为——它是让整个家族运转起来的交汇点——以下内容就是组合从它得到的一切。
29
+
30
+ ### 何时使用
31
+
32
+ 当组合中任何包需要持久化会话事件日志以外的数据——工作区记录、会话伴随数据——时就挂载枢纽。领域数据形式与两个内置后端都依赖它,因此组合的存储行是 `storage` 加一个后端加 `storage-domain`。没有任何此类数据的组合可以省略整个组;agent loop 永远不需要它。
33
+
34
+ ### 最小组合
35
+
36
+ ```yaml
37
+ - name: '@x1a0f3n9/dsh-storage'
38
+ - name: '@x1a0f3n9/dsh-storage-json'
39
+ config:
40
+ root: /var/lib/dsh/data
41
+ - name: '@x1a0f3n9/dsh-storage-domain'
42
+ config:
43
+ backend: json
44
+ ```
45
+
46
+ 这些行加载后,`json` 后端注册自身、`domain` 数据形式挂载;诸如 `dsh-workspace` 之类的消费方随后在已路由后端上打开自己的领域,并通过 `ctx.storageDomain` 读写记录。多个后端可以并排保持挂载;哪个后端服务哪个领域由领域数据形式的配置决定,绝非枢纽的全局选择。
47
+
48
+ ### 你能得到什么
49
+
50
+ - 已挂载的后端按名称解析,因此同时挂载两个内置后端的组合可以把每个领域按配置路由到任一种介质。
51
+ - 已挂载的数据形式解析为 `ctx.storage.<form>`;领域数据形式还直接以 `ctx.storageDomain` 对外服务。
52
+ - 错误配置会以稳定的 `StorageError` 代码明确报错,而不是静默推迟:未知的后端名称、在其所有者挂载前读取数据形式、或重复注册都会抛出异常。
53
+
54
+ ### 失败与恢复
55
+
56
+ - `backend-not-found`——领域数据形式路由到未挂载的后端;请添加后端包。数据形式会等待所有已配置后端注册,因此行序不会造成失败。
57
+ - `form-not-mounted`——消费方在 `dsh-storage-domain` 加载前读取 `ctx.storage.domain`;请把领域行放在消费方之前。
58
+ - `duplicate-backend`/`duplicate-mount`——同一名称或形式注册了两次;这是组合错误,会明确报错。
59
+
60
+ -----
61
+
62
+ <a id="understand-the-implementation"></a>
63
+ ## 理解实现
64
+
65
+ <details>
66
+ <summary>实现细节——点击展开</summary>
67
+
68
+ 枢纽是一张纯注册表,拥有两个面,设计目标是后端与数据形式可以独立替换,而枢纽无需了解它们的内部实现。
69
+
70
+ ### 设计理念
71
+
72
+ - **后端拥有介质,数据形式拥有语义。** 枢纽从不执行 IO;它只持有名称 → 后端表和形式名 → 设施表。后端包注册其介质所有者,数据形式包挂载其设施,双方都不需要对方的细节。
73
+ - **多个后端并排共存。** 哪个后端服务哪个消费方由消费方自身的配置决定(领域数据形式的路由表),绝非枢纽全局的二选一。
74
+ - **注册与挂载都是 effect。** `register()` 与 `mount()` 返回资源释放函数;释放只移除该次注册的贡献,且不会关闭后端——由所属插件在注销后关闭。
75
+ - **激活不会与注册竞争。** 每个后端插件还会发布一个仅用于生命周期的服务键(`storage.backend.<name>`);数据形式提供方注入这些键,因此领域数据形式只在所有已配置后端注册后激活,而调用方仍通过枢纽按名称解析后端。
76
+
77
+ ### 后端约定
78
+
79
+ [`src/backend.ts`](src/backend.ts) 是后端实现者的规范性约定,由 `tests/contract.ts` 中的共享一致性套件逐条款检查。一个后端只拥有一种介质,并暴露可选的数据形状分面;`kv` 是唯一的分面,打开单元即可获得一个带版本、全局单例的 schema 句柄,其每次单独调用都是原子的,且 resolve 后即已持久。单元名与表名必须匹配 `UNIT_NAME_RE`;记录键是任意字符串,绝不进入文件路径。单元不对并发写入做串行化——顺序由调用方负责——介质上记录的版本与描述符不同时拒绝 `version-mismatch`(不做迁移)。
80
+
81
+ ### 源码地图
82
+
83
+ | 文件 | 职责 |
84
+ |---|---|
85
+ | [`src/index.ts`](src/index.ts) | 插件入口:`Storage` 服务、数据形式挂载、`StorageForms` 表 |
86
+ | [`src/registry.ts`](src/registry.ts) | `BackendRegistry`:名称 → 后端表、注册资源释放函数 |
87
+ | [`src/backend.ts`](src/backend.ts) | 后端约定:分面、单元、`UNIT_NAME_RE` |
88
+ | [`src/error.ts`](src/error.ts) | 枢纽与每个后端共享的 `StorageError` 代码 |
89
+ | — | 不发布运行时不变式伴生入口;纯注册表。 |
90
+ | [`tests/contract.ts`](tests/contract.ts) | 针对每个后端运行的共享一致性套件 |
91
+
92
+ </details>
93
+
94
+ -----
95
+
96
+ <a id="further-exploration"></a>
97
+ ## 进一步探索
98
+
99
+ 当枢纽视角不够用时阅读以下页面:子系统参考是权威约定,Agent Note 记录了家族设计与延期工作。
100
+
101
+ - [存储子系统](../../../docs/subsystems/storage.zh.md)——后端约定、领域语义、变更事件与生成的 API。
102
+ - [存储包映射](../README.zh.md)——家族的各包及其在仓库中的位置。
103
+ - [领域 KV 存储 Agent Note](../../../.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.zh.md)——枢纽、领域数据形式与会话后端迁移背后的设计。
104
+
105
+ -----
106
+
107
+ <a id="model-experience"></a>
108
+ ## 模型体验
109
+
110
+ ### 后端与形式注册
111
+
112
+ #### 模型看到什么
113
+
114
+ 无。`ctx.storage` 是宿主侧注册表:枢纽不注册工具、不注入提示词,也不写入会话事件,因此任何请求字段都不会携带本包的数据。
115
+
116
+ #### Token 影响
117
+
118
+ 每次请求都不会直接增加 token。
119
+
120
+ #### KV Cache 影响
121
+
122
+ 与实时请求相互独立:枢纽绝不触碰请求前缀,因此无法使提供方缓存复用失效。
123
+
124
+ ## 已知限制与延期工作
125
+
126
+ <a id="known-limitations-and-deferred-work"></a>
127
+
128
+
129
+ 这些限制定义了枢纽不能做什么。它们是当前包约束,不是任务积压。
130
+
131
+ - **`kv` 是唯一的数据形状**——后端只实现一个分面;面向会话事件日志的 `log` 分面被推迟到会话后端迁移([Agent Note](../../../.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.zh.md))。
132
+ - **数据形式按需解析**——在领域插件挂载前读取 `ctx.storage.domain` 会抛出 `form-not-mounted`;组装会按相应顺序排列插件,而不是静默推迟。
133
+
134
+ <a id="dev-note"></a>
135
+ ### 开发备注
136
+
137
+ <details>
138
+ <summary>维护者的工作上下文——点击展开</summary>
139
+
140
+ 无。
141
+
142
+ </details>
package/lib/index.js ADDED
@@ -0,0 +1,140 @@
1
+ import { Service } from "@deepseek-ai/cordis";
2
+ //#region lib/types/error.js
3
+ /**
4
+ * Error vocabulary for the storage hub and its backends.
5
+ * @module @x1a0f3n9/dsh-storage/src/error
6
+ */
7
+ /**
8
+ * Error thrown by the hub and by backend implementations. The `code` is the
9
+ * stable contract consumers may switch on; `message` is diagnostic prose.
10
+ */
11
+ var StorageError = class extends Error {
12
+ code;
13
+ name = "StorageError";
14
+ /**
15
+ * @param code - Stable discriminant for the failure class.
16
+ * @param message - Human-readable diagnostic detail.
17
+ * @param options - Standard error options (`cause`).
18
+ */
19
+ constructor(code, message, options) {
20
+ super(message, options);
21
+ this.code = code;
22
+ }
23
+ };
24
+ //#endregion
25
+ //#region lib/types/registry.js
26
+ /**
27
+ * Named backend registry of the storage hub.
28
+ * @module @x1a0f3n9/dsh-storage/src/registry
29
+ */
30
+ /**
31
+ * Mutable name → backend table. Multiple backends stay mounted side by side;
32
+ * which backend serves which consumer is the consumer's configuration
33
+ * (e.g. the domain layer's route table), never a hub-global choice.
34
+ */
35
+ var BackendRegistry = class {
36
+ backends = /* @__PURE__ */ new Map();
37
+ /**
38
+ * Register a named backend. Registration is an effect: the returned
39
+ * disposer removes the name. Disposal does NOT close the backend — the
40
+ * owning plugin closes it after unregistering.
41
+ * @param name - Backend name, e.g. `json` or `sqlite`.
42
+ * @param backend - The backend instance.
43
+ * @returns the disposer that unregisters the name.
44
+ */
45
+ register(name, backend) {
46
+ if (this.backends.has(name)) throw new StorageError("duplicate-backend", `storage backend '${name}' is already registered`);
47
+ this.backends.set(name, backend);
48
+ return () => {
49
+ if (this.backends.get(name) === backend) this.backends.delete(name);
50
+ };
51
+ }
52
+ /**
53
+ * Resolve a backend by name.
54
+ * @param name - Registered backend name.
55
+ * @returns the backend.
56
+ */
57
+ get(name) {
58
+ const backend = this.backends.get(name);
59
+ if (!backend) throw new StorageError("backend-not-found", `storage backend '${name}' is not registered (registered: ${[...this.backends.keys()].join(", ") || "none"})`);
60
+ return backend;
61
+ }
62
+ /**
63
+ * Registered backend names, for diagnostics.
64
+ * @returns a snapshot array of names.
65
+ */
66
+ names() {
67
+ return [...this.backends.keys()];
68
+ }
69
+ };
70
+ //#endregion
71
+ //#region lib/types/backend.js
72
+ /**
73
+ * Backend-facing vocabulary of the storage hub: a backend owns one medium
74
+ * (a file-tree root, a database file) and exposes operation groups over it.
75
+ * This module defines the normative contract text for backend implementers; the shared
76
+ * conformance suite in `tests/contract.ts` checks every rule.
77
+ * @module @x1a0f3n9/dsh-storage/src/backend
78
+ */
79
+ /** Allowed format for unit and table names: safe as a file name and as a SQL identifier segment without escaping. */
80
+ const UNIT_NAME_RE = /^[a-z][a-z0-9_]*$/;
81
+ //#endregion
82
+ //#region lib/types/index.js
83
+ /**
84
+ * Storage hub (`ctx.storage`): a named backend registry plus mounted
85
+ * data-form facilities. The hub itself performs no IO — backends own media,
86
+ * data forms (the domain layer first) own semantics.
87
+ * @module @x1a0f3n9/dsh-storage
88
+ */
89
+ /**
90
+ * Derive the Cordis lifecycle service that one named backend plugin provides.
91
+ * Domain-form providers inject these keys so activation cannot race backend
92
+ * registration even though callers continue resolving backends through the
93
+ * storage registry.
94
+ * @param name - Backend registry name.
95
+ * @returns the corresponding lifecycle-only service key.
96
+ */
97
+ function storageBackendServiceKey(name) {
98
+ return `storage.backend.${name}`;
99
+ }
100
+ /**
101
+ * The storage hub service. Backends register under `backend`; data forms
102
+ * mount under their `StorageForms` key and are reached as `ctx.storage.<form>`.
103
+ */
104
+ var Storage = class extends Service {
105
+ /** Named backend table; multiple backends stay mounted side by side. */
106
+ backend = new BackendRegistry();
107
+ forms = /* @__PURE__ */ new Map();
108
+ constructor(ctx) {
109
+ super(ctx, "storage");
110
+ }
111
+ /**
112
+ * Mount a data-form facility on the hub. Mounting is an effect: the
113
+ * returned disposer unmounts the form.
114
+ * @param form - Form key declared in {@link StorageForms}.
115
+ * @param facility - The facility instance to expose.
116
+ * @returns the disposer that unmounts the form.
117
+ */
118
+ mount(form, facility) {
119
+ if (this.forms.has(form)) throw new StorageError("duplicate-mount", `storage form '${String(form)}' is already mounted`);
120
+ this.forms.set(form, facility);
121
+ return () => {
122
+ if (this.forms.get(form) === facility) this.forms.delete(form);
123
+ };
124
+ }
125
+ /**
126
+ * Resolve a mounted data form.
127
+ * @param form - Form key declared in {@link StorageForms}.
128
+ * @returns the mounted facility.
129
+ */
130
+ form(form) {
131
+ if (!this.forms.has(form)) throw new StorageError("form-not-mounted", `storage form '${String(form)}' is not mounted`);
132
+ return this.forms.get(form);
133
+ }
134
+ /** Domain data form; present once the domain layer plugin is loaded. */
135
+ get domain() {
136
+ return this.form("domain");
137
+ }
138
+ };
139
+ //#endregion
140
+ export { BackendRegistry, Storage, Storage as default, StorageError, UNIT_NAME_RE, storageBackendServiceKey };
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Backend-facing vocabulary of the storage hub: a backend owns one medium
3
+ * (a file-tree root, a database file) and exposes operation groups over it.
4
+ * This module defines the normative contract text for backend implementers; the shared
5
+ * conformance suite in `tests/contract.ts` checks every rule.
6
+ * @module @x1a0f3n9/dsh-storage/src/backend
7
+ */
8
+ /** Allowed format for unit and table names: safe as a file name and as a SQL identifier segment without escaping. */
9
+ export declare const UNIT_NAME_RE: RegExp;
10
+ /**
11
+ * One registered backend. A backend owns exactly one medium and shares its
12
+ * lifecycle across all facets; facets are optional members — a backend that
13
+ * cannot serve a data kind simply omits it, and resolution fails loud instead.
14
+ */
15
+ export interface StorageBackend {
16
+ /** Key-value operations; absent when this backend cannot serve them. */
17
+ readonly kv?: KvFacet;
18
+ /**
19
+ * Drain in-flight writes across all open units and release the medium.
20
+ * Idempotent; concurrent and repeated calls resolve once teardown finishes.
21
+ * @returns resolution after the medium is released.
22
+ */
23
+ close(): Promise<void>;
24
+ }
25
+ /** The key-value data shape: whole-unit snapshots plus per-record durable writes. */
26
+ export interface KvFacet {
27
+ /**
28
+ * Open one unit, creating it when the medium holds no trace of it yet
29
+ * (materialization may defer to the first write, but {@link KvUnit.loadAll}
30
+ * must immediately serve the empty shape). A version already stamped on the
31
+ * medium that differs from `descriptor.version` rejects with
32
+ * `version-mismatch`; a medium that cannot be parsed as this unit rejects
33
+ * with `malformed-medium`. Opening the same unit name twice without closing
34
+ * is a caller bug and rejects.
35
+ * @param descriptor - Static identity and shape of the unit to open.
36
+ * @returns the opened unit.
37
+ */
38
+ open(descriptor: KvUnitDescriptor): Promise<KvUnit>;
39
+ }
40
+ /** Static identity and shape of one KV unit, projected from its owner's spec. */
41
+ export interface KvUnitDescriptor {
42
+ /** Unit name; must match {@link UNIT_NAME_RE}. Also the file-name / SQL-identifier segment. */
43
+ readonly name: string;
44
+ /** Unit format version; a non-negative integer stamped on the medium at first materialization. */
45
+ readonly version: number;
46
+ /** Table names; each must match {@link UNIT_NAME_RE}. */
47
+ readonly tables: readonly string[];
48
+ /** Whether this unit carries the global singleton slot. */
49
+ readonly hasGlobal: boolean;
50
+ /**
51
+ * Medium layout. `single` (the default) keeps the whole unit in one
52
+ * document; `per-record` keeps each record in its own document, so a unit
53
+ * whose records are large or sparse never rewrites the rest on one write,
54
+ * and an unaccepted version stamp discards only that record instead of
55
+ * rejecting the whole unit. Backends that only serve one layout accept the
56
+ * other's units as foreign documents.
57
+ */
58
+ readonly layout?: 'single' | 'per-record';
59
+ /**
60
+ * Older unit versions whose stored records are also readable under the
61
+ * declaring owner's current record schemas (the owner vouches for that —
62
+ * typically by declaring the fields old records lack as optional). Reads of
63
+ * a `per-record` unit accept documents stamped with any listed version, and
64
+ * the legacy whole-unit bootstrap accepts a legacy file stamped with one;
65
+ * writes always stamp {@link version}. `single`-layout reads stay
66
+ * exact-version.
67
+ */
68
+ readonly compatibleVersions?: readonly number[];
69
+ }
70
+ /**
71
+ * One opened unit. Values are opaque JSON to this layer: no schema, no
72
+ * events, no domain meaning. The unit does NOT serialize concurrent writes —
73
+ * write ordering is the caller's responsibility (the domain layer runs one
74
+ * write chain per unit); the unit only guarantees that each single call is
75
+ * atomic on the medium and durable once resolved (a crash after resolution
76
+ * followed by a re-open observes the write). Any call after {@link close}
77
+ * rejects with `closed`.
78
+ */
79
+ export interface KvUnit {
80
+ /**
81
+ * Read the full current snapshot.
82
+ * @returns every table's records keyed by table name, plus the global
83
+ * singleton (`null` when never written or not declared).
84
+ */
85
+ loadAll(): Promise<{
86
+ tables: Record<string, Record<string, unknown>>;
87
+ global: unknown;
88
+ }>;
89
+ /**
90
+ * Upsert one record durably. Overwrite semantics: an existing key is replaced.
91
+ * @param table - Declared table name.
92
+ * @param key - Record key. In the `per-record` layout a key becomes a path
93
+ * segment and must match `[a-zA-Z0-9_-]+` (an unsafe key rejects); in the
94
+ * `single` layout keys stay opaque.
95
+ * @param value - Opaque JSON-serializable record.
96
+ * @returns resolution after durability.
97
+ */
98
+ putRecord(table: string, key: string, value: unknown): Promise<void>;
99
+ /**
100
+ * Delete one record durably. Idempotent: a missing key is a no-op.
101
+ * @param table - Declared table name.
102
+ * @param key - Record key.
103
+ * @returns resolution after durability.
104
+ */
105
+ deleteRecord(table: string, key: string): Promise<void>;
106
+ /**
107
+ * Move one record's stored document out of the unit's readable set,
108
+ * preserving its bytes for inspection instead of deleting them. Backends
109
+ * whose medium has no per-record document to move (the `single` layout, a
110
+ * row store) omit this member, and the caller falls back to its
111
+ * reject-loud path. Absent after the move: a later {@link loadAll} reads
112
+ * the key as missing and a later {@link putRecord} recreates it fresh.
113
+ * @param table - Declared table name.
114
+ * @param key - Record key.
115
+ * @returns the medium location the document was moved to (diagnostics).
116
+ */
117
+ backupRecord?(table: string, key: string): Promise<string>;
118
+ /**
119
+ * Write the global singleton durably. Only valid when the descriptor
120
+ * declared `hasGlobal`.
121
+ * @param value - Opaque JSON-serializable value.
122
+ * @returns resolution after durability.
123
+ */
124
+ setGlobal(value: unknown): Promise<void>;
125
+ /**
126
+ * Drain this unit's in-flight writes and release it. Idempotent.
127
+ * @returns resolution after the unit is released.
128
+ */
129
+ close(): Promise<void>;
130
+ }
131
+ //# sourceMappingURL=backend.d.ts.map
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Error vocabulary for the storage hub and its backends.
3
+ * @module @x1a0f3n9/dsh-storage/src/error
4
+ */
5
+ /** Discriminant codes carried by every {@link StorageError}. */
6
+ export type StorageErrorCode = 'backend-not-found' | 'form-not-mounted' | 'duplicate-backend' | 'duplicate-mount' | 'version-mismatch' | 'malformed-medium' | 'closed';
7
+ /**
8
+ * Error thrown by the hub and by backend implementations. The `code` is the
9
+ * stable contract consumers may switch on; `message` is diagnostic prose.
10
+ */
11
+ export declare class StorageError extends Error {
12
+ readonly code: StorageErrorCode;
13
+ readonly name = "StorageError";
14
+ /**
15
+ * @param code - Stable discriminant for the failure class.
16
+ * @param message - Human-readable diagnostic detail.
17
+ * @param options - Standard error options (`cause`).
18
+ */
19
+ constructor(code: StorageErrorCode, message: string, options?: ErrorOptions);
20
+ }
21
+ //# sourceMappingURL=error.d.ts.map
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Storage hub (`ctx.storage`): a named backend registry plus mounted
3
+ * data-form facilities. The hub itself performs no IO — backends own media,
4
+ * data forms (the domain layer first) own semantics.
5
+ * @module @x1a0f3n9/dsh-storage
6
+ */
7
+ import { Context, Service } from '@deepseek-ai/cordis';
8
+ import { BackendRegistry } from './registry.ts';
9
+ export { BackendRegistry } from './registry.ts';
10
+ export { StorageError } from './error.ts';
11
+ export type { StorageErrorCode } from './error.ts';
12
+ export { UNIT_NAME_RE } from './backend.ts';
13
+ export type { StorageBackend, KvFacet, KvUnit, KvUnitDescriptor } from './backend.ts';
14
+ /**
15
+ * Derive the Cordis lifecycle service that one named backend plugin provides.
16
+ * Domain-form providers inject these keys so activation cannot race backend
17
+ * registration even though callers continue resolving backends through the
18
+ * storage registry.
19
+ * @param name - Backend registry name.
20
+ * @returns the corresponding lifecycle-only service key.
21
+ */
22
+ export declare function storageBackendServiceKey(name: string): string;
23
+ declare module '@deepseek-ai/cordis' {
24
+ interface Context {
25
+ storage: Storage;
26
+ }
27
+ }
28
+ /**
29
+ * Data forms mountable on the hub, keyed by form name. Form owners extend
30
+ * this map via declaration merging (the domain layer merges
31
+ * `domain: DomainFacility`) and mount the facility in their `apply`.
32
+ */
33
+ export interface StorageForms {
34
+ }
35
+ /**
36
+ * The storage hub service. Backends register under `backend`; data forms
37
+ * mount under their `StorageForms` key and are reached as `ctx.storage.<form>`.
38
+ */
39
+ export declare class Storage extends Service {
40
+ /** Named backend table; multiple backends stay mounted side by side. */
41
+ readonly backend: BackendRegistry;
42
+ private readonly forms;
43
+ constructor(ctx: Context);
44
+ /**
45
+ * Mount a data-form facility on the hub. Mounting is an effect: the
46
+ * returned disposer unmounts the form.
47
+ * @param form - Form key declared in {@link StorageForms}.
48
+ * @param facility - The facility instance to expose.
49
+ * @returns the disposer that unmounts the form.
50
+ */
51
+ mount<K extends keyof StorageForms>(form: K, facility: StorageForms[K]): () => void;
52
+ /**
53
+ * Resolve a mounted data form.
54
+ * @param form - Form key declared in {@link StorageForms}.
55
+ * @returns the mounted facility.
56
+ */
57
+ form<K extends keyof StorageForms>(form: K): StorageForms[K];
58
+ /** Domain data form; present once the domain layer plugin is loaded. */
59
+ get domain(): StorageForms extends {
60
+ domain: infer D;
61
+ } ? D : never;
62
+ }
63
+ export default Storage;
64
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Named backend registry of the storage hub.
3
+ * @module @x1a0f3n9/dsh-storage/src/registry
4
+ */
5
+ import type { StorageBackend } from './backend.ts';
6
+ /**
7
+ * Mutable name → backend table. Multiple backends stay mounted side by side;
8
+ * which backend serves which consumer is the consumer's configuration
9
+ * (e.g. the domain layer's route table), never a hub-global choice.
10
+ */
11
+ export declare class BackendRegistry {
12
+ private readonly backends;
13
+ /**
14
+ * Register a named backend. Registration is an effect: the returned
15
+ * disposer removes the name. Disposal does NOT close the backend — the
16
+ * owning plugin closes it after unregistering.
17
+ * @param name - Backend name, e.g. `json` or `sqlite`.
18
+ * @param backend - The backend instance.
19
+ * @returns the disposer that unregisters the name.
20
+ */
21
+ register(name: string, backend: StorageBackend): () => void;
22
+ /**
23
+ * Resolve a backend by name.
24
+ * @param name - Registered backend name.
25
+ * @returns the backend.
26
+ */
27
+ get(name: string): StorageBackend;
28
+ /**
29
+ * Registered backend names, for diagnostics.
30
+ * @returns a snapshot array of names.
31
+ */
32
+ names(): string[];
33
+ }
34
+ //# sourceMappingURL=registry.d.ts.map
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@x1a0f3n9/dsh-storage",
3
+ "description": "Storage hub (ctx.storage): named backend registry plus mounted data-form facilities for the DeepSeek Harness",
4
+ "version": "0.1.5-alpha.2",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
11
+ "directory": "packages/storage/storage"
12
+ },
13
+ "type": "module",
14
+ "main": "lib/index.js",
15
+ "types": "lib/types/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./lib/types/index.d.ts",
19
+ "default": "./lib/index.js"
20
+ },
21
+ "./src/*": "./src/*",
22
+ "./package.json": "./package.json"
23
+ },
24
+ "files": [
25
+ "lib/index.js",
26
+ "lib/types/**/*.d.ts"
27
+ ],
28
+ "license": "MIT",
29
+ "peerDependencies": {
30
+ "@deepseek-ai/cordis": "^4.0.2"
31
+ },
32
+ "devDependencies": {
33
+ "@deepseek-ai/cordis": "^4.0.2"
34
+ }
35
+ }