@deepseek-ai/dsh-storage-sqlite 0.0.1-rc.1

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,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, DeepSeek
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -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-sqlite/README.md
5
+ README.md: 48aae14aa8613886d69652c39194878c190259cf
6
+ README.zh.md: d95da614deb2fe4b6380532aa43836441a0db01c
package/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # @deepseek-ai/dsh-storage-sqlite
2
+
3
+ English | [中文](README.zh.md)
4
+
5
+ SQLite backend for the [storage hub](../storage/README.md): registers as backend `sqlite`, serving the `kv` facet over one `node:sqlite` database file (or `:memory:`). Design and trade-offs: [domain KV storage Agent Note](../../../.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.zh.md).
6
+
7
+ ## Storage model
8
+
9
+ Document-per-row: each unit table becomes a physical `"u_<unit>_<table>" (key TEXT PRIMARY KEY, value TEXT)` STRICT table whose `value` is the record's JSON text, so one key updates one row (the reason to route a high-churn domain here instead of the JSON backend). Unit identity lives in two metadata tables — `units` stamps each unit's format version at first open and rejects a differing descriptor with `version-mismatch`; `unit_globals` holds each unit's global singleton row. The physical layout version lives in `PRAGMA user_version`; any other stamped value rejects (unreleased format, no migrations). Unit and table names are validated against the hub's `UNIT_NAME_RE` before they reach DDL, so no external input is ever interpolated into SQL identifiers.
10
+
11
+ Every write primitive is a single prepared statement — SQLite's per-statement atomicity satisfies the KV contract without explicit transactions, and write ordering stays the caller's responsibility (the domain layer's write chain). Missing directories and database files are created owner-only (`0o700`/`0o600`), matching the session-persistence SQLite backend.
12
+
13
+ ## Configuration (schemastery)
14
+
15
+ ```ts
16
+ interface Config {
17
+ path: string // SQLite database file path, or ':memory:' for an in-process DB
18
+ journalMode?: 'wal' | 'delete' | 'truncate' | 'persist' // journal_mode pragma; default 'wal'
19
+ }
20
+ ```
21
+
22
+ ## Model Experience
23
+
24
+ ### Stored domain records
25
+
26
+ #### What the model sees
27
+
28
+ Nothing. This backend contributes no prompt, tool, or schema; it persists non-session domain data (workspace records, future session sidecar metadata) behind `ctx.storage` for host-side consumers only.
29
+
30
+ #### Token effect
31
+
32
+ Zero live-request tokens.
33
+
34
+ #### KV Cache effect
35
+
36
+ None — the backend never touches live request prefixes.
37
+
38
+ ## Known Limitations and Deferred Work
39
+
40
+ - **`DatabaseSync` is synchronous** — each write blocks the event loop for its (single-statement) duration; acceptable at domain-data scale.
41
+ - **No busy-wait or retry policy** — another connection holding a write transaction rejects the operation immediately; there is no multi-process write protection.
42
+ - **Only the current `STORAGE_SQLITE_SCHEMA_VERSION` opens** — any other stamped version is rejected rather than migrated (pre-release stance).
43
+ - **`openDatabase` duplicates the session-persistence SQLite open sequence** — extraction into a shared media layer is deferred to the planned session-backend migration (see the Agent Note's reuse audit).
package/README.zh.md ADDED
@@ -0,0 +1,43 @@
1
+ # @deepseek-ai/dsh-storage-sqlite
2
+
3
+ [English](README.md) | 中文
4
+
5
+ [存储中心](../storage/README.md)的 SQLite 后端:注册为后端 `sqlite`,通过一个数据库文件提供 `kv` facet;该文件使用 `node:sqlite`(也可以是 `:memory:`)。设计与取舍见[领域 KV 存储 Agent Note](../../../.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.zh.md)。
6
+
7
+ ## 存储模型
8
+
9
+ 每行一个文档:每个单元表都会成为一个物理 STRICT 表 `"u_<unit>_<table>" (key TEXT PRIMARY KEY, value TEXT)`,其中 `value` 是记录的 JSON 文本,因此一个 key 只更新一行(高频变更领域路由到这里而非 JSON 后端的原因)。单元标识位于两个元数据表中:`units` 在单元首次打开时标记其格式版本,描述符不同时以 `version-mismatch` 拒绝;`unit_globals` 保存每个单元的全局单例行。物理布局版本位于 `PRAGMA user_version`;其他任何标记值都会被拒绝(未发布格式,不迁移)。单元名和表名在进入 DDL 之前依据中心的 `UNIT_NAME_RE` 进行验证,因此不会把外部输入插值到 SQL 标识符中。
10
+
11
+ 每个写入原语都是一条预处理语句:SQLite 的逐语句原子性无需显式事务即可满足 KV 约定,写入顺序仍由调用方负责(领域层写入链)。缺失目录和数据库文件会以仅所有者可访问的权限创建(`0o700`/`0o600`),与 session-persistence SQLite 后端一致。
12
+
13
+ ## 配置(schemastery)
14
+
15
+ ```ts
16
+ interface Config {
17
+ path: string // SQLite database file path, or ':memory:' for an in-process DB
18
+ journalMode?: 'wal' | 'delete' | 'truncate' | 'persist' // journal_mode pragma; default 'wal'
19
+ }
20
+ ```
21
+
22
+ ## 模型体验
23
+
24
+ ### 已存领域记录
25
+
26
+ #### 模型看到的内容
27
+
28
+ 无。该后端不贡献提示词、工具或 schema;它在 `ctx.storage` 后面持久化非会话领域数据(工作区记录、未来的会话伴随元数据),只供主机侧消费方使用。
29
+
30
+ #### Token 影响
31
+
32
+ 实时请求 token 为零。
33
+
34
+ #### KV Cache 影响
35
+
36
+ 无:该后端从不触碰实时请求前缀。
37
+
38
+ ## 已知限制与暂缓事项
39
+
40
+ - **`DatabaseSync` 是同步的**:每次写入都会在单条语句执行期间阻塞事件循环;在领域数据规模下可以接受。
41
+ - **没有忙等待或重试策略**:另一个连接持有写事务时,该操作会立即被拒绝;没有多进程写入保护。
42
+ - **只打开当前的 `STORAGE_SQLITE_SCHEMA_VERSION`**:其他任何已标记版本都会被拒绝而不是迁移(预发布立场)。
43
+ - **`openDatabase` 重复了 session-persistence SQLite 打开顺序**:提取到共享介质层的工作暂缓至计划的会话后端迁移(见 Agent Note 的复用审计)。
package/lib/index.js ADDED
@@ -0,0 +1,308 @@
1
+ import z from "@deepseek-ai/schemastery";
2
+ import { StorageError, UNIT_NAME_RE, storageBackendServiceKey } from "@deepseek-ai/dsh-storage";
3
+ import { DatabaseSync } from "node:sqlite";
4
+ import { mkdir, open } from "node:fs/promises";
5
+ import { dirname, resolve } from "node:path";
6
+ //#region lib/types/schema.js
7
+ /**
8
+ * Schema + open-time helpers for the SQLite storage backend: the physical
9
+ * layout version, the database open/configure sequence (permissions, pragmas,
10
+ * version stamp/reject), and the unit metadata tables. Unit record tables are
11
+ * created per descriptor in `unit.ts`.
12
+ * @module @deepseek-ai/dsh-storage-sqlite/schema
13
+ */
14
+ /**
15
+ * The on-disk physical layout version, stored in `PRAGMA user_version`.
16
+ * Orthogonal to each unit's own `version` (stamped per unit in the `units`
17
+ * row). Bumped only on a breaking change to the table layout; any other
18
+ * stamped version rejects — this unreleased format has no migrations.
19
+ */
20
+ const STORAGE_SQLITE_SCHEMA_VERSION = 1;
21
+ /**
22
+ * Exclusively create a missing database file with owner-only permissions.
23
+ * Existing files retain their modes, and errors other than `EEXIST` propagate.
24
+ * `DatabaseSync` reopens by path, so this does not protect confidentiality or
25
+ * integrity when another principal can replace the database entry in its
26
+ * parent directory.
27
+ */
28
+ async function createDatabaseFile(path) {
29
+ try {
30
+ await (await open(path, "wx", 384)).close();
31
+ } catch (error) {
32
+ if (error.code !== "EEXIST") throw error;
33
+ }
34
+ }
35
+ /**
36
+ * Open the database and apply its schema and pragmas. Missing directories and
37
+ * database files are created owner-only (`:memory:` skips filesystem setup).
38
+ * A zero `user_version` is stamped with {@link STORAGE_SQLITE_SCHEMA_VERSION};
39
+ * every other non-current version rejects rather than being migrated in place.
40
+ * @param path - the SQLite database file to open, or `:memory:`.
41
+ * @param journalMode - validated journal pragma.
42
+ * @returns the open handle with pragmas applied and the unit metadata tables ensured.
43
+ */
44
+ async function openDatabase(path, journalMode) {
45
+ const actual = path === ":memory:" ? path : resolve(path);
46
+ if (actual !== ":memory:") {
47
+ await mkdir(dirname(actual), {
48
+ recursive: true,
49
+ mode: 448
50
+ });
51
+ await createDatabaseFile(actual);
52
+ }
53
+ const db = new DatabaseSync(actual);
54
+ try {
55
+ configureDatabase(db, actual, journalMode);
56
+ return db;
57
+ } catch (error) {
58
+ db.close();
59
+ throw error;
60
+ }
61
+ }
62
+ function configureDatabase(db, path, journalMode) {
63
+ db.exec("PRAGMA foreign_keys = ON");
64
+ db.exec(`PRAGMA journal_mode = ${journalMode.toUpperCase()}`);
65
+ const { user_version: onDisk } = db.prepare("PRAGMA user_version").get();
66
+ if (onDisk !== 0 && onDisk !== 1) throw new StorageError("version-mismatch", `storage database at "${path}" has schema version ${onDisk}, incompatible with this build (1)`);
67
+ db.exec(`
68
+ CREATE TABLE IF NOT EXISTS units (
69
+ name TEXT PRIMARY KEY,
70
+ version INTEGER NOT NULL
71
+ ) STRICT
72
+ `);
73
+ db.exec(`
74
+ CREATE TABLE IF NOT EXISTS unit_globals (
75
+ unit TEXT PRIMARY KEY REFERENCES units(name),
76
+ value TEXT NOT NULL
77
+ ) STRICT
78
+ `);
79
+ if (onDisk === 0) db.exec(`PRAGMA user_version = 1`);
80
+ }
81
+ /**
82
+ * Physical table name for one unit table. Both segments are validated against
83
+ * `UNIT_NAME_RE` before reaching this, so the result is safe to interpolate
84
+ * into DDL and prepared-statement text.
85
+ * @param unit - Validated unit name.
86
+ * @param table - Validated table name.
87
+ * @returns the `u_<unit>_<table>` identifier.
88
+ */
89
+ function recordTableName(unit, table) {
90
+ return `u_${unit}_${table}`;
91
+ }
92
+ //#endregion
93
+ //#region lib/types/unit.js
94
+ /**
95
+ * One opened SQLite KV unit: prepared per-table statements over the
96
+ * `u_<unit>_<table>` record tables plus this unit's row in the shared
97
+ * `unit_globals` table. Each primitive is a single statement, so atomicity
98
+ * comes from SQLite itself — no explicit transactions, and no write queue
99
+ * (write ordering is the caller's responsibility per the KV contract).
100
+ * @module @deepseek-ai/dsh-storage-sqlite/unit
101
+ */
102
+ /**
103
+ * The SQLite {@link KvUnit}. Constructed by the backend AFTER the unit's
104
+ * record tables exist; statements are prepared once here and reused for every
105
+ * primitive. Values are stored as JSON text in the `value` column.
106
+ */
107
+ var SqliteKvUnit = class {
108
+ descriptor;
109
+ onClose;
110
+ tables = /* @__PURE__ */ new Map();
111
+ globalUpsert;
112
+ globalSelect;
113
+ closed = false;
114
+ /**
115
+ * @param db - Open database handle owned by the backend (never closed here).
116
+ * @param descriptor - Validated descriptor whose record tables already exist.
117
+ * @param onClose - Backend callback releasing this unit's open-name slot.
118
+ */
119
+ constructor(db, descriptor, onClose) {
120
+ this.descriptor = descriptor;
121
+ this.onClose = onClose;
122
+ for (const table of descriptor.tables) {
123
+ const physical = recordTableName(descriptor.name, table);
124
+ this.tables.set(table, {
125
+ upsert: db.prepare(`INSERT INTO "${physical}" (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value`),
126
+ remove: db.prepare(`DELETE FROM "${physical}" WHERE key = ?`),
127
+ selectAll: db.prepare(`SELECT key, value FROM "${physical}"`)
128
+ });
129
+ }
130
+ this.globalUpsert = descriptor.hasGlobal ? db.prepare("INSERT INTO unit_globals (unit, value) VALUES (?, ?) ON CONFLICT(unit) DO UPDATE SET value = excluded.value") : void 0;
131
+ this.globalSelect = descriptor.hasGlobal ? db.prepare("SELECT value FROM unit_globals WHERE unit = ?") : void 0;
132
+ }
133
+ loadAll() {
134
+ return this.settle(() => {
135
+ const tables = {};
136
+ for (const [name, statements] of this.tables) {
137
+ const records = Object.create(null);
138
+ for (const row of statements.selectAll.all()) records[row.key] = this.parseValue(row.value, `table '${name}' key '${row.key}'`);
139
+ tables[name] = records;
140
+ }
141
+ let global = null;
142
+ if (this.globalSelect !== void 0) {
143
+ const row = this.globalSelect.get(this.descriptor.name);
144
+ if (row !== void 0) global = this.parseValue(row.value, "global slot");
145
+ }
146
+ return {
147
+ tables,
148
+ global
149
+ };
150
+ });
151
+ }
152
+ /** Parse one stored value column, mapping bad JSON to `malformed-medium`. */
153
+ parseValue(text, slot) {
154
+ try {
155
+ return JSON.parse(text);
156
+ } catch (error) {
157
+ throw new StorageError("malformed-medium", `kv unit '${this.descriptor.name}' holds unparsable JSON at ${slot}`, { cause: error });
158
+ }
159
+ }
160
+ putRecord(table, key, value) {
161
+ return this.settle(() => {
162
+ this.statementsFor(table).upsert.run(key, JSON.stringify(value));
163
+ });
164
+ }
165
+ deleteRecord(table, key) {
166
+ return this.settle(() => {
167
+ this.statementsFor(table).remove.run(key);
168
+ });
169
+ }
170
+ setGlobal(value) {
171
+ return this.settle(() => {
172
+ if (this.globalUpsert === void 0) throw new Error(`kv unit '${this.descriptor.name}' declared no global slot`);
173
+ this.globalUpsert.run(this.descriptor.name, JSON.stringify(value));
174
+ });
175
+ }
176
+ close() {
177
+ if (!this.closed) {
178
+ this.closed = true;
179
+ this.onClose();
180
+ }
181
+ return Promise.resolve();
182
+ }
183
+ /**
184
+ * Run one synchronous primitive behind the closed guard, mapping a throw to
185
+ * a rejection so the Promise-returning contract never throws synchronously.
186
+ */
187
+ settle(operation) {
188
+ try {
189
+ this.ensureOpen();
190
+ return Promise.resolve(operation());
191
+ } catch (error) {
192
+ return Promise.reject(error instanceof Error ? error : new Error(String(error)));
193
+ }
194
+ }
195
+ ensureOpen() {
196
+ if (this.closed) throw new StorageError("closed", `kv unit '${this.descriptor.name}' is closed`);
197
+ }
198
+ statementsFor(table) {
199
+ const statements = this.tables.get(table);
200
+ if (statements === void 0) throw new Error(`kv unit '${this.descriptor.name}' declared no table '${table}'`);
201
+ return statements;
202
+ }
203
+ };
204
+ //#endregion
205
+ //#region lib/types/index.js
206
+ /**
207
+ * SQLite storage backend for the storage hub: one database file hosts every
208
+ * routed unit, document-per-row (`key TEXT` / `value TEXT` JSON). Registers
209
+ * as backend `sqlite`; the disposer unregisters first, then closes the medium.
210
+ * @module @deepseek-ai/dsh-storage-sqlite
211
+ */
212
+ /** Cordis plugin name. */
213
+ const name = "storage-sqlite";
214
+ /** The backend registers on the storage hub. */
215
+ const inject = ["storage"];
216
+ /** Schemastery validator for {@link Config}. */
217
+ const Config = z.object({
218
+ path: z.string().required(),
219
+ journalMode: z.union([
220
+ "wal",
221
+ "delete",
222
+ "truncate",
223
+ "persist"
224
+ ]).default("wal")
225
+ });
226
+ /**
227
+ * The SQLite {@link StorageBackend}. Owns one `DatabaseSync` connection and
228
+ * the open-unit table; `kv.open` validates names, enforces the per-unit
229
+ * version stamp in `units`, and ensures the unit's record tables.
230
+ */
231
+ var SqliteStorageBackend = class {
232
+ /** The key-value facet; the only shape this backend serves. */
233
+ kv = { open: (descriptor) => this.openUnit(descriptor) };
234
+ ready;
235
+ /** Open (or still-opening) units by name; presence is the double-open guard. */
236
+ units = /* @__PURE__ */ new Map();
237
+ closing;
238
+ /**
239
+ * @param config - Validated plugin configuration.
240
+ */
241
+ constructor(config) {
242
+ this.ready = openDatabase(config.path, config.journalMode);
243
+ this.ready.catch(() => {});
244
+ }
245
+ openUnit(descriptor) {
246
+ if (this.closing !== void 0) return Promise.reject(new StorageError("closed", "sqlite storage backend is closed"));
247
+ if (!UNIT_NAME_RE.test(descriptor.name)) return Promise.reject(/* @__PURE__ */ new Error(`kv unit name '${descriptor.name}' violates ${UNIT_NAME_RE}`));
248
+ for (const table of descriptor.tables) if (!UNIT_NAME_RE.test(table)) return Promise.reject(/* @__PURE__ */ new Error(`kv table name '${table}' in unit '${descriptor.name}' violates ${UNIT_NAME_RE}`));
249
+ if (this.units.has(descriptor.name)) return Promise.reject(/* @__PURE__ */ new Error(`kv unit '${descriptor.name}' is already open (double-open is a caller bug)`));
250
+ const pending = this.materializeUnit(descriptor);
251
+ this.units.set(descriptor.name, pending);
252
+ pending.catch(() => this.units.delete(descriptor.name));
253
+ return pending;
254
+ }
255
+ async materializeUnit(descriptor) {
256
+ const db = await this.ready;
257
+ const row = db.prepare("SELECT version FROM units WHERE name = ?").get(descriptor.name);
258
+ if (row === void 0) db.prepare("INSERT INTO units (name, version) VALUES (?, ?)").run(descriptor.name, descriptor.version);
259
+ else if (row.version !== descriptor.version) throw new StorageError("version-mismatch", `kv unit '${descriptor.name}' is stamped version ${row.version} on the medium, incompatible with descriptor version ${descriptor.version}`);
260
+ for (const table of descriptor.tables) db.exec(`
261
+ CREATE TABLE IF NOT EXISTS "${recordTableName(descriptor.name, table)}" (
262
+ key TEXT PRIMARY KEY,
263
+ value TEXT NOT NULL
264
+ ) STRICT
265
+ `);
266
+ return new SqliteKvUnit(db, descriptor, () => {
267
+ this.units.delete(descriptor.name);
268
+ });
269
+ }
270
+ /**
271
+ * Close every open unit and release the database. Idempotent; concurrent
272
+ * and repeated calls resolve once teardown finishes.
273
+ * @returns resolution after the medium is released.
274
+ */
275
+ close() {
276
+ this.closing ??= this.doClose();
277
+ return this.closing;
278
+ }
279
+ async doClose() {
280
+ let db;
281
+ try {
282
+ db = await this.ready;
283
+ } catch {
284
+ return;
285
+ }
286
+ for (const pending of [...this.units.values()]) await (await pending.catch(() => void 0))?.close();
287
+ db.close();
288
+ }
289
+ };
290
+ /**
291
+ * Register the SQLite backend as `sqlite` on the storage hub. The disposer
292
+ * unregisters the name first, then closes the backend.
293
+ * @param ctx - Plugin context (must inject `storage`).
294
+ * @param config - Validated plugin configuration.
295
+ */
296
+ function apply(ctx, config) {
297
+ const backend = new SqliteStorageBackend(config);
298
+ ctx.effect(() => {
299
+ const dispose = ctx.storage.backend.register("sqlite", backend);
300
+ return async () => {
301
+ dispose();
302
+ await backend.close();
303
+ };
304
+ }, "storage-sqlite.registerBackend");
305
+ ctx.provide(storageBackendServiceKey("sqlite"), backend);
306
+ }
307
+ //#endregion
308
+ export { Config, STORAGE_SQLITE_SCHEMA_VERSION, SqliteStorageBackend, apply, inject, name };
@@ -0,0 +1,25 @@
1
+ //#region lib/types/invariant.js
2
+ /**
3
+ * Package-owned invariant companion for `@deepseek-ai/dsh-storage-sqlite`.
4
+ * @module @deepseek-ai/dsh-storage-sqlite/invariant
5
+ */
6
+ const PACKAGE_NAME = "@deepseek-ai/dsh-storage-sqlite";
7
+ /** Cordis companion plugin name. */
8
+ const name = "storage-sqlite-invariant";
9
+ /** Service required before the companion can reserve package ownership. */
10
+ const inject = ["invariants"];
11
+ /**
12
+ * No runtime invariant: schema-version and unit-version consistency are
13
+ * open-time checks that reject before a unit exists, and durability needs the
14
+ * backend round-trip tests in the shared KV conformance suite; this package
15
+ * exposes no continuously observable in-process relation.
16
+ */
17
+ const install = () => {};
18
+ /**
19
+ * Register this package's invariant companion.
20
+ * @param ctx - Cordis context carrying the invariant service.
21
+ * @returns the installed registration's disposer after setup succeeds.
22
+ */
23
+ const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
24
+ //#endregion
25
+ export { apply, inject, name };
@@ -0,0 +1,71 @@
1
+ /**
2
+ * SQLite storage backend for the storage hub: one database file hosts every
3
+ * routed unit, document-per-row (`key TEXT` / `value TEXT` JSON). Registers
4
+ * as backend `sqlite`; the disposer unregisters first, then closes the medium.
5
+ * @module @deepseek-ai/dsh-storage-sqlite
6
+ */
7
+ import type { Context } from '@deepseek-ai/cordis';
8
+ import z from '@deepseek-ai/schemastery';
9
+ import type { KvFacet, StorageBackend } from '@deepseek-ai/dsh-storage';
10
+ import { type JournalMode } from './schema.ts';
11
+ export { STORAGE_SQLITE_SCHEMA_VERSION, type JournalMode } from './schema.ts';
12
+ /** Cordis plugin name. */
13
+ export declare const name = "storage-sqlite";
14
+ /** The backend registers on the storage hub. */
15
+ export declare const inject: string[];
16
+ /** Plugin configuration. */
17
+ export interface Config {
18
+ /**
19
+ * Filesystem path to the SQLite database file. The special value `:memory:`
20
+ * opens an in-process database (tests). On filesystems with POSIX modes,
21
+ * missing directories and databases are created owner-only; existing path
22
+ * modes are preserved. Filesystem setup errors other than an existing
23
+ * database fail the open. The backend does not protect confidentiality or
24
+ * integrity when another principal can replace the database entry in its
25
+ * parent directory.
26
+ */
27
+ path: string;
28
+ /**
29
+ * SQLite `journal_mode` pragma. `wal` (the default) suits local disks; pick
30
+ * a rollback-journal mode (`delete`/`truncate`/`persist`) on filesystems
31
+ * where WAL's shared-memory files do not work (network mounts). See
32
+ * {@link JournalMode}.
33
+ */
34
+ journalMode?: JournalMode;
35
+ }
36
+ /** Schemastery validator for {@link Config}. */
37
+ export declare const Config: z<Config>;
38
+ /**
39
+ * The SQLite {@link StorageBackend}. Owns one `DatabaseSync` connection and
40
+ * the open-unit table; `kv.open` validates names, enforces the per-unit
41
+ * version stamp in `units`, and ensures the unit's record tables.
42
+ */
43
+ export declare class SqliteStorageBackend implements StorageBackend {
44
+ /** The key-value facet; the only shape this backend serves. */
45
+ readonly kv: KvFacet;
46
+ private readonly ready;
47
+ /** Open (or still-opening) units by name; presence is the double-open guard. */
48
+ private readonly units;
49
+ private closing;
50
+ /**
51
+ * @param config - Validated plugin configuration.
52
+ */
53
+ constructor(config: Config);
54
+ private openUnit;
55
+ private materializeUnit;
56
+ /**
57
+ * Close every open unit and release the database. Idempotent; concurrent
58
+ * and repeated calls resolve once teardown finishes.
59
+ * @returns resolution after the medium is released.
60
+ */
61
+ close(): Promise<void>;
62
+ private doClose;
63
+ }
64
+ /**
65
+ * Register the SQLite backend as `sqlite` on the storage hub. The disposer
66
+ * unregisters the name first, then closes the backend.
67
+ * @param ctx - Plugin context (must inject `storage`).
68
+ * @param config - Validated plugin configuration.
69
+ */
70
+ export declare function apply(ctx: Context, config: Config): void;
71
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Package-owned invariant companion for `@deepseek-ai/dsh-storage-sqlite`.
3
+ * @module @deepseek-ai/dsh-storage-sqlite/invariant
4
+ */
5
+ import type { Context } from '@deepseek-ai/cordis';
6
+ /** Cordis companion plugin name. */
7
+ export declare const name = "storage-sqlite-invariant";
8
+ /** Service required before the companion can reserve package ownership. */
9
+ export declare const inject: string[];
10
+ /**
11
+ * Register this package's invariant companion.
12
+ * @param ctx - Cordis context carrying the invariant service.
13
+ * @returns the installed registration's disposer after setup succeeds.
14
+ */
15
+ export declare const apply: (ctx: Context) => Promise<() => void>;
16
+ //# sourceMappingURL=invariant.d.ts.map
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Schema + open-time helpers for the SQLite storage backend: the physical
3
+ * layout version, the database open/configure sequence (permissions, pragmas,
4
+ * version stamp/reject), and the unit metadata tables. Unit record tables are
5
+ * created per descriptor in `unit.ts`.
6
+ * @module @deepseek-ai/dsh-storage-sqlite/schema
7
+ */
8
+ import { DatabaseSync } from 'node:sqlite';
9
+ /**
10
+ * The on-disk physical layout version, stored in `PRAGMA user_version`.
11
+ * Orthogonal to each unit's own `version` (stamped per unit in the `units`
12
+ * row). Bumped only on a breaking change to the table layout; any other
13
+ * stamped version rejects — this unreleased format has no migrations.
14
+ */
15
+ export declare const STORAGE_SQLITE_SCHEMA_VERSION = 1;
16
+ /**
17
+ * Journal modes the backend will run under. `wal` is the default; the
18
+ * rollback-journal modes (`delete`/`truncate`/`persist`) exist for
19
+ * filesystems where WAL's shared-memory files do not work (network mounts).
20
+ * `memory`/`off` are excluded: dropping journal durability silently
21
+ * contradicts the durability clause of the KV backend contract.
22
+ */
23
+ export type JournalMode = 'wal' | 'delete' | 'truncate' | 'persist';
24
+ /**
25
+ * Open the database and apply its schema and pragmas. Missing directories and
26
+ * database files are created owner-only (`:memory:` skips filesystem setup).
27
+ * A zero `user_version` is stamped with {@link STORAGE_SQLITE_SCHEMA_VERSION};
28
+ * every other non-current version rejects rather than being migrated in place.
29
+ * @param path - the SQLite database file to open, or `:memory:`.
30
+ * @param journalMode - validated journal pragma.
31
+ * @returns the open handle with pragmas applied and the unit metadata tables ensured.
32
+ */
33
+ export declare function openDatabase(path: string, journalMode: JournalMode): Promise<DatabaseSync>;
34
+ /**
35
+ * Physical table name for one unit table. Both segments are validated against
36
+ * `UNIT_NAME_RE` before reaching this, so the result is safe to interpolate
37
+ * into DDL and prepared-statement text.
38
+ * @param unit - Validated unit name.
39
+ * @param table - Validated table name.
40
+ * @returns the `u_<unit>_<table>` identifier.
41
+ */
42
+ export declare function recordTableName(unit: string, table: string): string;
43
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1,47 @@
1
+ /**
2
+ * One opened SQLite KV unit: prepared per-table statements over the
3
+ * `u_<unit>_<table>` record tables plus this unit's row in the shared
4
+ * `unit_globals` table. Each primitive is a single statement, so atomicity
5
+ * comes from SQLite itself — no explicit transactions, and no write queue
6
+ * (write ordering is the caller's responsibility per the KV contract).
7
+ * @module @deepseek-ai/dsh-storage-sqlite/unit
8
+ */
9
+ import type { DatabaseSync } from 'node:sqlite';
10
+ import type { KvUnit, KvUnitDescriptor } from '@deepseek-ai/dsh-storage';
11
+ /**
12
+ * The SQLite {@link KvUnit}. Constructed by the backend AFTER the unit's
13
+ * record tables exist; statements are prepared once here and reused for every
14
+ * primitive. Values are stored as JSON text in the `value` column.
15
+ */
16
+ export declare class SqliteKvUnit implements KvUnit {
17
+ private readonly descriptor;
18
+ private readonly onClose;
19
+ private readonly tables;
20
+ private readonly globalUpsert;
21
+ private readonly globalSelect;
22
+ private closed;
23
+ /**
24
+ * @param db - Open database handle owned by the backend (never closed here).
25
+ * @param descriptor - Validated descriptor whose record tables already exist.
26
+ * @param onClose - Backend callback releasing this unit's open-name slot.
27
+ */
28
+ constructor(db: DatabaseSync, descriptor: KvUnitDescriptor, onClose: () => void);
29
+ loadAll(): Promise<{
30
+ tables: Record<string, Record<string, unknown>>;
31
+ global: unknown;
32
+ }>;
33
+ /** Parse one stored value column, mapping bad JSON to `malformed-medium`. */
34
+ private parseValue;
35
+ putRecord(table: string, key: string, value: unknown): Promise<void>;
36
+ deleteRecord(table: string, key: string): Promise<void>;
37
+ setGlobal(value: unknown): Promise<void>;
38
+ close(): Promise<void>;
39
+ /**
40
+ * Run one synchronous primitive behind the closed guard, mapping a throw to
41
+ * a rejection so the Promise-returning contract never throws synchronously.
42
+ */
43
+ private settle;
44
+ private ensureOpen;
45
+ private statementsFor;
46
+ }
47
+ //# sourceMappingURL=unit.d.ts.map
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@deepseek-ai/dsh-storage-sqlite",
3
+ "description": "SQLite storage backend (kv facet) for the DeepSeek Harness storage hub",
4
+ "version": "0.0.1-rc.1",
5
+ "publishConfig": {
6
+ "access": "restricted"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
11
+ "directory": "packages/storage/storage-sqlite"
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
+ "./invariant": {
22
+ "types": "./lib/types/invariant.d.ts",
23
+ "default": "./lib/invariant.js"
24
+ },
25
+ "./src/*": "./src/*",
26
+ "./package.json": "./package.json"
27
+ },
28
+ "files": [
29
+ "lib/index.js",
30
+ "lib/invariant.js",
31
+ "lib/types/**/*.d.ts"
32
+ ],
33
+ "license": "BSD-3-Clause",
34
+ "peerDependencies": {
35
+ "@deepseek-ai/dsh-invariants": "^0.0.1-rc.1",
36
+ "@deepseek-ai/dsh-storage": "^0.0.1-rc.1",
37
+ "@deepseek-ai/cordis": "^4.0.1-rc.1"
38
+ },
39
+ "dependencies": {
40
+ "@deepseek-ai/schemastery": "^3.18.1-rc.1"
41
+ },
42
+ "devDependencies": {
43
+ "@deepseek-ai/dsh-invariants": "^0.0.1-rc.1",
44
+ "@deepseek-ai/dsh-storage": "^0.0.1-rc.1",
45
+ "@deepseek-ai/cordis": "^4.0.1-rc.1"
46
+ }
47
+ }