dsh-vscode-mode 0.21.4 → 0.22.0

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.
@@ -0,0 +1,324 @@
1
+ /**
2
+ * 待审投影的持久化:与官方历史共用工作区库,但只新增 pending_* 表,绝不改写 review_* 事实。
3
+ * @author ddj 2026年10月29号
4
+ */
5
+ import type { DatabaseSync } from 'node:sqlite'
6
+ import { activeReady, dbFor } from '../archiveDb.js'
7
+ import type { PendingLineage } from '../shared/pendingReview.js'
8
+ import type { PendingEntry } from './kernel.js'
9
+
10
+ /** 数据库行。 */
11
+ type Row = Record<string, string | number | null>
12
+
13
+ /** 条目上除两侧全文之外的持久标记。 */
14
+ export interface PendingFlags {
15
+ missing: boolean
16
+ diverged: boolean
17
+ oversized: boolean
18
+ revision: number
19
+ }
20
+
21
+ /** 一条已落库的条目。 */
22
+ export interface StoredEntry {
23
+ entry: PendingEntry
24
+ flags: PendingFlags
25
+ }
26
+
27
+ /** 待审库版本号,写进 metadata 便于日后迁移。v3 给评论表加了引文与上下文列。 */
28
+ const SCHEMA_VERSION = '3'
29
+ /** 迁移前的备份后缀。 */
30
+ const BACKUP_SUFFIX = '.before-pending.bak'
31
+
32
+ /**
33
+ * @author ddj 2026年10月29号
34
+ * @description: 打开工作区库并确保待审表存在;DDL 与版本标记在同一事务里提交。
35
+ * @param cwd 已迁移工作区
36
+ * @returns 可读写的连接
37
+ * @throws 未完成活跃库迁移时拒绝
38
+ */
39
+ export function pendingDb(cwd: string): DatabaseSync {
40
+ if (!activeReady(cwd)) throw new Error('待审投影要求先完成活跃库迁移')
41
+ const db = dbFor(cwd)
42
+ const stamp = db.prepare("SELECT value FROM metadata WHERE name='pending_schema'").get() as Row | undefined
43
+ if (stamp?.value === SCHEMA_VERSION) return db
44
+ db.exec('BEGIN IMMEDIATE')
45
+ try {
46
+ db.exec(DDL)
47
+ migrateHistory(db)
48
+ db.prepare("INSERT INTO metadata(name,value) VALUES('pending_schema',?) ON CONFLICT(name) DO UPDATE SET value=excluded.value")
49
+ .run(SCHEMA_VERSION)
50
+ db.exec('COMMIT')
51
+ } catch (error) {
52
+ if (db.isTransaction) db.exec('ROLLBACK')
53
+ throw error
54
+ }
55
+ return db
56
+ }
57
+
58
+ /** 待审表结构:条目、块决定、幂等操作、撤销栈。 */
59
+ const DDL = `
60
+ CREATE TABLE IF NOT EXISTS pending_entries (
61
+ id TEXT PRIMARY KEY, path TEXT NOT NULL, earlier TEXT NOT NULL,
62
+ old_text TEXT NOT NULL, new_text TEXT NOT NULL, updated_at INTEGER NOT NULL,
63
+ unseen INTEGER NOT NULL DEFAULT 0, missing INTEGER NOT NULL DEFAULT 0,
64
+ diverged INTEGER NOT NULL DEFAULT 0, oversized INTEGER NOT NULL DEFAULT 0,
65
+ revision INTEGER NOT NULL DEFAULT 0, session_id TEXT NOT NULL,
66
+ session_ids TEXT NOT NULL, lineage TEXT);
67
+ CREATE TABLE IF NOT EXISTS pending_blocks (
68
+ entry_id TEXT NOT NULL, block_id TEXT NOT NULL, decision TEXT NOT NULL,
69
+ PRIMARY KEY(entry_id,block_id));
70
+ CREATE TABLE IF NOT EXISTS pending_ops (
71
+ operation_id TEXT PRIMARY KEY, request TEXT NOT NULL, state TEXT NOT NULL,
72
+ result TEXT, undo TEXT);
73
+ CREATE TABLE IF NOT EXISTS pending_history (
74
+ id INTEGER PRIMARY KEY AUTOINCREMENT, kind TEXT NOT NULL, entry_id TEXT,
75
+ snapshot TEXT NOT NULL, created_at INTEGER NOT NULL,
76
+ state TEXT NOT NULL DEFAULT 'applied');
77
+ `
78
+
79
+ /** v1 历史表补 state 列(rebuild 成本高于一次 ALTER)。 */
80
+ function migrateHistory(db: DatabaseSync): void {
81
+ const columns = db.prepare('PRAGMA table_info(pending_history)').all() as Row[]
82
+ if (columns.some((column) => column.name === 'state')) return
83
+ db.exec("ALTER TABLE pending_history ADD COLUMN state TEXT NOT NULL DEFAULT 'applied'")
84
+ }
85
+
86
+ /**
87
+ * @author ddj 2026年10月29号
88
+ * @description: 读出全部待审条目,按更新时间升序。
89
+ * @param cwd 工作区
90
+ * @returns 条目与标记
91
+ */
92
+ export function loadEntries(cwd: string): StoredEntry[] {
93
+ const rows = pendingDb(cwd).prepare('SELECT * FROM pending_entries ORDER BY updated_at ASC').all() as Row[]
94
+ return rows.map((row) => ({ entry: entryOfRow(row), flags: flagsOfRow(row) }))
95
+ }
96
+
97
+ /**
98
+ * @author ddj 2026年10月29号
99
+ * @description: 写入或更新一条条目(含标记),单条原子完成。
100
+ * @param cwd 工作区
101
+ * @param stored 条目与标记
102
+ * @returns 无
103
+ */
104
+ export function saveEntry(cwd: string, stored: StoredEntry): void {
105
+ const { entry, flags } = stored
106
+ pendingDb(cwd).prepare(`INSERT INTO pending_entries
107
+ (id,path,earlier,old_text,new_text,updated_at,unseen,missing,diverged,oversized,revision,session_id,session_ids,lineage)
108
+ VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)
109
+ ON CONFLICT(id) DO UPDATE SET path=excluded.path,earlier=excluded.earlier,old_text=excluded.old_text,
110
+ new_text=excluded.new_text,updated_at=excluded.updated_at,unseen=excluded.unseen,missing=excluded.missing,
111
+ diverged=excluded.diverged,oversized=excluded.oversized,revision=excluded.revision,
112
+ session_id=excluded.session_id,session_ids=excluded.session_ids,lineage=excluded.lineage`)
113
+ .run(entry.id, entry.path, entry.earlierVersion, entry.oldText, entry.newText, entry.updatedAt,
114
+ entry.unseen === true ? 1 : 0, flags.missing ? 1 : 0, flags.diverged ? 1 : 0, flags.oversized ? 1 : 0,
115
+ flags.revision, entry.sessionId, JSON.stringify(entry.sessionIds), entry.lineage ? JSON.stringify(entry.lineage) : null)
116
+ }
117
+
118
+ /**
119
+ * @author ddj 2026年10月29号
120
+ * @description: 删除条目及其块决定,用于"已解决条目移除"。
121
+ * @param cwd 工作区
122
+ * @param id 条目身份
123
+ * @returns 无
124
+ */
125
+ export function dropEntry(cwd: string, id: string): void {
126
+ const db = pendingDb(cwd)
127
+ db.exec('BEGIN IMMEDIATE')
128
+ try {
129
+ db.prepare('DELETE FROM pending_entries WHERE id=?').run(id)
130
+ db.prepare('DELETE FROM pending_blocks WHERE entry_id=?').run(id)
131
+ db.exec('COMMIT')
132
+ } catch (error) {
133
+ if (db.isTransaction) db.exec('ROLLBACK')
134
+ throw error
135
+ }
136
+ }
137
+
138
+ /**
139
+ * @author ddj 2026年10月29号
140
+ * @description: 读出某条目的块决定。
141
+ * @param cwd 工作区
142
+ * @param entryId 条目身份
143
+ * @returns 块决定列表
144
+ */
145
+ export function loadBlocks(cwd: string, entryId: string): { id: string; decision: string }[] {
146
+ const rows = pendingDb(cwd).prepare('SELECT block_id AS id,decision FROM pending_blocks WHERE entry_id=? ORDER BY rowid')
147
+ .all(entryId) as Row[]
148
+ return rows.map((row) => ({ id: String(row.id), decision: String(row.decision) }))
149
+ }
150
+
151
+ /**
152
+ * @author ddj 2026年10月29号
153
+ * @description: 覆盖某条目的块决定集合。
154
+ * @param cwd 工作区
155
+ * @param entryId 条目身份
156
+ * @param blocks 块决定
157
+ * @returns 无
158
+ */
159
+ export function saveBlocks(cwd: string, entryId: string, blocks: { id: string; decision: string }[]): void {
160
+ const db = pendingDb(cwd)
161
+ db.exec('BEGIN IMMEDIATE')
162
+ try {
163
+ db.prepare('DELETE FROM pending_blocks WHERE entry_id=?').run(entryId)
164
+ const insert = db.prepare('INSERT INTO pending_blocks VALUES(?,?,?)')
165
+ for (const block of blocks) insert.run(entryId, block.id, block.decision)
166
+ db.exec('COMMIT')
167
+ } catch (error) {
168
+ if (db.isTransaction) db.exec('ROLLBACK')
169
+ throw error
170
+ }
171
+ }
172
+
173
+
174
+ /**
175
+ * @author ddj 2026年10月29号
176
+ * @description: 读取幂等操作记录;同 operationId 的重复请求必须复用结果。
177
+ * @param cwd 工作区
178
+ * @param operationId 操作身份
179
+ * @returns 请求原文、状态与结果
180
+ */
181
+ export function readOp(cwd: string, operationId: string): { request: string; state: string; result: string | null } | undefined {
182
+ const row = pendingDb(cwd).prepare('SELECT request,state,result FROM pending_ops WHERE operation_id=?').get(operationId) as Row | undefined
183
+ return row ? { request: String(row.request), state: String(row.state), result: row.result === null ? null : String(row.result) } : undefined
184
+ }
185
+
186
+ /**
187
+ * @author ddj 2026年10月29号
188
+ * @description: 记录一次操作的阶段与撤销快照。
189
+ * @param cwd 工作区
190
+ * @param operationId 操作身份
191
+ * @param request 请求原文
192
+ * @param state prepared / committed
193
+ * @param payload 结果与撤销快照
194
+ * @returns 无
195
+ */
196
+ export function writeOp(
197
+ cwd: string,
198
+ operationId: string,
199
+ request: string,
200
+ state: 'prepared' | 'committed',
201
+ payload: { result?: string; undo?: string } = {},
202
+ ): void {
203
+ pendingDb(cwd).prepare(`INSERT INTO pending_ops VALUES(?,?,?,?,?)
204
+ ON CONFLICT(operation_id) DO UPDATE SET state=excluded.state,
205
+ result=COALESCE(excluded.result,pending_ops.result), undo=COALESCE(excluded.undo,pending_ops.undo)`)
206
+ .run(operationId, request, state, payload.result ?? null, payload.undo ?? null)
207
+ }
208
+
209
+ /**
210
+ * @author ddj 2026年10月29号
211
+ * @description: 追加一条撤销栈记录,并截断重做分支(新决定之后旧的"已撤销"记录不能再重做)。
212
+ * @param cwd 工作区
213
+ * @param kind 操作种类
214
+ * @param entryId 涉及的条目
215
+ * @param snapshot 可重放快照
216
+ * @param now 时间
217
+ * @returns 无
218
+ */
219
+ export function pushHistory(cwd: string, kind: string, entryId: string, snapshot: string, now: number): void {
220
+ const db = pendingDb(cwd)
221
+ db.prepare("DELETE FROM pending_history WHERE state='undone'").run()
222
+ db.prepare("INSERT INTO pending_history(kind,entry_id,snapshot,created_at,state) VALUES(?,?,?,?,'applied')")
223
+ .run(kind, entryId, snapshot, now)
224
+ }
225
+
226
+ /**
227
+ * @author ddj 2026年10月30号
228
+ * @description: 取最近一条仍生效的历史记录,撤销就作用在它上面。
229
+ * @param cwd 工作区
230
+ * @returns 记录或 undefined
231
+ */
232
+ export function latestApplied(cwd: string): HistoryRow | undefined {
233
+ const row = pendingDb(cwd).prepare(
234
+ "SELECT id,kind,entry_id,snapshot FROM pending_history WHERE state='applied' ORDER BY id DESC LIMIT 1",
235
+ ).get() as Row | undefined
236
+ return row ? rowOfHistory(row) : undefined
237
+ }
238
+
239
+ /**
240
+ * @author ddj 2026年10月30号
241
+ * @description: 取最早一条被撤销的记录,重做按"撤销的逆序"重新生效。
242
+ * @param cwd 工作区
243
+ * @returns 记录或 undefined
244
+ */
245
+ export function earliestUndone(cwd: string): HistoryRow | undefined {
246
+ const row = pendingDb(cwd).prepare(
247
+ "SELECT id,kind,entry_id,snapshot FROM pending_history WHERE state='undone' ORDER BY id ASC LIMIT 1",
248
+ ).get() as Row | undefined
249
+ return row ? rowOfHistory(row) : undefined
250
+ }
251
+
252
+ /**
253
+ * @author ddj 2026年10月30号
254
+ * @description: 翻转一条历史记录的生效状态。
255
+ * @param cwd 工作区
256
+ * @param id 记录主键
257
+ * @param state applied / undone
258
+ * @returns 无
259
+ */
260
+ export function setHistoryState(cwd: string, id: number, state: 'applied' | 'undone'): void {
261
+ pendingDb(cwd).prepare('UPDATE pending_history SET state=? WHERE id=?').run(state, id)
262
+ }
263
+
264
+ /**
265
+ * @author ddj 2026年10月30号
266
+ * @description: 把数据库行还原成历史记录。
267
+ * @param row 数据库行
268
+ * @returns 记录
269
+ */
270
+ function rowOfHistory(row: Row): HistoryRow {
271
+ return { id: Number(row.id), kind: String(row.kind), entryId: String(row.entry_id), snapshot: String(row.snapshot) }
272
+ }
273
+
274
+ /** 历史记录行形态(快照为 JSON 文本)。 */
275
+ export interface HistoryRow {
276
+ id: number
277
+ kind: string
278
+ entryId: string
279
+ snapshot: string
280
+ }
281
+
282
+ /**
283
+ * @author ddj 2026年10月29号
284
+ * @description: 迁移前备份库文件路径,便于回退只读旧视图。
285
+ * @param dbFile 工作区库文件
286
+ * @returns 备份路径
287
+ */
288
+ export function backupPath(dbFile: string): string {
289
+ return dbFile + BACKUP_SUFFIX
290
+ }
291
+
292
+ /**
293
+ * @author ddj 2026年10月29号
294
+ * @description: 由数据库行还原条目。
295
+ * @param row 数据库行
296
+ * @returns 条目
297
+ */
298
+ function entryOfRow(row: Row): PendingEntry {
299
+ const lineage = row.lineage === null ? undefined : JSON.parse(String(row.lineage)) as PendingLineage
300
+ const entry: PendingEntry = {
301
+ id: String(row.id), path: String(row.path),
302
+ earlierVersion: String(row.earlier) === 'none' ? 'none' : 'file',
303
+ oldText: String(row.old_text), newText: String(row.new_text), updatedAt: Number(row.updated_at),
304
+ sessionId: String(row.session_id), sessionIds: JSON.parse(String(row.session_ids)) as string[],
305
+ }
306
+ if (Number(row.unseen) === 1) entry.unseen = true
307
+ if (lineage !== undefined) entry.lineage = lineage
308
+ return entry
309
+ }
310
+
311
+ /**
312
+ * @author ddj 2026年10月29号
313
+ * @description: 由数据库行还原标记。
314
+ * @param row 数据库行
315
+ * @returns 标记
316
+ */
317
+ function flagsOfRow(row: Row): PendingFlags {
318
+ return {
319
+ missing: Number(row.missing) === 1,
320
+ diverged: Number(row.diverged) === 1,
321
+ oversized: Number(row.oversized) === 1,
322
+ revision: Number(row.revision),
323
+ }
324
+ }