@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,121 @@
1
+ // @whispering233/ai-editor-db 回收站数据层(S4.1):实体列表/级联还原/物理清除 + 级联 helper(自 server 下沉)
2
+ //
3
+ // 单一事实来源:
4
+ // - doc/api/endpoints.md 第 660-736 行(回收站端点契约:GET /trash、POST restore、DELETE purge)
5
+ // - doc/design/decisions.md 决策 12(软删/级联还原/物理清除;2026-08 修订:软删/还原更新
6
+ // updated_at;restore 级联还原全部关系不因另一端仍软删而跳过;手动删关系 = 物理删)
7
+ // - doc/ui/pages/trash.md(列表字段:entities[].{ id, type, name, deleted_at },deleted_at 倒序)
8
+ //
9
+ // 边界(与路由层的分工):
10
+ // - 本模块只提供 data.db 侧原子操作;「先 DB 后 JSON」写序(决策 16)与 outline restore 的
11
+ // 祖先链 409 校验由路由层组合(S4.3 保持现状,不在本层实现)。
12
+ // - 「purge 仅用于回收站清理」(未软删拒绝)属路由层语义拦截,本层不校验
13
+ // (与 outline purge 路由的 isSoftDeleted 拦截同构)。
14
+ // - 可见性过滤(端点仍软删的关系/Delta 不可见)由查询层(relation.ts / entity.ts)负责,
15
+ // restore 层不做——全部还原(决策 12 修订),端点还原后自动可见。
16
+ //
17
+ // 级联 helper(cascadeRestore/cascadePurge)自 server/routes/outline.ts 下沉(trash.ts 注释
18
+ // 留痕的「S3 建模块后可下沉」项,S4.1 兑现):参数与 SQL 语义不变,仅补充单库事务包裹。
19
+ import { nowIso } from "../storage/atomic.js";
20
+ import { withTransaction } from "../connection.js";
21
+ /**
22
+ * 回收站列表(实体侧,GET /api/v1/trash):entities 表 deleted_at IS NOT NULL,
23
+ * 按 deleted_at 倒序(回收站排序约定——ISO 字符串字典序即时间序,与 listDeletedNodes 一致)。
24
+ * 常规查询默认过滤软删(决策 12),回收站 API 是访问软删对象的唯一入口。
25
+ */
26
+ export function listDeletedEntities(db) {
27
+ const rows = db
28
+ .prepare("SELECT id, type, name, deleted_at FROM entities WHERE deleted_at IS NOT NULL ORDER BY deleted_at DESC")
29
+ .all();
30
+ return rows.map((r) => ({ id: r.id, type: r.type, name: r.name, deletedAt: r.deleted_at }));
31
+ }
32
+ /**
33
+ * 还原软删实体(POST /api/v1/trash/entity/:type/:id/restore,endpoints.md 第 676-692 行,决策 12 修订):
34
+ * - 自身:deleted_at 置 NULL + **updated_at 刷新**(决策 12 修订:与常规编辑一致,
35
+ * 保证决策 14 提案快照比对语义统一——还原后基于旧快照的提案必然 PROPOSAL_STALE)
36
+ * - 关联关系:relation_records(source_id = id **或** target_id = id)deleted_at 置 NULL——
37
+ * **全部还原**,不因另一端仍软删而跳过(决策 12 修订:数据永不丢失;端点仍软删的
38
+ * 关系由查询层可见性过滤,端点还原后自动可见)
39
+ * - 关联 Delta:delta_records(target_id = id)同规则
40
+ * 单库事务(withTransaction,原子性);幂等:实体不存在、类型不匹配或未软删 → 返回 null
41
+ * 且无任何副作用(存在性检查限定 deleted_at IS NOT NULL,级联 UPDATE 均带
42
+ * deleted_at IS NOT NULL 过滤——已还原的行不重复计数)。
43
+ *
44
+ * @param type 实体类型(路由路径参数;与 id 联合校验,类型不匹配视同不存在)
45
+ * @returns { restoredRelations, restoredDeltas } 实际还原的级联行数;实体不在回收站返回 null
46
+ */
47
+ export function restoreEntity(db, type, id) {
48
+ return withTransaction(db, () => {
49
+ const exists = db
50
+ .prepare("SELECT id FROM entities WHERE id = ? AND type = ? AND deleted_at IS NOT NULL")
51
+ .get(id, type);
52
+ if (exists === undefined)
53
+ return null;
54
+ const rel = db
55
+ .prepare(`UPDATE relation_records SET deleted_at = NULL WHERE deleted_at IS NOT NULL AND (source_id = ? OR target_id = ?)`)
56
+ .run(id, id);
57
+ const delta = db
58
+ .prepare(`UPDATE delta_records SET deleted_at = NULL WHERE deleted_at IS NOT NULL AND target_id = ?`)
59
+ .run(id);
60
+ db.prepare("UPDATE entities SET deleted_at = NULL, updated_at = ? WHERE id = ?").run(nowIso(), id);
61
+ return { restoredRelations: rel.changes, restoredDeltas: delta.changes };
62
+ });
63
+ }
64
+ /**
65
+ * 物理清除软删实体(DELETE /api/v1/trash/entity/:type/:id,endpoints.md 第 713-724 行):
66
+ * 不可恢复——DELETE 实体本体 + 关联 relations(source_id = id 或 target_id = id)+
67
+ * deltas(target_id = id)。单库事务。
68
+ * 幂等:实体不存在或类型不匹配 → 返回 null 且无副作用。
69
+ *
70
+ * 注意:本层**不校验「必须已软删」**——「purge 仅用于回收站清理」的语义拦截由路由层负责
71
+ * (S4.3,与 outline purge 的 isSoftDeleted 拦截同构),本层只保证原子物理清除。
72
+ *
73
+ * @returns true(清除成功);实体不存在返回 null
74
+ */
75
+ export function purgeEntity(db, type, id) {
76
+ return withTransaction(db, () => {
77
+ const exists = db.prepare("SELECT id FROM entities WHERE id = ? AND type = ?").get(id, type);
78
+ if (exists === undefined)
79
+ return null;
80
+ db.prepare("DELETE FROM relation_records WHERE source_id = ? OR target_id = ?").run(id, id);
81
+ db.prepare("DELETE FROM delta_records WHERE target_id = ?").run(id);
82
+ db.prepare("DELETE FROM entities WHERE id = ?").run(id);
83
+ return true;
84
+ });
85
+ }
86
+ /** 生成 SQL IN 占位符串(id 集来自服务端生成的 nanoid,无注入面;空集返回 "(NULL)" 恒假) */
87
+ function inPlaceholders(ids) {
88
+ return ids.length === 0 ? "(NULL)" : `(${ids.map(() => "?").join(",")})`;
89
+ }
90
+ /**
91
+ * 级联还原子树关联的 relation/delta(决策 12 修订:全部还原,不因另一端仍软删而跳过;
92
+ * 自 server/routes/outline.ts 下沉,参数与 SQL 语义不变):
93
+ * - relations:source 或 target 命中子树任一节点 → deleted_at 置 NULL(含 plot_edge 画布连线)
94
+ * - deltas:node_id 命中子树任一节点 → deleted_at 置 NULL(delta_records.node_id = 触发节点列)
95
+ * @returns { relations, deltas } 实际还原数(UPDATE.changes)
96
+ */
97
+ export function cascadeRestore(db, subtreeIds) {
98
+ return withTransaction(db, () => {
99
+ const rel = db
100
+ .prepare(`UPDATE relation_records SET deleted_at = NULL WHERE deleted_at IS NOT NULL AND
101
+ (source_id IN ${inPlaceholders(subtreeIds)} OR target_id IN ${inPlaceholders(subtreeIds)})`)
102
+ .run(...subtreeIds, ...subtreeIds);
103
+ const delta = db
104
+ .prepare(`UPDATE delta_records SET deleted_at = NULL WHERE deleted_at IS NOT NULL AND node_id IN ${inPlaceholders(subtreeIds)}`)
105
+ .run(...subtreeIds);
106
+ return { relations: rel.changes, deltas: delta.changes };
107
+ });
108
+ }
109
+ /**
110
+ * 物理清除子树关联的 relation/delta(purge,决策 12:物理清除且不可恢复;
111
+ * 自 server/routes/outline.ts 下沉,参数与 SQL 语义不变):
112
+ * - relations:source 或 target 命中子树任一节点 → 物理删除
113
+ * - deltas:node_id 命中子树任一节点 → 物理删除
114
+ */
115
+ export function cascadePurge(db, subtreeIds) {
116
+ withTransaction(db, () => {
117
+ db.prepare(`DELETE FROM relation_records WHERE source_id IN ${inPlaceholders(subtreeIds)} OR target_id IN ${inPlaceholders(subtreeIds)}`).run(...subtreeIds, ...subtreeIds);
118
+ db.prepare(`DELETE FROM delta_records WHERE node_id IN ${inPlaceholders(subtreeIds)}`).run(...subtreeIds);
119
+ });
120
+ }
121
+ //# sourceMappingURL=trash.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trash.js","sourceRoot":"","sources":["../../src/queries/trash.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ import type Database from "better-sqlite3";
2
+ /**
3
+ * data.db 当前 schema 版本(决策 13)。
4
+ * MVP 采用删库重建:以 PRAGMA user_version 为准判定版本,不匹配即备份并重建,无迁移脚本(YAGNI)。
5
+ */
6
+ export declare const SCHEMA_VERSION = 1;
7
+ /**
8
+ * 四张业务表 + 索引的建表 SQL(幂等:CREATE TABLE/INDEX IF NOT EXISTS)。
9
+ *
10
+ * - entities 实体(character/setting/location/hook),type 受 CHECK 约束,data 存 JSON,软删列 deleted_at(决策 12)
11
+ * - relation_records 通用关系表(含 plot_edge 剧情连线),3 个部分索引 WHERE deleted_at IS NULL(决策 12 修订)
12
+ * - delta_records 属性变更记录("order" 列引号保留——ORDER 是 SQLite 关键字)
13
+ * - chat_messages 对话历史(决策 18),含 project_id/tool_call_id,会话索引 (session_id, created_at)
14
+ */
15
+ export declare const CREATE_TABLES_SQL = "\n-- entities\uFF1A\u5B9E\u4F53\u8868\uFF08\u4EBA\u7269/\u8BBE\u5B9A/\u5730\u70B9/\u4F0F\u7B14\uFF09\nCREATE TABLE IF NOT EXISTS entities (\n id TEXT PRIMARY KEY,\n type TEXT NOT NULL CHECK(type IN ('character', 'setting', 'location', 'hook')),\n name TEXT NOT NULL,\n data TEXT NOT NULL DEFAULT '{}', -- JSON: \u5404\u7C7B\u578B\u7684\u4E13\u5C5E\u5B57\u6BB5\n created_at TEXT NOT NULL, -- ISO 8601\uFF0C\u5E94\u7528\u5C42\u5199\u5165\n updated_at TEXT NOT NULL, -- ISO 8601\uFF0C\u5E94\u7528\u5C42\u5199\u5165\uFF08\u63D0\u6848\u5FEB\u7167\u6BD4\u5BF9\uFF0C\u51B3\u7B56 14\uFF09\n deleted_at TEXT -- \u8F6F\u5220\u6807\u8BB0\uFF08\u51B3\u7B56 12\uFF09\uFF0CNULL \u8868\u793A\u672A\u5220\u9664\uFF1B\u975E NULL \u65F6\u8BE5\u5B9E\u4F53\u8FDB\u5165\u56DE\u6536\u7AD9\uFF0C\u672C\u4F53\u4FDD\u7559\u53EF\u8FD8\u539F\n);\n\n-- relation_records\uFF1A\u901A\u7528\u5173\u7CFB\u8868\nCREATE TABLE IF NOT EXISTS relation_records (\n id TEXT PRIMARY KEY,\n source_type TEXT NOT NULL, -- \u7AEF\u70B9\u7C7B\u578B\uFF1A\u5B9E\u4F53 'character'|'setting'|'location'|'hook'\uFF0C\u5927\u7EB2\u8282\u70B9 'outline_node'\n source_id TEXT NOT NULL,\n target_type TEXT NOT NULL,\n target_id TEXT NOT NULL,\n relation_type TEXT NOT NULL,\n metadata TEXT, -- JSON \u6269\u5C55\u5143\u6570\u636E\n created_at TEXT NOT NULL, -- ISO 8601\uFF0C\u5E94\u7528\u5C42\u5199\u5165\n updated_at TEXT NOT NULL, -- ISO 8601\uFF0C\u5E94\u7528\u5C42\u5199\u5165\uFF08\u63D0\u6848\u5FEB\u7167\u6BD4\u5BF9\uFF0C\u51B3\u7B56 14\uFF1B\u8F6F\u5220/\u8FD8\u539F\u4EA6\u66F4\u65B0\uFF0C\u51B3\u7B56 12 \u4FEE\u8BA2\uFF09\n deleted_at TEXT -- \u7EA7\u8054\u8F6F\u5220\u6807\u8BB0\uFF08\u51B3\u7B56 12\uFF09\uFF1A\u4EC5\u5B9E\u4F53/\u8282\u70B9\u7EA7\u8054\u5220\u9664\u65F6\u5199\u5165\uFF1B\n -- \u624B\u52A8\u5220\u9664\u5173\u7CFB = \u7269\u7406\u5220\uFF08\u4E0D\u7F6E deleted_at\uFF0C\u4E0D\u8FDB\u5165\u56DE\u6536\u7AD9\uFF09\n);\n\n-- \u7D22\u5F15\uFF08\u51B3\u7B56 12 \u4FEE\u8BA2\u8865\uFF09\uFF1Ak \u8DF3\u904D\u5386\u4E0E\u9AD8\u9891\u5173\u7CFB\u67E5\u8BE2\uFF08\u90E8\u5206\u7D22\u5F15\uFF0C\u4EC5\u8986\u76D6\u672A\u8F6F\u5220\u884C\uFF09\nCREATE INDEX IF NOT EXISTS idx_relation_source ON relation_records(source_id) WHERE deleted_at IS NULL;\nCREATE INDEX IF NOT EXISTS idx_relation_target ON relation_records(target_id) WHERE deleted_at IS NULL;\nCREATE INDEX IF NOT EXISTS idx_relation_type ON relation_records(relation_type) WHERE deleted_at IS NULL;\n\n-- delta_records\uFF1A\u5C5E\u6027\u53D8\u66F4\u8868\nCREATE TABLE IF NOT EXISTS delta_records (\n id TEXT PRIMARY KEY,\n node_id TEXT NOT NULL, -- \u89E6\u53D1\u53D8\u66F4\u7684\u5927\u7EB2\u8282\u70B9\n target_type TEXT NOT NULL,\n target_id TEXT NOT NULL,\n changes TEXT NOT NULL, -- JSON: [{field, op, from?, to?, value?}]\n description TEXT NOT NULL, -- \u4EBA\u7C7B\u53EF\u8BFB\u63CF\u8FF0\n \"order\" INTEGER NOT NULL DEFAULT 0, -- \u540C\u4E00\u8282\u70B9\u5185\u591A\u4E2A Delta \u7684\u6392\u5E8F\uFF08\u5168\u5C40\u5355\u8C03\u9012\u589E\uFF0C\u670D\u52A1\u7AEF\u751F\u6210\uFF09\n created_at TEXT NOT NULL, -- ISO 8601\uFF0C\u5E94\u7528\u5C42\u5199\u5165\n updated_at TEXT NOT NULL, -- ISO 8601\uFF0C\u5E94\u7528\u5C42\u5199\u5165\uFF08\u63D0\u6848\u5FEB\u7167\u6BD4\u5BF9\uFF0C\u51B3\u7B56 14\uFF09\n deleted_at TEXT -- \u7EA7\u8054\u8F6F\u5220\u6807\u8BB0\uFF08\u51B3\u7B56 12\uFF09\uFF1A\u4EC5\u5B9E\u4F53/\u8282\u70B9\u7EA7\u8054\u5220\u9664\u65F6\u5199\u5165\u3002\n -- \u53EF\u89C1\u6027\u8054\u52A8\u89E6\u53D1\u8282\u70B9\u4E0E\u76EE\u6807\u5B9E\u4F53\uFF08\u51B3\u7B56 12 \u4FEE\u8BA2\uFF09\uFF1A\u4EFB\u4E00\u7AEF\u8F6F\u5220\u5373\u4E0D\u53EF\u89C1\n);\n\n-- chat_messages\uFF1A\u5BF9\u8BDD\u5386\u53F2\u8868\uFF08\u51B3\u7B56 18\uFF0C\u4E0E data.db \u540C\u5E93\uFF09\nCREATE TABLE IF NOT EXISTS chat_messages (\n id TEXT PRIMARY KEY,\n session_id TEXT NOT NULL,\n project_id TEXT NOT NULL, -- \u4F1A\u8BDD\u6309\u9879\u76EE\u9694\u79BB\uFF08\u51B3\u7B56 18 \u4FEE\u8BA2\uFF09\n role TEXT NOT NULL CHECK(role IN ('user','assistant','tool')),\n content TEXT,\n tool_calls TEXT, -- JSON: \u52A9\u624B\u6D88\u606F\u7684\u5DE5\u5177\u8C03\u7528\u6570\u7EC4\n tool_call_id TEXT, -- tool \u6D88\u606F\u5173\u8054\u7684 assistant \u5DE5\u5177\u8C03\u7528 id\uFF08\u51B3\u7B56 18 \u4FEE\u8BA2\uFF09\n created_at TEXT NOT NULL -- ISO 8601\uFF0C\u5E94\u7528\u5C42\u5199\u5165\n);\n\nCREATE INDEX IF NOT EXISTS idx_chat_session ON chat_messages(session_id, created_at);\n";
16
+ /**
17
+ * 建表:执行全部 DDL(CREATE TABLE/INDEX IF NOT EXISTS),幂等,可重复调用。
18
+ * 打开数据库后由 connection.openDatabase 自动调用。
19
+ */
20
+ export declare function createTables(db: Database.Database): void;
21
+ /** 读取 data.db schema 版本(决策 13:以 PRAGMA user_version 为准判定是否重建) */
22
+ export declare function getUserVersion(db: Database.Database): number;
23
+ /**
24
+ * 写入 data.db schema 版本(决策 13)。
25
+ * 首次初始化时写入 SCHEMA_VERSION,与 outline.json / project.json 顶层 schema_version 同步(决策 8)。
26
+ * 入参必须是整数(user_version 为 SQLite 整数 PRAGMA),非整数直接抛错,杜绝模板拼接的注入面。
27
+ */
28
+ export declare function setUserVersion(db: Database.Database, version: number): void;
29
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAE3C;;;GAGG;AACH,eAAO,MAAM,cAAc,IAAI,CAAC;AAEhC;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,+wJA4D7B,CAAC;AAEF;;;GAGG;AACH,wBAAgB,YAAY,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,GAAG,IAAI,CAExD;AAED,iEAAiE;AACjE,wBAAgB,cAAc,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,GAAG,MAAM,CAE5D;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAK3E"}
package/dist/schema.js ADDED
@@ -0,0 +1,102 @@
1
+ // @whispering233/ai-editor-db 建表 SQL 与 schema 版本管理(T2.1)
2
+ //
3
+ // 单一事实来源:doc/database/schema.md——列名/类型/CHECK/索引逐字对照,勿在此处自行改动结构。
4
+ // 时间约定(schema.md 第 16 行):所有时间列统一 ISO 8601 字符串、由应用层写入,
5
+ // 不使用 SQLite 内置 datetime('now')——回收站按 deleted_at 排序需跨 SQLite 与 outline.json 统一格式。
6
+ /**
7
+ * data.db 当前 schema 版本(决策 13)。
8
+ * MVP 采用删库重建:以 PRAGMA user_version 为准判定版本,不匹配即备份并重建,无迁移脚本(YAGNI)。
9
+ */
10
+ export const SCHEMA_VERSION = 1;
11
+ /**
12
+ * 四张业务表 + 索引的建表 SQL(幂等:CREATE TABLE/INDEX IF NOT EXISTS)。
13
+ *
14
+ * - entities 实体(character/setting/location/hook),type 受 CHECK 约束,data 存 JSON,软删列 deleted_at(决策 12)
15
+ * - relation_records 通用关系表(含 plot_edge 剧情连线),3 个部分索引 WHERE deleted_at IS NULL(决策 12 修订)
16
+ * - delta_records 属性变更记录("order" 列引号保留——ORDER 是 SQLite 关键字)
17
+ * - chat_messages 对话历史(决策 18),含 project_id/tool_call_id,会话索引 (session_id, created_at)
18
+ */
19
+ export const CREATE_TABLES_SQL = `
20
+ -- entities:实体表(人物/设定/地点/伏笔)
21
+ CREATE TABLE IF NOT EXISTS entities (
22
+ id TEXT PRIMARY KEY,
23
+ type TEXT NOT NULL CHECK(type IN ('character', 'setting', 'location', 'hook')),
24
+ name TEXT NOT NULL,
25
+ data TEXT NOT NULL DEFAULT '{}', -- JSON: 各类型的专属字段
26
+ created_at TEXT NOT NULL, -- ISO 8601,应用层写入
27
+ updated_at TEXT NOT NULL, -- ISO 8601,应用层写入(提案快照比对,决策 14)
28
+ deleted_at TEXT -- 软删标记(决策 12),NULL 表示未删除;非 NULL 时该实体进入回收站,本体保留可还原
29
+ );
30
+
31
+ -- relation_records:通用关系表
32
+ CREATE TABLE IF NOT EXISTS relation_records (
33
+ id TEXT PRIMARY KEY,
34
+ source_type TEXT NOT NULL, -- 端点类型:实体 'character'|'setting'|'location'|'hook',大纲节点 'outline_node'
35
+ source_id TEXT NOT NULL,
36
+ target_type TEXT NOT NULL,
37
+ target_id TEXT NOT NULL,
38
+ relation_type TEXT NOT NULL,
39
+ metadata TEXT, -- JSON 扩展元数据
40
+ created_at TEXT NOT NULL, -- ISO 8601,应用层写入
41
+ updated_at TEXT NOT NULL, -- ISO 8601,应用层写入(提案快照比对,决策 14;软删/还原亦更新,决策 12 修订)
42
+ deleted_at TEXT -- 级联软删标记(决策 12):仅实体/节点级联删除时写入;
43
+ -- 手动删除关系 = 物理删(不置 deleted_at,不进入回收站)
44
+ );
45
+
46
+ -- 索引(决策 12 修订补):k 跳遍历与高频关系查询(部分索引,仅覆盖未软删行)
47
+ CREATE INDEX IF NOT EXISTS idx_relation_source ON relation_records(source_id) WHERE deleted_at IS NULL;
48
+ CREATE INDEX IF NOT EXISTS idx_relation_target ON relation_records(target_id) WHERE deleted_at IS NULL;
49
+ CREATE INDEX IF NOT EXISTS idx_relation_type ON relation_records(relation_type) WHERE deleted_at IS NULL;
50
+
51
+ -- delta_records:属性变更表
52
+ CREATE TABLE IF NOT EXISTS delta_records (
53
+ id TEXT PRIMARY KEY,
54
+ node_id TEXT NOT NULL, -- 触发变更的大纲节点
55
+ target_type TEXT NOT NULL,
56
+ target_id TEXT NOT NULL,
57
+ changes TEXT NOT NULL, -- JSON: [{field, op, from?, to?, value?}]
58
+ description TEXT NOT NULL, -- 人类可读描述
59
+ "order" INTEGER NOT NULL DEFAULT 0, -- 同一节点内多个 Delta 的排序(全局单调递增,服务端生成)
60
+ created_at TEXT NOT NULL, -- ISO 8601,应用层写入
61
+ updated_at TEXT NOT NULL, -- ISO 8601,应用层写入(提案快照比对,决策 14)
62
+ deleted_at TEXT -- 级联软删标记(决策 12):仅实体/节点级联删除时写入。
63
+ -- 可见性联动触发节点与目标实体(决策 12 修订):任一端软删即不可见
64
+ );
65
+
66
+ -- chat_messages:对话历史表(决策 18,与 data.db 同库)
67
+ CREATE TABLE IF NOT EXISTS chat_messages (
68
+ id TEXT PRIMARY KEY,
69
+ session_id TEXT NOT NULL,
70
+ project_id TEXT NOT NULL, -- 会话按项目隔离(决策 18 修订)
71
+ role TEXT NOT NULL CHECK(role IN ('user','assistant','tool')),
72
+ content TEXT,
73
+ tool_calls TEXT, -- JSON: 助手消息的工具调用数组
74
+ tool_call_id TEXT, -- tool 消息关联的 assistant 工具调用 id(决策 18 修订)
75
+ created_at TEXT NOT NULL -- ISO 8601,应用层写入
76
+ );
77
+
78
+ CREATE INDEX IF NOT EXISTS idx_chat_session ON chat_messages(session_id, created_at);
79
+ `;
80
+ /**
81
+ * 建表:执行全部 DDL(CREATE TABLE/INDEX IF NOT EXISTS),幂等,可重复调用。
82
+ * 打开数据库后由 connection.openDatabase 自动调用。
83
+ */
84
+ export function createTables(db) {
85
+ db.exec(CREATE_TABLES_SQL);
86
+ }
87
+ /** 读取 data.db schema 版本(决策 13:以 PRAGMA user_version 为准判定是否重建) */
88
+ export function getUserVersion(db) {
89
+ return db.pragma("user_version", { simple: true });
90
+ }
91
+ /**
92
+ * 写入 data.db schema 版本(决策 13)。
93
+ * 首次初始化时写入 SCHEMA_VERSION,与 outline.json / project.json 顶层 schema_version 同步(决策 8)。
94
+ * 入参必须是整数(user_version 为 SQLite 整数 PRAGMA),非整数直接抛错,杜绝模板拼接的注入面。
95
+ */
96
+ export function setUserVersion(db, version) {
97
+ if (!Number.isInteger(version)) {
98
+ throw new Error(`setUserVersion: version 必须是整数,收到 ${String(version)}`);
99
+ }
100
+ db.pragma(`user_version = ${version}`);
101
+ }
102
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA,yDAAyD;AACzD,EAAE;AACF,gEAAgE;AAChE,sDAAsD;AACtD,kFAAkF;AAIlF;;;GAGG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC;AAEhC;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4DhC,CAAC;AAEF;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,EAAqB;IAChD,EAAE,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;AAC7B,CAAC;AAED,iEAAiE;AACjE,MAAM,UAAU,cAAc,CAAC,EAAqB;IAClD,OAAO,EAAE,CAAC,MAAM,CAAC,cAAc,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAW,CAAC;AAC/D,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,EAAqB,EAAE,OAAe;IACnE,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC;QAC/B,MAAM,IAAI,KAAK,CAAC,oCAAoC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IACzE,CAAC;IACD,EAAE,CAAC,MAAM,CAAC,kBAAkB,OAAO,EAAE,CAAC,CAAC;AACzC,CAAC"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * 当前时间 ISO 8601(UTC)。
3
+ *
4
+ * 仅作为「应用层写时间」的便捷辅助供调用方(server 层)使用——时间戳由应用层
5
+ * 写入数据文件(schema.md 第 16 行约定),本模块的 outline/project 读写接口
6
+ * 一律不隐式生成时间,由调用方显式传入。
7
+ */
8
+ export declare function nowIso(): string;
9
+ /**
10
+ * JSON 文件原子写(决策 11):
11
+ *
12
+ * 1. 序列化为 JSON(2 空格缩进 + 尾部换行),写同目录临时文件 `.${basename}.tmp`
13
+ * 2. `fsync` 临时文件——文件数据落盘,崩溃时不会留下半截内容
14
+ * 3. `rename` 覆盖目标文件——POSIX 上同目录 rename 是原子的,
15
+ * 任意时刻磁盘上要么是旧文件、要么是新文件,崩溃/断电不会损坏原文件
16
+ * 4. `fsync` 目录——保证 rename 的目录项也落盘(平台不支持时静默忽略)
17
+ *
18
+ * 失败处理取舍:
19
+ * - rename 成功即代表临时文件已不存在(rename 即移动),无需「成功后清理」;
20
+ * - 失败时**保留临时文件**作为排查现场(内容即失败时写入的数据),不主动清理,
21
+ * 下次写入会先清理残留再以独占方式重建;
22
+ * - 写前清理上次崩溃残留的临时文件(unlink,ENOENT 忽略),并用 `wx` 独占创建,
23
+ * 杜绝并发进程互相覆盖临时文件。
24
+ *
25
+ * @param filePath 目标文件绝对路径(父目录必须存在)
26
+ * @param data 任意可 JSON 序列化的数据
27
+ * @throws 序列化失败 / 目录不可写 / 磁盘错误时抛出,且原文件保持完好
28
+ */
29
+ export declare function writeJsonAtomic(filePath: string, data: unknown): void;
30
+ /** 读取 UTF-8 文本文件;文件不存在返回 null,其余错误上抛 */
31
+ export declare function readTextFileOrNull(filePath: string): string | null;
32
+ //# sourceMappingURL=atomic.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"atomic.d.ts","sourceRoot":"","sources":["../../src/storage/atomic.ts"],"names":[],"mappings":"AAUA;;;;;;GAMG;AACH,wBAAgB,MAAM,IAAI,MAAM,CAE/B;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,IAAI,CA+BrE;AA0BD,wCAAwC;AACxC,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOlE"}
@@ -0,0 +1,112 @@
1
+ // @whispering233/ai-editor-db JSON 原子写核心(T2.2)
2
+ //
3
+ // 单一事实来源:doc/design/decisions.md 决策 11(outline.json 原子写)——
4
+ // 「写临时文件 → fsync → rename 覆盖」,禁止直接 writeFileSync 覆盖原文件;
5
+ // schema.md 第 186 行:project.json 同款流程。
6
+ // 时间约定(schema.md 第 16 行):所有时间由应用层写入(ISO 8601),模块内不生成时间。
7
+ import { closeSync, fsyncSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
8
+ import { basename, dirname, join } from "node:path";
9
+ /**
10
+ * 当前时间 ISO 8601(UTC)。
11
+ *
12
+ * 仅作为「应用层写时间」的便捷辅助供调用方(server 层)使用——时间戳由应用层
13
+ * 写入数据文件(schema.md 第 16 行约定),本模块的 outline/project 读写接口
14
+ * 一律不隐式生成时间,由调用方显式传入。
15
+ */
16
+ export function nowIso() {
17
+ return new Date().toISOString();
18
+ }
19
+ /**
20
+ * JSON 文件原子写(决策 11):
21
+ *
22
+ * 1. 序列化为 JSON(2 空格缩进 + 尾部换行),写同目录临时文件 `.${basename}.tmp`
23
+ * 2. `fsync` 临时文件——文件数据落盘,崩溃时不会留下半截内容
24
+ * 3. `rename` 覆盖目标文件——POSIX 上同目录 rename 是原子的,
25
+ * 任意时刻磁盘上要么是旧文件、要么是新文件,崩溃/断电不会损坏原文件
26
+ * 4. `fsync` 目录——保证 rename 的目录项也落盘(平台不支持时静默忽略)
27
+ *
28
+ * 失败处理取舍:
29
+ * - rename 成功即代表临时文件已不存在(rename 即移动),无需「成功后清理」;
30
+ * - 失败时**保留临时文件**作为排查现场(内容即失败时写入的数据),不主动清理,
31
+ * 下次写入会先清理残留再以独占方式重建;
32
+ * - 写前清理上次崩溃残留的临时文件(unlink,ENOENT 忽略),并用 `wx` 独占创建,
33
+ * 杜绝并发进程互相覆盖临时文件。
34
+ *
35
+ * @param filePath 目标文件绝对路径(父目录必须存在)
36
+ * @param data 任意可 JSON 序列化的数据
37
+ * @throws 序列化失败 / 目录不可写 / 磁盘错误时抛出,且原文件保持完好
38
+ */
39
+ export function writeJsonAtomic(filePath, data) {
40
+ const dir = dirname(filePath);
41
+ const tmpPath = join(dir, `.${basename(filePath)}.tmp`);
42
+ let fd;
43
+ try {
44
+ // 清理上次崩溃可能残留的临时文件(ENOENT 忽略,其余错误上抛)
45
+ try {
46
+ unlinkSync(tmpPath);
47
+ }
48
+ catch (err) {
49
+ if (err.code !== "ENOENT")
50
+ throw err;
51
+ }
52
+ const payload = `${JSON.stringify(data, null, 2)}\n`;
53
+ // "wx":独占创建——若存在文件(含并发竞争)立即失败,绝不覆盖他人临时文件
54
+ fd = openSync(tmpPath, "wx", 0o644);
55
+ writeFileSync(fd, payload);
56
+ fsyncSync(fd); // 文件数据落盘后再 rename
57
+ closeSync(fd);
58
+ fd = undefined;
59
+ renameSync(tmpPath, filePath); // 原子覆盖
60
+ fsyncDir(dir);
61
+ }
62
+ catch (err) {
63
+ // 失败:关闭可能还开着的 fd,保留临时文件供排查,异常向上抛(原文件未被触碰)
64
+ if (fd !== undefined) {
65
+ try {
66
+ closeSync(fd);
67
+ }
68
+ catch {
69
+ // 忽略关闭失败,原始异常优先
70
+ }
71
+ }
72
+ throw err;
73
+ }
74
+ }
75
+ /**
76
+ * fsync 目录,确保 rename 的目录项持久化。
77
+ * 部分平台(如 Windows)不支持对目录 open/fsync,失败时静默忽略——
78
+ * 主链路(文件 fsync + rename)已保证文件内容完整,目录项未落盘仅意味着
79
+ * 断电后文件可能回到旧版本,属可接受的降级。
80
+ */
81
+ function fsyncDir(dir) {
82
+ let dfd;
83
+ try {
84
+ dfd = openSync(dir, "r");
85
+ fsyncSync(dfd);
86
+ }
87
+ catch {
88
+ // 平台不支持目录 fsync 时忽略
89
+ }
90
+ finally {
91
+ if (dfd !== undefined) {
92
+ try {
93
+ closeSync(dfd);
94
+ }
95
+ catch {
96
+ // 忽略
97
+ }
98
+ }
99
+ }
100
+ }
101
+ /** 读取 UTF-8 文本文件;文件不存在返回 null,其余错误上抛 */
102
+ export function readTextFileOrNull(filePath) {
103
+ try {
104
+ return readFileSync(filePath, "utf8");
105
+ }
106
+ catch (err) {
107
+ if (err.code === "ENOENT")
108
+ return null;
109
+ throw err;
110
+ }
111
+ }
112
+ //# sourceMappingURL=atomic.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"atomic.js","sourceRoot":"","sources":["../../src/storage/atomic.ts"],"names":[],"mappings":"AAAA,+CAA+C;AAC/C,EAAE;AACF,2DAA2D;AAC3D,wDAAwD;AACxD,uCAAuC;AACvC,wDAAwD;AAExD,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC9G,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEpD;;;;;;GAMG;AACH,MAAM,UAAU,MAAM;IACpB,OAAO,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;AAClC,CAAC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,eAAe,CAAC,QAAgB,EAAE,IAAa;IAC7D,MAAM,GAAG,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC9B,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,IAAI,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACxD,IAAI,EAAsB,CAAC;IAC3B,IAAI,CAAC;QACH,oCAAoC;QACpC,IAAI,CAAC;YACH,UAAU,CAAC,OAAO,CAAC,CAAC;QACtB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAK,GAA6B,CAAC,IAAI,KAAK,QAAQ;gBAAE,MAAM,GAAG,CAAC;QAClE,CAAC;QACD,MAAM,OAAO,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC;QACrD,yCAAyC;QACzC,EAAE,GAAG,QAAQ,CAAC,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;QACpC,aAAa,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;QAC3B,SAAS,CAAC,EAAE,CAAC,CAAC,CAAC,kBAAkB;QACjC,SAAS,CAAC,EAAE,CAAC,CAAC;QACd,EAAE,GAAG,SAAS,CAAC;QACf,UAAU,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,OAAO;QACtC,QAAQ,CAAC,GAAG,CAAC,CAAC;IAChB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,0CAA0C;QAC1C,IAAI,EAAE,KAAK,SAAS,EAAE,CAAC;YACrB,IAAI,CAAC;gBACH,SAAS,CAAC,EAAE,CAAC,CAAC;YAChB,CAAC;YAAC,MAAM,CAAC;gBACP,gBAAgB;YAClB,CAAC;QACH,CAAC;QACD,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,SAAS,QAAQ,CAAC,GAAW;IAC3B,IAAI,GAAuB,CAAC;IAC5B,IAAI,CAAC;QACH,GAAG,GAAG,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QACzB,SAAS,CAAC,GAAG,CAAC,CAAC;IACjB,CAAC;IAAC,MAAM,CAAC;QACP,oBAAoB;IACtB,CAAC;YAAS,CAAC;QACT,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;YACtB,IAAI,CAAC;gBACH,SAAS,CAAC,GAAG,CAAC,CAAC;YACjB,CAAC;YAAC,MAAM,CAAC;gBACP,KAAK;YACP,CAAC;QACH,CAAC;IACH,CAAC;AACH,CAAC;AAED,wCAAwC;AACxC,MAAM,UAAU,kBAAkB,CAAC,QAAgB;IACjD,IAAI,CAAC;QACH,OAAO,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IACxC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAK,GAA6B,CAAC,IAAI,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC;QAClE,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC"}
@@ -0,0 +1,68 @@
1
+ import type { OutlineFileNode, OutlineFileNodeBase, OutlineFileTree } from "@whispering233/ai-editor-shared";
2
+ /** outline.json 文件名(决策 8:项目根目录) */
3
+ export declare const OUTLINE_FILE_NAME = "outline.json";
4
+ /**
5
+ * 读取 outline.json。
6
+ *
7
+ * 缺失文件语义(本项目首次初始化即创建,决策 8):**返回最小空树**而非抛错——
8
+ * 调用方(server 打开项目前的探测、初始化流程)无需为「文件还不存在」分支处理;
9
+ * 空树的 schema_version 取当前 SCHEMA_VERSION,与初始化流程写入的一致(决策 13)。
10
+ * 注意:返回的是新对象,调用方修改它不会影响后续读取。
11
+ *
12
+ * JSON 损坏 / 结构不符:**抛错**,不静默重建——静默覆盖会掩盖文件损坏并可能
13
+ * 丢弃用户数据,由上层决定提示用户还是重建。
14
+ * schema_version 数值与 SCHEMA_VERSION 不一致时**不在此校验**(决策 13:以
15
+ * data.db 的 user_version 为准判定重建,JSON 版本仅用于结构判断,由上层流程处理)。
16
+ *
17
+ * @param dir 项目根目录
18
+ * @throws 文件存在但 JSON 解析失败或顶层结构不符时抛出
19
+ */
20
+ export declare function readOutlineFile(dir: string): OutlineFileTree;
21
+ /**
22
+ * 原子写 outline.json(决策 11:临时文件 + fsync + rename,见 writeJsonAtomic)。
23
+ * 顶层 schema_version 随 tree.schema_version 原样写入——保持与 project.json
24
+ * 同步写入是调用方职责(决策 13 修订:两个文件用同一版本号常量)。
25
+ * 节点 updated_at 版本戳(决策 19)由调用方在变更后通过 touch/update 更新,
26
+ * 本函数不隐式修改树。
27
+ */
28
+ export declare function writeOutlineFile(dir: string, tree: OutlineFileTree): void;
29
+ /**
30
+ * 按 id 递归查找节点(卷/章/场景,不匹配 root 自身)。
31
+ * @returns 找到的节点引用(修改它即修改树);未找到返回 undefined
32
+ */
33
+ export declare function findOutlineNode(tree: OutlineFileTree, nodeId: string): OutlineFileNode | undefined;
34
+ /**
35
+ * 更新节点版本戳(决策 19:任何字段变更——含 children 重排、软删/还原——
36
+ * 由服务端原子写时统一更新 updated_at,支撑决策 14 提案快照比对)。
37
+ * 仅更新 updated_at,不改动其他字段;改 title/summary 用 updateOutlineNode。
38
+ *
39
+ * @param updatedAt ISO 8601,由调用方(应用层)传入,模块不生成时间
40
+ * @throws 节点不存在时抛错——静默忽略会掩盖「改了个不存在的节点」的调用方 bug
41
+ */
42
+ export declare function touchOutlineNode(tree: OutlineFileTree, nodeId: string, updatedAt: string): void;
43
+ /**
44
+ * updateOutlineNode 可更新的节点字段
45
+ * (决策 19:title/summary;决策 23:data——与 title/summary 同等对待,patch 含 data 时整体替换,
46
+ * 未传字段保留;data 的浅合并语义在 outline-ops.updateOutlineNodeInfo(与实体 updateEntity 同构);
47
+ * 决策 12:软删字段)
48
+ */
49
+ export type OutlineNodePatch = Partial<Pick<OutlineFileNodeBase, "title" | "summary" | "data" | "deleted" | "deleted_at">>;
50
+ /**
51
+ * 更新节点字段并统一更新 updated_at 版本戳(决策 19)。
52
+ * 就地修改树(返回节点引用),调用方随后 writeOutlineFile 落盘;
53
+ * children 重排等无法用 patch 表达的变更请用 touchOutlineNode。
54
+ *
55
+ * @param updatedAt ISO 8601,由调用方(应用层)传入,模块不生成时间
56
+ * @returns 更新后的节点引用
57
+ * @throws 节点不存在时抛错
58
+ */
59
+ export declare function updateOutlineNode(tree: OutlineFileTree, nodeId: string, patch: OutlineNodePatch, updatedAt: string): OutlineFileNode;
60
+ /**
61
+ * 根 → 目标节点的路径 id 列表(含 root 与目标节点自身)。
62
+ * 供后续卡使用:章节序现推(决策 21)、computeState 树路径排序(决策 9,
63
+ * data-flow.md 第 50 行 getNodePathIds)。
64
+ *
65
+ * @throws 节点不存在时抛错(路径唯一是严格三层的推论,找不到即调用方 bug)
66
+ */
67
+ export declare function getOutlinePathIds(tree: OutlineFileTree, nodeId: string): string[];
68
+ //# sourceMappingURL=outline.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"outline.d.ts","sourceRoot":"","sources":["../../src/storage/outline.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,eAAe,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAI7G,mCAAmC;AACnC,eAAO,MAAM,iBAAiB,iBAAiB,CAAC;AAUhD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,CAK5D;AAsBD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,GAAG,IAAI,CAEzE;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAElG;AAoBD;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAM/F;AAED;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG,OAAO,CACpC,IAAI,CAAC,mBAAmB,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,GAAG,YAAY,CAAC,CACnF,CAAC;AAEF;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,eAAe,EACrB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,gBAAgB,EACvB,SAAS,EAAE,MAAM,GAChB,eAAe,CAQjB;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAIjF"}