@whispering233/ai-editor-db 0.0.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.
Files changed (73) hide show
  1. package/dist/connection.d.ts +24 -0
  2. package/dist/connection.d.ts.map +1 -0
  3. package/dist/connection.js +39 -0
  4. package/dist/connection.js.map +1 -0
  5. package/dist/index.d.ts +15 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +17 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/migrations/index.d.ts +10 -0
  10. package/dist/migrations/index.d.ts.map +1 -0
  11. package/dist/migrations/index.js +34 -0
  12. package/dist/migrations/index.js.map +1 -0
  13. package/dist/queries/atomic.d.ts +15 -0
  14. package/dist/queries/atomic.d.ts.map +1 -0
  15. package/dist/queries/atomic.js +44 -0
  16. package/dist/queries/atomic.js.map +1 -0
  17. package/dist/queries/chat.d.ts +74 -0
  18. package/dist/queries/chat.d.ts.map +1 -0
  19. package/dist/queries/chat.js +188 -0
  20. package/dist/queries/chat.js.map +1 -0
  21. package/dist/queries/compute-state.d.ts +32 -0
  22. package/dist/queries/compute-state.d.ts.map +1 -0
  23. package/dist/queries/compute-state.js +135 -0
  24. package/dist/queries/compute-state.js.map +1 -0
  25. package/dist/queries/delta.d.ts +76 -0
  26. package/dist/queries/delta.d.ts.map +1 -0
  27. package/dist/queries/delta.js +287 -0
  28. package/dist/queries/delta.js.map +1 -0
  29. package/dist/queries/entity.d.ts +110 -0
  30. package/dist/queries/entity.d.ts.map +1 -0
  31. package/dist/queries/entity.js +285 -0
  32. package/dist/queries/entity.js.map +1 -0
  33. package/dist/queries/migration.d.ts +153 -0
  34. package/dist/queries/migration.d.ts.map +1 -0
  35. package/dist/queries/migration.js +260 -0
  36. package/dist/queries/migration.js.map +1 -0
  37. package/dist/queries/outline-ops.d.ts +126 -0
  38. package/dist/queries/outline-ops.d.ts.map +1 -0
  39. package/dist/queries/outline-ops.js +346 -0
  40. package/dist/queries/outline-ops.js.map +1 -0
  41. package/dist/queries/outline.d.ts +48 -0
  42. package/dist/queries/outline.d.ts.map +1 -0
  43. package/dist/queries/outline.js +134 -0
  44. package/dist/queries/outline.js.map +1 -0
  45. package/dist/queries/project.d.ts +21 -0
  46. package/dist/queries/project.d.ts.map +1 -0
  47. package/dist/queries/project.js +43 -0
  48. package/dist/queries/project.js.map +1 -0
  49. package/dist/queries/relation.d.ts +83 -0
  50. package/dist/queries/relation.d.ts.map +1 -0
  51. package/dist/queries/relation.js +418 -0
  52. package/dist/queries/relation.js.map +1 -0
  53. package/dist/queries/trash.d.ts +65 -0
  54. package/dist/queries/trash.d.ts.map +1 -0
  55. package/dist/queries/trash.js +121 -0
  56. package/dist/queries/trash.js.map +1 -0
  57. package/dist/schema.d.ts +29 -0
  58. package/dist/schema.d.ts.map +1 -0
  59. package/dist/schema.js +102 -0
  60. package/dist/schema.js.map +1 -0
  61. package/dist/storage/atomic.d.ts +32 -0
  62. package/dist/storage/atomic.d.ts.map +1 -0
  63. package/dist/storage/atomic.js +112 -0
  64. package/dist/storage/atomic.js.map +1 -0
  65. package/dist/storage/outline.d.ts +68 -0
  66. package/dist/storage/outline.d.ts.map +1 -0
  67. package/dist/storage/outline.js +154 -0
  68. package/dist/storage/outline.js.map +1 -0
  69. package/dist/storage/project.d.ts +24 -0
  70. package/dist/storage/project.d.ts.map +1 -0
  71. package/dist/storage/project.js +55 -0
  72. package/dist/storage/project.js.map +1 -0
  73. package/package.json +36 -0
@@ -0,0 +1,260 @@
1
+ // @whispering233/ai-editor-db schema 演进:删库重建(S1.1)+ 未来版本拒绝(E4)+ 增量迁移(E5)
2
+ //
3
+ // 单一事实来源:doc/design/decisions.md 决策 13(MVP 删库重建 + E5 增补「v0.1.0 发布终止
4
+ // 删库重建」)与修订(2026-08)——
5
+ // - 以 data.db 的 PRAGMA user_version 为准判定版本;三态分流:
6
+ // - user_version === SCHEMA_VERSION → 正常打开
7
+ // - user_version > SCHEMA_VERSION(未来版本,E4)→ **拒绝打开**(提示升级程序,
8
+ // 不触发任何重建/备份/写操作,数据原封不动)——堵「装新版后回退旧版 → 降级清零」
9
+ // - user_version < SCHEMA_VERSION(旧版本,E5)→ **有迁移路径**(MIGRATIONS 存在
10
+ // 从当前版本到目标版本的连续链)→ runMigrations 前向迁移(数据保全,迁移前
11
+ // 时间戳快照);**无迁移路径** → 删库重建兜底(决策 13,备份 v{n}.bak 留档)
12
+ // - 重建时同步重置 outline.json(先备份为 outline.json.v{n}.bak,n=旧版本号)、清空回收站
13
+ // - 旧 data.db 一并备份为 data.db.v{n}.bak;备份带版本号、不覆盖旧备份(多次重建各自留档)
14
+ // - project.json 的 schema_version 仅用于 JSON 结构判断(与 user_version 不同维度),重建不修改 project.json
15
+ // - 决策 13 增补(E5):删库重建策略于 v0.1.0 发布终止——增量迁移机制替代
16
+ // 触发语义(doc/api/endpoints.md 第 68-70 行):open 时检测,重建完成后向客户端提示。
17
+ import { copyFileSync, existsSync, rmSync } from "node:fs";
18
+ import { join } from "node:path";
19
+ import { closeDatabase, openDatabase } from "../connection.js";
20
+ import { getUserVersion, SCHEMA_VERSION, setUserVersion } from "../schema.js";
21
+ import { OUTLINE_FILE_NAME, writeOutlineFile } from "../storage/outline.js";
22
+ import { MIGRATIONS } from "../migrations/index.js";
23
+ /** data.db 文件名(决策 8:项目根目录,与 server middleware 的常量一致) */
24
+ export const DATA_DB_FILE_NAME = "data.db";
25
+ /**
26
+ * data.db user_version 高于当前程序版本(E4 拒绝打开专用错误)。
27
+ *
28
+ * 语义(release-review §一 风险 2):用户安装新版程序后回退旧版 → 旧版程序打开高版本库,
29
+ * 若按「不匹配即重建」处理会把用户全部数据重建清零(且备份仅配合旧版本回滚,普通用户
30
+ * 不可自救)。E4 起该分支改为拒绝打开并抛出本错误——**不触发任何重建/备份/写操作**,
31
+ * 数据文件原封不动;上层(server open 路由)捕获后转 409 + 明确提示升级程序。
32
+ *
33
+ * 独立于 import 侧的 SCHEMA_VERSION_MISMATCH(备份导入校验):本错误是**打开已存在项目**
34
+ * 时发现项目版本高于程序版本;SCHEMA_VERSION_MISMATCH 是导入 zip 备份时版本不匹配。
35
+ */
36
+ export class SchemaVersionError extends Error {
37
+ /** 项目 data.db 的实际 user_version(未来版本) */
38
+ version;
39
+ /** 当前程序支持的版本 */
40
+ current;
41
+ constructor(version, current) {
42
+ super(`项目 data.db 版本 (${version}) 高于当前程序版本 (${current}),请升级程序后打开`);
43
+ this.name = "SchemaVersionError";
44
+ this.version = version;
45
+ this.current = current;
46
+ }
47
+ }
48
+ /**
49
+ * schema 版本检测 + 版本对齐的高层入口(open 流程调用,endpoints.md 第 68-70 行)。
50
+ *
51
+ * - user_version 与 SCHEMA_VERSION **匹配**:直接返回,db 为原连接,rebuilt=false。
52
+ * - user_version **> SCHEMA_VERSION(未来版本,E4)**:**拒绝打开**——关闭连接并抛
53
+ * `SchemaVersionError`(提示升级程序),不触发任何重建/备份/写操作,数据原封不动。
54
+ * - user_version **< SCHEMA_VERSION(旧版本)**:
55
+ * - **有迁移路径**(opts.migrations 中存在从当前版本到 SCHEMA_VERSION 的连续迁移链,
56
+ * 默认 MIGRATIONS)→ runMigrations 前向迁移(迁移前时间戳快照,数据保全完整)
57
+ * - **无迁移路径** → rebuildProjectStorage 删库重建兜底(决策 13,E5 迁移机制
58
+ * 覆盖不到的历史版本保留;备份 v{n}.bak 留档)
59
+ *
60
+ * @param opts.migrations 迁移集注入(默认 MIGRATIONS;测试注入假迁移)
61
+ * @throws SchemaVersionError 未来版本拒绝打开(连接已由本函数关闭,无句柄泄漏)
62
+ * @throws 备份/重建/迁移过程中的 I/O 错误;迁移失败时版本停在前一迁移后(快照保留)
63
+ */
64
+ export function ensureSchemaCompatible(db, dir, dbPath, opts = {}) {
65
+ const current = getUserVersion(db);
66
+ if (current === SCHEMA_VERSION) {
67
+ return { db, result: { rebuilt: false, toVersion: SCHEMA_VERSION, backups: [] } };
68
+ }
69
+ // E4:未来版本拒绝打开(先关连接防句柄泄漏,再抛错;数据文件零触碰)
70
+ if (current > SCHEMA_VERSION) {
71
+ closeDatabase(db);
72
+ throw new SchemaVersionError(current, SCHEMA_VERSION);
73
+ }
74
+ // E5:旧版本——有迁移路径 → 前向迁移(数据保全);无迁移路径 → 重建兜底
75
+ if (hasMigrationPath(current, SCHEMA_VERSION, opts.migrations ?? MIGRATIONS)) {
76
+ try {
77
+ const { snapshot } = runMigrations(db, {
78
+ migrations: opts.migrations ?? MIGRATIONS,
79
+ targetVersion: SCHEMA_VERSION,
80
+ dbPath,
81
+ });
82
+ return {
83
+ db,
84
+ result: {
85
+ rebuilt: false,
86
+ migrated: true,
87
+ fromVersion: current,
88
+ toVersion: SCHEMA_VERSION,
89
+ backups: snapshot === null ? [] : [snapshot],
90
+ },
91
+ };
92
+ }
93
+ catch (err) {
94
+ // ora-3 S2:迁移中途失败 → 关闭本次打开的连接(对比 E4 拒绝分支与重建分支
95
+ // 均关连接;迁移失败语义 = open 未生效,不留句柄)后 rethrow
96
+ closeDatabase(db);
97
+ throw err;
98
+ }
99
+ }
100
+ return rebuildProjectStorage(db, dir, dbPath, current);
101
+ }
102
+ /**
103
+ * 判定从 fromVersion 到 targetVersion 是否存在**连续迁移链**(E5 纯函数):
104
+ * (fromVersion, targetVersion] 区间内每个版本号都恰好有迁移条目 → true。
105
+ * 连续性是硬要求——跳版本迁移意味着中间版本的数据形态未经处理,拒绝走迁移路径。
106
+ *
107
+ * @param migrations 迁移集(默认 MIGRATIONS;测试注入)
108
+ */
109
+ export function hasMigrationPath(fromVersion, targetVersion, migrations = MIGRATIONS) {
110
+ // from >= target 无迁移需求(相等/未来版本由调用方分支处理),视为「无需求即有路径」
111
+ if (fromVersion >= targetVersion)
112
+ return true;
113
+ for (let v = fromVersion + 1; v <= targetVersion; v++) {
114
+ if (!migrations.some((m) => m.version === v))
115
+ return false;
116
+ }
117
+ return true;
118
+ }
119
+ /**
120
+ * 前向执行缺失的增量迁移(E5 核心):
121
+ *
122
+ * 1. **迁移前快照一次**(整批;若提供 dbPath):checkpoint 后复制 data.db →
123
+ * `data.db.v{from}.{YYYYMMDDHHmmssZ}.bak`(时间戳命名,不覆盖旧备份)——
124
+ * 失败重试时现场保留(每次调用生成新时间戳快照,旧快照不删)
125
+ * 2. **逐个执行** (fromVersion, targetVersion] 区间内按 version 升序的迁移:
126
+ * 每个迁移一个事务(`db.transaction`):`up(db)` + `setUserVersion(m.version)`
127
+ * **原子提交**——「迁移成功 ⇒ 版本已写入;失败 ⇒ 版本未变」;失败时该迁移整体
128
+ * 回滚(含 DDL 与已写数据)、后续迁移不执行、异常向上传播(快照保留供重试)
129
+ *
130
+ * @param opts.dbPath data.db 绝对路径(提供则迁移前快照;缺省跳过快照——纯逻辑调用)
131
+ * @returns 实际执行的迁移(升序)与快照路径(未快照为 null)
132
+ */
133
+ export function runMigrations(db, opts = {}) {
134
+ const migrations = (opts.migrations ?? MIGRATIONS).slice().sort((a, b) => a.version - b.version);
135
+ const targetVersion = opts.targetVersion ?? SCHEMA_VERSION;
136
+ const fromVersion = getUserVersion(db);
137
+ const pending = migrations.filter((m) => m.version > fromVersion && m.version <= targetVersion);
138
+ if (pending.length === 0)
139
+ return { applied: [], snapshot: null };
140
+ // 1. 迁移前快照(整批一次):checkpoint 把 WAL 合并进主文件(连接打开期间写入的数据
141
+ // 在 WAL 中,直接复制主文件会缺数据)→ 复制带时间戳快照
142
+ let snapshot = null;
143
+ if (opts.dbPath !== undefined) {
144
+ checkpointWal(db);
145
+ snapshot = snapshotDbFile(opts.dbPath, fromVersion);
146
+ }
147
+ // 2. 逐个执行(每迁移一个事务,up + setUserVersion 原子)
148
+ const applied = [];
149
+ for (const m of pending) {
150
+ db.transaction(() => {
151
+ m.up(db);
152
+ setUserVersion(db, m.version);
153
+ })();
154
+ applied.push(m);
155
+ }
156
+ return { applied, snapshot };
157
+ }
158
+ /**
159
+ * 迁移前快照 data.db → `data.db.v{n}.{YYYYMMDDHHmmssSSSZ}.bak`(E5):
160
+ * 复用 rebuild 备份的复制语义(copyFileSync 复制主文件),命名带 UTC 时间戳
161
+ * (含毫秒:20260804T173000.123Z)——同版本多次迁移/重试各自留档,不覆盖。
162
+ * WAL 一致性同 backupDbFile:调用方需先 checkpoint(runMigrations 已做)。
163
+ */
164
+ export function snapshotDbFile(dbPath, fromVersion) {
165
+ const ts = new Date().toISOString().replace(/[-:]/g, ""); // 20260804T173000.123Z(保留毫秒)
166
+ const backupPath = `${dbPath}.v${fromVersion}.${ts}.bak`;
167
+ copyFileSync(dbPath, backupPath);
168
+ return backupPath;
169
+ }
170
+ /**
171
+ * WAL checkpoint(TRUNCATE):把 WAL 内容合并回主文件并截断(busy>0 说明有其他连接
172
+ * 在写——MVP 单进程单连接,不检查)。供**导出前**调用(E1:release-review §二)——
173
+ * 保证 data.db 主文件为完整快照,导出 zip 内的 data.db 无需附带 -wal/-shm。
174
+ */
175
+ export function checkpointWal(db) {
176
+ db.pragma("wal_checkpoint(TRUNCATE)");
177
+ }
178
+ /**
179
+ * 删库重建全流程(决策 13):
180
+ *
181
+ * 1. **checkpoint + 关闭连接**:`wal_checkpoint(TRUNCATE)` 把 WAL 内容合并回主文件并截断,
182
+ * 然后关闭连接——保证后续复制的是完整快照(WAL 一致性取舍见 backupDbFile 注释)
183
+ * 2. **备份 data.db** → `data.db.v{n}.bak`(n=旧版本号;文件复制,原文件随后删除重建)
184
+ * 3. **备份 outline.json** → `outline.json.v{n}.bak`(文件复制;非 SQLite 文件无 WAL 问题,
185
+ * 复制保留原始字节,不存在时跳过备份)
186
+ * 4. **删除旧库文件**(data.db + -wal + -shm,checkpoint 后 -wal 已截断但文件仍在,一并删除)
187
+ * 5. **重建空库**:openDatabase(自动建表)→ setUserVersion(SCHEMA_VERSION)
188
+ * 6. **重置 outline.json**:写最小空树(严格三层空树,决策 19;与 T2.2 readOutlineFile
189
+ * 缺失语义的空树同形)——顶层 schema_version 同步写 SCHEMA_VERSION(决策 13 修订)
190
+ *
191
+ * **清空回收站**:新库四张表全空,软删行(deleted_at 非 NULL)天然不存在,无需单独操作。
192
+ * **project.json 不动**:其 schema_version 仅用于 JSON 结构判断(决策 13),id/name 跨启动稳定。
193
+ *
194
+ * **崩溃窗口(MVP 可接受)**:步骤 4(删除旧库)与步骤 5(重建空库)之间进程崩溃 →
195
+ * 磁盘上无 data.db(下次 open 自动重建空库),且旧库备份已留档不丢数据;
196
+ * 但同版本号多次重建会覆盖旧备份(命名 v{n}.bak 不带时间戳,决策 13 修订的
197
+ * 「各自留档」仅对不同版本号成立)——MVP 阶段无真实用户数据,可接受。
198
+ *
199
+ * @param db 待重建的连接(流程内关闭;调用方丢弃原引用,使用返回值中的新连接)
200
+ * @param dir 项目根目录
201
+ * @param dbPath data.db 绝对路径
202
+ * @param oldVersion 重建前的旧版本号(备份文件名与 fromVersion 用)
203
+ * @returns 新连接(已开库、版本号已写、表已建)+ 重建结果
204
+ */
205
+ export function rebuildProjectStorage(db, dir, dbPath, oldVersion) {
206
+ // 1. WAL 合并回主文件后关闭连接(busy>0 说明有其他连接在写——MVP 单进程单连接,不检查)
207
+ checkpointWal(db);
208
+ closeDatabase(db);
209
+ // 2/3. 备份(复制而非 rename:原文件在重建流程中继续使用/删除)
210
+ const dbBackup = backupDbFile(dbPath, oldVersion);
211
+ const outlineBackup = backupOutlineFile(dir, oldVersion);
212
+ // 4. 删除旧库文件(含 WAL/SHM 伴生文件)
213
+ rmSync(dbPath, { force: true });
214
+ rmSync(`${dbPath}-wal`, { force: true });
215
+ rmSync(`${dbPath}-shm`, { force: true });
216
+ // 5. 重建空库并写版本号(openDatabase 自动建表 + WAL + synchronous=FULL)
217
+ const fresh = openDatabase(dbPath);
218
+ setUserVersion(fresh, SCHEMA_VERSION);
219
+ // 6. 重置 outline.json 为最小空树(决策 13 修订:顶层 schema_version 同步写入)
220
+ const emptyTree = { id: "root", type: "root", schema_version: SCHEMA_VERSION, children: [] };
221
+ writeOutlineFile(dir, emptyTree);
222
+ const backups = [dbBackup, ...(outlineBackup === null ? [] : [outlineBackup])];
223
+ return {
224
+ db: fresh,
225
+ result: { rebuilt: true, fromVersion: oldVersion, toVersion: SCHEMA_VERSION, backups },
226
+ };
227
+ }
228
+ /**
229
+ * 备份 data.db 主文件 → `data.db.v{n}.bak`(决策 13:备份带版本号、不覆盖旧备份)。
230
+ *
231
+ * **WAL 一致性取舍**:调用方必须在连接关闭前先执行 `wal_checkpoint(TRUNCATE)`(rebuildProjectStorage
232
+ * 已做)——checkpoint 后主文件即完整快照,只需复制主文件;-wal/-shm 内容已合并进主文件
233
+ * (TRUNCATE 后 -wal 为空文件),不再复制。若库处于 DELETE journal 模式(从未开过 WAL),
234
+ * 主文件本身即全部数据,同样安全。
235
+ *
236
+ * @returns 备份文件绝对路径
237
+ */
238
+ export function backupDbFile(dbPath, oldVersion) {
239
+ const backupPath = `${dbPath}.v${oldVersion}.bak`;
240
+ copyFileSync(dbPath, backupPath);
241
+ return backupPath;
242
+ }
243
+ /**
244
+ * 备份 outline.json → `outline.json.v{n}.bak`(决策 13 修订:重建时同步重置并留档)。
245
+ *
246
+ * 取舍:用复制而非 rename——outline.json 不是 SQLite 文件、无 WAL 问题,复制保留原始字节
247
+ * (含格式/顺序);rename 会把原文件移走,与「随后重置为空树」的流程相斥。
248
+ * outline.json 不存在(异常状态)时跳过备份返回 null——重置仍会执行(写空树)。
249
+ *
250
+ * @returns 备份文件绝对路径;文件不存在返回 null
251
+ */
252
+ export function backupOutlineFile(dir, oldVersion) {
253
+ const src = join(dir, OUTLINE_FILE_NAME);
254
+ if (!existsSync(src))
255
+ return null;
256
+ const backupPath = join(dir, `${OUTLINE_FILE_NAME}.v${oldVersion}.bak`);
257
+ copyFileSync(src, backupPath);
258
+ return backupPath;
259
+ }
260
+ //# sourceMappingURL=migration.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migration.js","sourceRoot":"","sources":["../../src/queries/migration.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,EAAE;AACF,oEAAoE;AACpE,uBAAuB;AACvB,iDAAiD;AACjD,6CAA6C;AAC7C,+DAA+D;AAC/D,iDAAiD;AACjD,qEAAqE;AACrE,mDAAmD;AACnD,sDAAsD;AACtD,kEAAkE;AAClE,6DAA6D;AAC7D,wFAAwF;AACxF,+CAA+C;AAC/C,6DAA6D;AAE7D,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAC3D,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,aAAa,EAAE,YAAY,EAAW,MAAM,kBAAkB,CAAC;AACxE,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9E,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAC5E,OAAO,EAAE,UAAU,EAAkB,MAAM,wBAAwB,CAAC;AAEpE,wDAAwD;AACxD,MAAM,CAAC,MAAM,iBAAiB,GAAG,SAAS,CAAC;AAuB3C;;;;;;;;;;GAUG;AACH,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAC3C,wCAAwC;IAC/B,OAAO,CAAS;IACzB,gBAAgB;IACP,OAAO,CAAS;IACzB,YAAY,OAAe,EAAE,OAAe;QAC1C,KAAK,CAAC,kBAAkB,OAAO,eAAe,OAAO,YAAY,CAAC,CAAC;QACnE,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;QACjC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;CACF;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,sBAAsB,CACpC,EAAM,EACN,GAAW,EACX,MAAc,EACd,OAA8C,EAAE;IAEhD,MAAM,OAAO,GAAG,cAAc,CAAC,EAAE,CAAC,CAAC;IACnC,IAAI,OAAO,KAAK,cAAc,EAAE,CAAC;QAC/B,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,OAAO,EAAE,EAAE,EAAE,EAAE,CAAC;IACpF,CAAC;IACD,qCAAqC;IACrC,IAAI,OAAO,GAAG,cAAc,EAAE,CAAC;QAC7B,aAAa,CAAC,EAAE,CAAC,CAAC;QAClB,MAAM,IAAI,kBAAkB,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC;IACxD,CAAC;IACD,0CAA0C;IAC1C,IAAI,gBAAgB,CAAC,OAAO,EAAE,cAAc,EAAE,IAAI,CAAC,UAAU,IAAI,UAAU,CAAC,EAAE,CAAC;QAC7E,IAAI,CAAC;YACH,MAAM,EAAE,QAAQ,EAAE,GAAG,aAAa,CAAC,EAAE,EAAE;gBACrC,UAAU,EAAE,IAAI,CAAC,UAAU,IAAI,UAAU;gBACzC,aAAa,EAAE,cAAc;gBAC7B,MAAM;aACP,CAAC,CAAC;YACH,OAAO;gBACL,EAAE;gBACF,MAAM,EAAE;oBACN,OAAO,EAAE,KAAK;oBACd,QAAQ,EAAE,IAAI;oBACd,WAAW,EAAE,OAAO;oBACpB,SAAS,EAAE,cAAc;oBACzB,OAAO,EAAE,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;iBAC7C;aACF,CAAC;QACJ,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,8CAA8C;YAC9C,wCAAwC;YACxC,aAAa,CAAC,EAAE,CAAC,CAAC;YAClB,MAAM,GAAG,CAAC;QACZ,CAAC;IACH,CAAC;IACD,OAAO,qBAAqB,CAAC,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;AACzD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAC9B,WAAmB,EACnB,aAAqB,EACrB,aAAmC,UAAU;IAE7C,oDAAoD;IACpD,IAAI,WAAW,IAAI,aAAa;QAAE,OAAO,IAAI,CAAC;IAC9C,KAAK,IAAI,CAAC,GAAG,WAAW,GAAG,CAAC,EAAE,CAAC,IAAI,aAAa,EAAE,CAAC,EAAE,EAAE,CAAC;QACtD,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC;YAAE,OAAO,KAAK,CAAC;IAC7D,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,aAAa,CAC3B,EAAM,EACN,OAAuF,EAAE;IAEzF,MAAM,UAAU,GAAG,CAAC,IAAI,CAAC,UAAU,IAAI,UAAU,CAAC,CAAC,KAAK,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC;IACjG,MAAM,aAAa,GAAG,IAAI,CAAC,aAAa,IAAI,cAAc,CAAC;IAC3D,MAAM,WAAW,GAAG,cAAc,CAAC,EAAE,CAAC,CAAC;IACvC,MAAM,OAAO,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,GAAG,WAAW,IAAI,CAAC,CAAC,OAAO,IAAI,aAAa,CAAC,CAAC;IAChG,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAEjE,qDAAqD;IACrD,oCAAoC;IACpC,IAAI,QAAQ,GAAkB,IAAI,CAAC;IACnC,IAAI,IAAI,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QAC9B,aAAa,CAAC,EAAE,CAAC,CAAC;QAClB,QAAQ,GAAG,cAAc,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IACtD,CAAC;IAED,0CAA0C;IAC1C,MAAM,OAAO,GAAgB,EAAE,CAAC;IAChC,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE;YAClB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;YACT,cAAc,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC;QAChC,CAAC,CAAC,EAAE,CAAC;QACL,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC;AAC/B,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,MAAc,EAAE,WAAmB;IAChE,MAAM,EAAE,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC,6BAA6B;IACvF,MAAM,UAAU,GAAG,GAAG,MAAM,KAAK,WAAW,IAAI,EAAE,MAAM,CAAC;IACzD,YAAY,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACjC,OAAO,UAAU,CAAC;AACpB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,EAAM;IAClC,EAAE,CAAC,MAAM,CAAC,0BAA0B,CAAC,CAAC;AACxC,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,UAAU,qBAAqB,CAAC,EAAM,EAAE,GAAW,EAAE,MAAc,EAAE,UAAkB;IAC3F,uDAAuD;IACvD,aAAa,CAAC,EAAE,CAAC,CAAC;IAClB,aAAa,CAAC,EAAE,CAAC,CAAC;IAElB,wCAAwC;IACxC,MAAM,QAAQ,GAAG,YAAY,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IAClD,MAAM,aAAa,GAAG,iBAAiB,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC;IAEzD,4BAA4B;IAC5B,MAAM,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAChC,MAAM,CAAC,GAAG,MAAM,MAAM,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACzC,MAAM,CAAC,GAAG,MAAM,MAAM,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAEzC,2DAA2D;IAC3D,MAAM,KAAK,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;IACnC,cAAc,CAAC,KAAK,EAAE,cAAc,CAAC,CAAC;IAEtC,4DAA4D;IAC5D,MAAM,SAAS,GAAoB,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,cAAc,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;IAC9G,gBAAgB,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;IAEjC,MAAM,OAAO,GAAG,CAAC,QAAQ,EAAE,GAAG,CAAC,aAAa,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;IAC/E,OAAO;QACL,EAAE,EAAE,KAAK;QACT,MAAM,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,cAAc,EAAE,OAAO,EAAE;KACvF,CAAC;AACJ,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,YAAY,CAAC,MAAc,EAAE,UAAkB;IAC7D,MAAM,UAAU,GAAG,GAAG,MAAM,KAAK,UAAU,MAAM,CAAC;IAClD,YAAY,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACjC,OAAO,UAAU,CAAC;AACpB,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAAW,EAAE,UAAkB;IAC/D,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,iBAAiB,CAAC,CAAC;IACzC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAClC,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,iBAAiB,KAAK,UAAU,MAAM,CAAC,CAAC;IACxE,YAAY,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC;IAC9B,OAAO,UAAU,CAAC;AACpB,CAAC"}
@@ -0,0 +1,126 @@
1
+ import type { OutlineFileNode, OutlineNodeType } from "@whispering233/ai-editor-shared";
2
+ /** 大纲操作错误码(server 层映射 HttpError,见文件头注释) */
3
+ export type OutlineErrorCode = "NODE_NOT_FOUND" | "PARENT_NOT_FOUND" | "INVALID_HIERARCHY" | "OUTLINE_ANCESTOR_DELETED";
4
+ /** 大纲操作错误:带 code,供上层精确映射 HTTP 语义(替代 message 字符串匹配) */
5
+ export declare class OutlineError extends Error {
6
+ readonly code: OutlineErrorCode;
7
+ constructor(code: OutlineErrorCode, message: string);
8
+ }
9
+ /**
10
+ * 严格三层约束校验(决策 19):childType 能否挂 parentType 下。
11
+ * volume → root;chapter → volume 或 root;scene → 必须 chapter。
12
+ */
13
+ export declare function assertCanHold(parentType: "root" | OutlineNodeType, childType: OutlineNodeType): void;
14
+ /**
15
+ * 创建大纲节点(POST /api/v1/outline,endpoints.md 第 550-577 行):
16
+ * 读树 → 校验父存在 + 三层约束 → 生成 id(shared generateOutlineNodeId:vol-/ch-/sc- 前缀)
17
+ * → 插入父 children 尾部 → **父节点 updated_at 统一更新**(决策 19)→ 原子写回。
18
+ *
19
+ * @param updatedAt ISO 8601 由调用方(应用层)传入,模块不生成时间
20
+ * @param data 节点结构化信息(决策 23,可选;校验由路由层按层级 schema 执行,本模块仅透传存储)
21
+ * @returns 新建节点(含 id/type/title/updated_at/data)
22
+ * @throws OutlineError PARENT_NOT_FOUND(父不存在)/ INVALID_HIERARCHY(三层违反)
23
+ */
24
+ export declare function createOutlineNode(dir: string, input: {
25
+ type: Exclude<OutlineNodeType, "root">;
26
+ title: string;
27
+ parentId: string;
28
+ summary?: string;
29
+ data?: Record<string, unknown>;
30
+ updatedAt: string;
31
+ }): OutlineFileNode;
32
+ /**
33
+ * 更新大纲节点信息(PUT /api/v1/outline/:nodeId,endpoints.md 第 579-597 行):
34
+ * title/summary 更新 + 节点 updated_at 统一更新(决策 19),原子写回;
35
+ * data **浅合并**(决策 23:未传字段保留,与实体 updateEntity 的 data 浅合并同语义)。
36
+ * @throws OutlineError NODE_NOT_FOUND
37
+ */
38
+ export declare function updateOutlineNodeInfo(dir: string, nodeId: string, patch: {
39
+ title?: string;
40
+ summary?: string;
41
+ data?: Record<string, unknown>;
42
+ }, updatedAt: string): void;
43
+ /**
44
+ * 移动大纲节点(PUT /api/v1/outline/:nodeId/move,endpoints.md 第 599-621 行):
45
+ * 从原父移除 → 插入新父指定 order(0-based;同父重排 = 移除后插入)→
46
+ * 三层约束校验(同 create)→ 新旧父 updated_at 统一更新(决策 19)→ 原子写回。
47
+ * order 越界时 clamp 到有效范围(拖拽场景边界值,宽松处理)。
48
+ *
49
+ * @returns { previousParentId, newParentId }(endpoints.md 语义;root 用 "root")
50
+ * @throws OutlineError NODE_NOT_FOUND / PARENT_NOT_FOUND / INVALID_HIERARCHY
51
+ */
52
+ export declare function moveOutlineNode(dir: string, nodeId: string, input: {
53
+ parentId: string;
54
+ order: number;
55
+ }, updatedAt: string): {
56
+ previousParentId: string;
57
+ newParentId: string;
58
+ };
59
+ /**
60
+ * 软删大纲节点(DELETE /api/v1/outline/:nodeId,endpoints.md 第 623-640 行,决策 12):
61
+ * 标记 deleted + deleted_at,**递归软删整棵子树**(本体保留可还原)。
62
+ * 已软删节点再次软删:幂等重标(不报错)。
63
+ *
64
+ * @returns { children } 级联软删的子节点数(不含自身)
65
+ * —— relations/deltas 的级联软删由上层(S2.2)组合 relation/delta 模块处理(本模块边界)
66
+ * @throws OutlineError NODE_NOT_FOUND
67
+ */
68
+ export declare function deleteOutlineNode(dir: string, nodeId: string, updatedAt: string): {
69
+ children: number;
70
+ };
71
+ /**
72
+ * 还原软删大纲节点(POST /api/v1/trash/outline/:nodeId/restore,endpoints.md 第 694-711 行):
73
+ * 清除 deleted/deleted_at → **递归还原子树**(仍软删的子孙一并还原,决策 12 修订)→
74
+ * **祖先链校验**:存在软删祖先 → 抛 OUTLINE_ANCESTOR_DELETED(409 语义,决策 12 修订,
75
+ * 杜绝「可见节点挂在不可见父」的畸形树——校验在还原前执行,祖先必须已还原)。
76
+ *
77
+ * @returns { children } 级联还原的子节点数
78
+ * @throws OutlineError NODE_NOT_FOUND / OUTLINE_ANCESTOR_DELETED
79
+ */
80
+ export declare function restoreOutlineNode(dir: string, nodeId: string, updatedAt: string): {
81
+ children: number;
82
+ };
83
+ /**
84
+ * 物理删除大纲节点(DELETE /api/v1/trash/outline/:nodeId,endpoints.md 第 726-736 行):
85
+ * 从树中移除整棵子树(递归,文件内物理清除,不可恢复)。
86
+ * 边界:不涉及 data.db(relations/deltas 的物理清除由上层组合);
87
+ * 不更新父节点 updated_at——purge 属回收站清理,节点已消失,
88
+ * 基于它的提案快照必然失效(节点不存在),父版本戳无需联动。
89
+ * @throws OutlineError NODE_NOT_FOUND
90
+ */
91
+ export declare function purgeOutlineNode(dir: string, nodeId: string): void;
92
+ /** 回收站节点条目(GET /api/v1/trash nodes 项,endpoints.md 第 668-674 行) */
93
+ export interface DeletedOutlineNodeInfo {
94
+ id: string;
95
+ type: Exclude<OutlineNodeType, "root">;
96
+ title: string;
97
+ deleted_at: string;
98
+ }
99
+ /**
100
+ * 回收站列表(大纲侧):收集整棵树中 deleted 标记的节点(含子树中的软删节点),
101
+ * 按 deleted_at 倒序(回收站排序约定,schema.md:跨 SQLite 与 outline.json 统一 ISO 格式)。
102
+ */
103
+ export declare function listDeletedNodes(dir: string): DeletedOutlineNodeInfo[];
104
+ /** 章节序条目(决策 21):全局章序号(跨卷连续) */
105
+ export interface ChapterOrderInfo {
106
+ chapterId: string;
107
+ /** 全局章序号,从 1 起(root → 卷 → 章先序遍历,跨卷连续累计) */
108
+ chapterNumber: number;
109
+ }
110
+ /**
111
+ * 章节序推导(决策 21):按大纲树先序遍历编号——root → 卷 → 章;
112
+ * 全局章序号**跨卷连续**;直接挂 root 的 chapter 按兄弟顺序编号(决策 19 允许);
113
+ * scene 归入所属章,不单独编号(见 getChapterNumber)。
114
+ */
115
+ export declare function deriveChapterOrder(dir: string): ChapterOrderInfo[];
116
+ /**
117
+ * 节点所属章序号(决策 21):
118
+ * - scene → 所属 chapter 的全局序号(严格三层下 scene 的父必为 chapter)
119
+ * - chapter → 自身序号
120
+ * - volume / root / 节点不存在 → null(卷无章节号;不存在防御性返回 null 供上层展示)
121
+ */
122
+ export declare function getChapterNumber(dir: string, nodeId: string): {
123
+ chapterId: string;
124
+ chapterNumber: number;
125
+ } | null;
126
+ //# sourceMappingURL=outline-ops.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"outline-ops.d.ts","sourceRoot":"","sources":["../../src/queries/outline-ops.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,eAAe,EAAmB,eAAe,EAAE,MAAM,iCAAiC,CAAC;AASzG,2CAA2C;AAC3C,MAAM,MAAM,gBAAgB,GACxB,gBAAgB,GAChB,kBAAkB,GAClB,mBAAmB,GACnB,0BAA0B,CAAC;AAE/B,sDAAsD;AACtD,qBAAa,YAAa,SAAQ,KAAK;IACrC,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;gBACpB,IAAI,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM;CAKpD;AAOD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,eAAe,EAAE,SAAS,EAAE,eAAe,GAAG,IAAI,CAWpG;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,MAAM,EACX,KAAK,EAAE;IACL,IAAI,EAAE,OAAO,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,SAAS,EAAE,MAAM,CAAC;CACnB,GACA,eAAe,CAgCjB;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,EACd,KAAK,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,EAC3E,SAAS,EAAE,MAAM,GAChB,IAAI,CAcN;AAwBD;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,EACd,KAAK,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,EAC1C,SAAS,EAAE,MAAM,GAChB;IAAE,gBAAgB,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CA6CnD;AAcD;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAYtG;AAcD;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAuBvG;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAalE;AAED,kEAAkE;AAClE,MAAM,WAAW,sBAAsB;IACrC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,OAAO,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,sBAAsB,EAAE,CAWtE;AAED,+BAA+B;AAC/B,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,2CAA2C;IAC3C,aAAa,EAAE,MAAM,CAAC;CACvB;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,gBAAgB,EAAE,CAgBlE;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,GACb;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAarD"}