dsh-vscode-mode 0.20.0 → 0.20.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -15
- package/lib/client.js +716 -82
- package/lib/client.js.map +1 -1
- package/lib/index.js +417 -15
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/mcpSessionState.ts +42 -15
- package/src/client/monaco/diffRender.ts +2 -2
- package/src/client/reviewBatch.ts +64 -0
- package/src/client/state/reviewFeed.ts +8 -4
- package/src/client/styles/editor.css +39 -0
- package/src/client/ui/DiffBox.ts +2 -17
- package/src/client/ui/EditorView.ts +176 -12
- package/src/client/ui/McpSessionStatus.ts +22 -7
- package/src/client/ui/OfficialDiffBar.tsx +112 -0
- package/src/client/ui/ReviewAllConfirm.ts +35 -0
- package/src/client-primitives.d.ts +34 -0
- package/src/index.ts +4 -1
- package/src/officialCapture.ts +7 -3
- package/src/reviewCapture.ts +112 -0
- package/src/reviewDb.ts +77 -6
- package/src/reviewEvidence.ts +70 -0
- package/src/reviewRpc.ts +10 -1
- package/src/reviewUndo.ts +104 -0
- package/src/shared/review.ts +5 -1
- package/src/shared/reviewRegions.ts +74 -0
package/lib/index.js
CHANGED
|
@@ -7180,13 +7180,64 @@ async function captureToolResult(ctx, registry, exec, result) {
|
|
|
7180
7180
|
}
|
|
7181
7181
|
}
|
|
7182
7182
|
//#endregion
|
|
7183
|
+
//#region src/shared/reviewRegions.ts
|
|
7184
|
+
/** @public @author ddj 2026年10月27号 @param diff Immutable official patch. @returns Contiguous change runs with stable source-coordinate identities. */
|
|
7185
|
+
function reviewBlocks(diff) {
|
|
7186
|
+
if (diff.kind !== "text") return [];
|
|
7187
|
+
const blocks = [];
|
|
7188
|
+
for (const [hunkIndex, hunk] of diff.hunks.entries()) {
|
|
7189
|
+
let oldLine = hunk.oldStart, newLine = hunk.newStart;
|
|
7190
|
+
let before = [];
|
|
7191
|
+
let current;
|
|
7192
|
+
for (const line of hunk.lines) {
|
|
7193
|
+
const marker = line[0], text = line.slice(1);
|
|
7194
|
+
if (marker === " ") {
|
|
7195
|
+
if (current) {
|
|
7196
|
+
current.contextAfter.push(text);
|
|
7197
|
+
current = void 0;
|
|
7198
|
+
} else if (blocks.at(-1)?.hunkIndex === hunkIndex) blocks.at(-1).contextAfter.push(text);
|
|
7199
|
+
before.push(text);
|
|
7200
|
+
oldLine++;
|
|
7201
|
+
newLine++;
|
|
7202
|
+
continue;
|
|
7203
|
+
}
|
|
7204
|
+
if (marker !== "+" && marker !== "-") continue;
|
|
7205
|
+
if (!current) {
|
|
7206
|
+
current = {
|
|
7207
|
+
id: `${hunkIndex}:${oldLine}:${newLine}`,
|
|
7208
|
+
hunkIndex,
|
|
7209
|
+
oldStart: oldLine,
|
|
7210
|
+
newStart: newLine,
|
|
7211
|
+
oldLines: [],
|
|
7212
|
+
newLines: [],
|
|
7213
|
+
contextBefore: before.slice(-3),
|
|
7214
|
+
contextAfter: []
|
|
7215
|
+
};
|
|
7216
|
+
blocks.push(current);
|
|
7217
|
+
before = [];
|
|
7218
|
+
}
|
|
7219
|
+
if (marker === "-") {
|
|
7220
|
+
current.oldLines.push(text);
|
|
7221
|
+
oldLine++;
|
|
7222
|
+
} else {
|
|
7223
|
+
current.newLines.push(text);
|
|
7224
|
+
newLine++;
|
|
7225
|
+
}
|
|
7226
|
+
}
|
|
7227
|
+
}
|
|
7228
|
+
return blocks;
|
|
7229
|
+
}
|
|
7230
|
+
//#endregion
|
|
7183
7231
|
//#region src/reviewDb.ts
|
|
7184
7232
|
/** Official history shares the archive connection, but never the legacy callId namespace. @author ddj 2026年10月26号 */
|
|
7185
7233
|
/** @public @author ddj 2026年10月26号 @param cwd Migrated workspace. @returns Schema4 connection; DDL and version change roll back together. */
|
|
7186
7234
|
function reviewDb(cwd) {
|
|
7187
7235
|
if (!activeReady(cwd)) throw new Error("官方审查要求先完成活跃库迁移");
|
|
7188
7236
|
const db = dbFor(cwd);
|
|
7189
|
-
if (db.prepare("SELECT value FROM metadata WHERE name='schema'").get().value === "4")
|
|
7237
|
+
if (db.prepare("SELECT value FROM metadata WHERE name='schema'").get().value === "4") {
|
|
7238
|
+
migrateBlocks(cwd, db);
|
|
7239
|
+
return db;
|
|
7240
|
+
}
|
|
7190
7241
|
db.exec("BEGIN IMMEDIATE");
|
|
7191
7242
|
try {
|
|
7192
7243
|
db.exec(`CREATE TABLE review_snapshots (
|
|
@@ -7212,8 +7263,33 @@ function reviewDb(cwd) {
|
|
|
7212
7263
|
if (db.isTransaction) db.exec("ROLLBACK");
|
|
7213
7264
|
throw error;
|
|
7214
7265
|
}
|
|
7266
|
+
migrateBlocks(cwd, db);
|
|
7215
7267
|
return db;
|
|
7216
7268
|
}
|
|
7269
|
+
/** @private @author ddj 2026年10月27号 @param cwd Workspace backup identity. @param db Open schema4 database. Extends the official-review sub-schema without changing legacy archive schema. @throws Migration failure, leaving prior decisions intact. */
|
|
7270
|
+
function migrateBlocks(cwd, db) {
|
|
7271
|
+
if (db.prepare("SELECT value FROM metadata WHERE name='review_schema'").get()) return;
|
|
7272
|
+
const backup = archiveDbFile(cwd) + ".before-review-blocks.bak";
|
|
7273
|
+
if (!existsSync(backup)) db.exec(`VACUUM INTO '${backup.replace(/'/g, "''")}'`);
|
|
7274
|
+
db.exec("BEGIN IMMEDIATE");
|
|
7275
|
+
try {
|
|
7276
|
+
db.exec(`CREATE TABLE review_blocks (file_id TEXT NOT NULL, block_id TEXT NOT NULL,
|
|
7277
|
+
decision TEXT NOT NULL, PRIMARY KEY(file_id,block_id));
|
|
7278
|
+
CREATE TABLE review_evidence (file_id TEXT PRIMARY KEY, payload TEXT NOT NULL);
|
|
7279
|
+
INSERT INTO metadata(name,value) VALUES('review_schema','2');`);
|
|
7280
|
+
const files = db.prepare("SELECT id,diff,decision FROM review_files").all();
|
|
7281
|
+
for (const file of files) seedBlocks(db, String(file.id), JSON.parse(String(file.diff)), String(file.decision));
|
|
7282
|
+
db.exec("COMMIT");
|
|
7283
|
+
} catch (error) {
|
|
7284
|
+
if (db.isTransaction) db.exec("ROLLBACK");
|
|
7285
|
+
throw error;
|
|
7286
|
+
}
|
|
7287
|
+
}
|
|
7288
|
+
/** @private @author ddj 2026年10月27号 @param db Transaction connection. @param id File record. @param diff Immutable official source. @param decision Prior file decision. Seeds deterministic blocks without changing source facts. */
|
|
7289
|
+
function seedBlocks(db, id, diff, decision) {
|
|
7290
|
+
const insert = db.prepare("INSERT INTO review_blocks VALUES(?,?,?)");
|
|
7291
|
+
for (const block of reviewBlocks(diff)) insert.run(id, block.id, decision);
|
|
7292
|
+
}
|
|
7217
7293
|
/** @private @author ddj 2026年10月26号 @param text Canonical serialized input. @returns Collision-resistant identity. */
|
|
7218
7294
|
function digest(text) {
|
|
7219
7295
|
return createHash("sha256").update(text).digest("hex");
|
|
@@ -7279,7 +7355,7 @@ async function repairPaths(cwd, resolvePath) {
|
|
|
7279
7355
|
/** @private @author ddj 2026年10月26号 @param db Transaction. @param snapshot Source payload. @param hash Source digest. */
|
|
7280
7356
|
function insertSnapshot(db, snapshot, hash) {
|
|
7281
7357
|
const { sessionId, turn, seq } = snapshot;
|
|
7282
|
-
const previous = db.prepare("SELECT identity,diff,decision FROM review_files WHERE session_id=? AND turn=? AND current=1").all(sessionId, turn);
|
|
7358
|
+
const previous = db.prepare("SELECT id,identity,diff,decision FROM review_files WHERE session_id=? AND turn=? AND current=1").all(sessionId, turn);
|
|
7283
7359
|
db.prepare("UPDATE review_snapshots SET current=0 WHERE session_id=? AND turn=?").run(sessionId, turn);
|
|
7284
7360
|
db.prepare("UPDATE review_files SET current=0 WHERE session_id=? AND turn=?").run(sessionId, turn);
|
|
7285
7361
|
db.prepare("INSERT INTO review_snapshots VALUES(?,?,?,?,?,1)").run(sessionId, turn, seq, JSON.stringify(snapshot), hash);
|
|
@@ -7293,7 +7369,15 @@ function insertSnapshot(db, snapshot, hash) {
|
|
|
7293
7369
|
seq,
|
|
7294
7370
|
file.identity
|
|
7295
7371
|
]));
|
|
7296
|
-
|
|
7372
|
+
const decision = String(unchanged?.decision ?? "pending");
|
|
7373
|
+
insert.run(id, sessionId, turn, seq, file.identity, JSON.stringify(snapshot.summary.files[index]), encoded, decision);
|
|
7374
|
+
seedBlocks(db, id, file.diff, decision);
|
|
7375
|
+
if (unchanged) {
|
|
7376
|
+
db.prepare(`UPDATE review_blocks SET decision=(SELECT p.decision FROM review_blocks p
|
|
7377
|
+
WHERE p.file_id=? AND p.block_id=review_blocks.block_id)
|
|
7378
|
+
WHERE file_id=? AND EXISTS(SELECT 1 FROM review_blocks p WHERE p.file_id=? AND p.block_id=review_blocks.block_id)`).run(unchanged.id, id, unchanged.id);
|
|
7379
|
+
db.prepare("INSERT INTO review_evidence SELECT ?,payload FROM review_evidence WHERE file_id=?").run(id, unchanged.id);
|
|
7380
|
+
}
|
|
7297
7381
|
}
|
|
7298
7382
|
}
|
|
7299
7383
|
/** @public @author ddj 2026年10月26号 @param cwd Workspace. @param gap Announced event identity. @param state Durable inbox or explicit failure. */
|
|
@@ -7371,20 +7455,39 @@ function hasOfficial(cwd, path) {
|
|
|
7371
7455
|
}
|
|
7372
7456
|
/** @public @author ddj 2026年10月26号 @param cwd Workspace. @param id Immutable file identity. @returns Historical patch, not a reconstruction from disk. */
|
|
7373
7457
|
function reviewDetail(cwd, id) {
|
|
7374
|
-
const
|
|
7458
|
+
const db = reviewDb(cwd);
|
|
7459
|
+
const row = db.prepare(`SELECT f.*,json_extract(s.payload,'$.capturedAt') AS captured_at FROM review_files f JOIN review_snapshots s
|
|
7375
7460
|
USING(session_id,turn,seq) WHERE f.id=?`).get(id);
|
|
7376
7461
|
if (!row) return null;
|
|
7462
|
+
const blocks = db.prepare("SELECT block_id AS id,decision FROM review_blocks WHERE file_id=? ORDER BY rowid").all(id);
|
|
7463
|
+
const diff = JSON.parse(String(row.diff));
|
|
7464
|
+
const evidence = db.prepare("SELECT 1 FROM review_evidence WHERE file_id=?").get(id);
|
|
7465
|
+
const pending = row.current === 1 && row.decision === "pending";
|
|
7466
|
+
const undo = pending && !!evidence && diff.kind === "text" && diff.before && diff.after && !diff.coarse;
|
|
7377
7467
|
return {
|
|
7378
7468
|
item: itemOf(row),
|
|
7379
|
-
diff
|
|
7469
|
+
diff,
|
|
7380
7470
|
capturedAt: String(row.captured_at),
|
|
7471
|
+
blocks,
|
|
7381
7472
|
capability: {
|
|
7382
|
-
keep:
|
|
7383
|
-
undo
|
|
7384
|
-
reason: "
|
|
7473
|
+
keep: pending,
|
|
7474
|
+
undo,
|
|
7475
|
+
reason: undo ? "Undo 将校验原始字节、当前文件版本及已处理差异块;文件变化会安全拒绝。" : "该记录缺少可验证的原始字节或受控写回能力;仍可显示差异和 Keep,不能猜测历史全文执行 Undo。"
|
|
7385
7476
|
}
|
|
7386
7477
|
};
|
|
7387
7478
|
}
|
|
7479
|
+
/** @public @author ddj 2026年10月27号 @param db Active decision transaction. @param request File or block action. Updates aggregate without losing mixed decisions. @throws Invalid or already-decided block. */
|
|
7480
|
+
function changeBlocks(db, request) {
|
|
7481
|
+
if (request.scope === "hunk") {
|
|
7482
|
+
if (db.prepare("UPDATE review_blocks SET decision=? WHERE file_id=? AND block_id=? AND decision='pending'").run(request.decision, request.id, request.blockId ?? "").changes !== 1) throw new Error("差异块不存在或已处理");
|
|
7483
|
+
} else db.prepare("UPDATE review_blocks SET decision=? WHERE file_id=? AND decision='pending'").run(request.decision, request.id);
|
|
7484
|
+
const decisions = db.prepare("SELECT DISTINCT decision FROM review_blocks WHERE file_id=?").all(request.id).map((row) => String(row.decision));
|
|
7485
|
+
let aggregate = request.decision;
|
|
7486
|
+
if (decisions.includes("pending")) aggregate = "pending";
|
|
7487
|
+
else if (decisions.length > 1) aggregate = "mixed";
|
|
7488
|
+
else if (decisions.length === 1) aggregate = decisions[0];
|
|
7489
|
+
db.prepare("UPDATE review_files SET decision=? WHERE id=?").run(aggregate, request.id);
|
|
7490
|
+
}
|
|
7388
7491
|
/** @public @author ddj 2026年10月26号 @param cwd Workspace. @param request Frozen identity/revision. @returns Idempotent decision; rejection never guesses a target. */
|
|
7389
7492
|
function decideReview(cwd, request) {
|
|
7390
7493
|
const db = reviewDb(cwd);
|
|
@@ -7395,12 +7498,15 @@ function decideReview(cwd, request) {
|
|
|
7395
7498
|
const previous = db.prepare("SELECT request,result FROM review_ops WHERE operation_id=?").get(request.operationId);
|
|
7396
7499
|
if (previous) {
|
|
7397
7500
|
if (previous.request !== encoded) throw new Error("operationId 已用于其他请求");
|
|
7501
|
+
if (!previous.result) throw new Error("Undo 尚待写后核对,请重试原操作");
|
|
7398
7502
|
db.exec("COMMIT");
|
|
7399
7503
|
return JSON.parse(String(previous.result));
|
|
7400
7504
|
}
|
|
7505
|
+
if (db.prepare("SELECT 1 FROM review_ops WHERE state='prepared' AND json_extract(request,'$.id')=?").get(request.id)) throw new Error("该文件有尚待核对的 Undo,暂不能 Keep");
|
|
7401
7506
|
if (request.decision !== "accepted") throw new Error("缺少可信全文证据,禁止拒绝写回");
|
|
7402
7507
|
if (db.prepare(`UPDATE review_files SET decision='accepted',revision=revision+1
|
|
7403
7508
|
WHERE id=? AND seq=? AND revision=? AND current=1 AND decision='pending'`).run(request.id, request.seq, request.revision).changes !== 1) throw new Error("审查版本冲突,请刷新后重试");
|
|
7509
|
+
changeBlocks(db, request);
|
|
7404
7510
|
const result = { revision: request.revision + 1 };
|
|
7405
7511
|
db.prepare("INSERT INTO review_ops VALUES(?,?,'committed',?,NULL)").run(request.operationId, encoded, JSON.stringify(result));
|
|
7406
7512
|
db.exec("COMMIT");
|
|
@@ -7580,7 +7686,7 @@ async function readDiff(source, task, index, signal) {
|
|
|
7580
7686
|
});
|
|
7581
7687
|
}
|
|
7582
7688
|
/** @public @author ddj 2026年10月26号 @param ctx Host lifecycle owner; services remain optional for older Hosts. */
|
|
7583
|
-
function installCapture(ctx) {
|
|
7689
|
+
function installCapture(ctx, onSnapshot) {
|
|
7584
7690
|
const collector = createCollector({
|
|
7585
7691
|
source: () => ctx.get("workspaceChanges"),
|
|
7586
7692
|
prepare: async (cwd) => {
|
|
@@ -7588,7 +7694,11 @@ function installCapture(ctx) {
|
|
|
7588
7694
|
await repairPaths(cwd, resolvePath);
|
|
7589
7695
|
},
|
|
7590
7696
|
identity: resolvePath,
|
|
7591
|
-
persist:
|
|
7697
|
+
persist: (cwd, snapshot) => {
|
|
7698
|
+
const saved = saveSnapshot(cwd, snapshot);
|
|
7699
|
+
onSnapshot?.(cwd, snapshot);
|
|
7700
|
+
return saved;
|
|
7701
|
+
},
|
|
7592
7702
|
state: captureState,
|
|
7593
7703
|
report: (message) => log.warn(message)
|
|
7594
7704
|
});
|
|
@@ -7632,6 +7742,194 @@ function installCapture(ctx) {
|
|
|
7632
7742
|
}
|
|
7633
7743
|
}
|
|
7634
7744
|
//#endregion
|
|
7745
|
+
//#region src/reviewEvidence.ts
|
|
7746
|
+
/** Raw UTF-8 evidence and patch correspondence; never reconstructs a historical file from disk. @author ddj 2026年10月27号 */
|
|
7747
|
+
/** @public @author ddj 2026年10月27号 @param bytes File bytes, or null for confirmed absence. @returns Existence-sensitive immutable digest and base64. */
|
|
7748
|
+
function rawReviewFile(bytes) {
|
|
7749
|
+
const payload = bytes === null ? Buffer.alloc(0) : Buffer.from(bytes);
|
|
7750
|
+
return {
|
|
7751
|
+
present: bytes !== null,
|
|
7752
|
+
bytes: payload.toString("base64"),
|
|
7753
|
+
hash: createHash("sha256").update(bytes === null ? "absent:" : "present:").update(payload).digest("hex")
|
|
7754
|
+
};
|
|
7755
|
+
}
|
|
7756
|
+
/** @private @author ddj 2026年10月27号 @param file Persisted raw image. @returns UTF-8 storage text preserving BOM and terminators. @throws Binary, invalid encoding or corrupt hash. */
|
|
7757
|
+
function rawText(file) {
|
|
7758
|
+
const bytes = Buffer.from(file.bytes, "base64");
|
|
7759
|
+
if (bytes.length > 8388608 || bytes.includes(0)) throw new Error("回滚证据不是受支持的 UTF-8 文本");
|
|
7760
|
+
if (rawReviewFile(file.present ? bytes : null).hash !== file.hash) throw new Error("回滚证据哈希不匹配");
|
|
7761
|
+
const text = new TextDecoder("utf-8", {
|
|
7762
|
+
fatal: true,
|
|
7763
|
+
ignoreBOM: true
|
|
7764
|
+
}).decode(bytes);
|
|
7765
|
+
if (!Buffer.from(text, "utf8").equals(bytes)) throw new Error("回滚证据无法字节往返");
|
|
7766
|
+
return text;
|
|
7767
|
+
}
|
|
7768
|
+
/** @private @author ddj 2026年10月27号 @param text Storage text. @returns Physical lines, retaining each original line ending and excluding BOM. */
|
|
7769
|
+
function rawLines(text) {
|
|
7770
|
+
return text.replace(/^\uFEFF/, "").match(/[^\r\n]*(?:\r\n|\n|\r|$)/g)?.filter((line, index, lines) => line !== "" || index !== lines.length - 1) ?? [];
|
|
7771
|
+
}
|
|
7772
|
+
/** @private @author ddj 2026年10月27号 @param lines Physical storage lines. @returns Official logical-line comparison basis. */
|
|
7773
|
+
function logical(lines) {
|
|
7774
|
+
return lines.map((line) => line.replace(/(?:\r\n|\r|\n)$/, ""));
|
|
7775
|
+
}
|
|
7776
|
+
/** @public @author ddj 2026年10月27号 @param diff Official source. @param evidence Independent raw captures. @returns Exact raw segments only when every official hunk reproduces the entire captured result. @throws Incomplete chain, coarse patch or mismatched context/content. */
|
|
7777
|
+
function evidenceBlocks(diff, evidence) {
|
|
7778
|
+
if (diff.kind !== "text" || diff.coarse) throw new Error("该官方差异没有可靠文本基线");
|
|
7779
|
+
if (diff.before !== evidence.before.present || diff.after !== evidence.after.present) throw new Error("文件存在性证据不一致");
|
|
7780
|
+
const beforeText = rawText(evidence.before), afterText = rawText(evidence.after);
|
|
7781
|
+
const before = rawLines(beforeText), after = rawLines(afterText);
|
|
7782
|
+
const expected = logical(before);
|
|
7783
|
+
let delta = 0;
|
|
7784
|
+
for (const hunk of diff.hunks) {
|
|
7785
|
+
const old = hunk.lines.filter((line) => line[0] === " " || line[0] === "-").map((line) => line.slice(1));
|
|
7786
|
+
const next = hunk.lines.filter((line) => line[0] === " " || line[0] === "+").map((line) => line.slice(1));
|
|
7787
|
+
if (old.length !== hunk.oldLines || next.length !== hunk.newLines) throw new Error("官方差异行数不一致");
|
|
7788
|
+
const position = Math.max(0, hunk.oldStart - 1) + delta;
|
|
7789
|
+
if (JSON.stringify(expected.slice(position, position + old.length)) !== JSON.stringify(old)) throw new Error("原始基线与官方上下文不一致");
|
|
7790
|
+
expected.splice(position, old.length, ...next);
|
|
7791
|
+
delta += next.length - old.length;
|
|
7792
|
+
}
|
|
7793
|
+
if (JSON.stringify(expected) !== JSON.stringify(logical(after))) throw new Error("采集链未覆盖全部官方变更");
|
|
7794
|
+
if (!diff.hunks.length && beforeText !== afterText) throw new Error("官方没有提供可撤销的变更块");
|
|
7795
|
+
const segments = reviewBlocks(diff).map((block) => ({
|
|
7796
|
+
id: block.id,
|
|
7797
|
+
before: before.slice(block.oldStart - 1, block.oldStart - 1 + block.oldLines.length).join(""),
|
|
7798
|
+
after: after.slice(block.newStart - 1, block.newStart - 1 + block.newLines.length).join(""),
|
|
7799
|
+
offset: (afterText.startsWith("") ? 1 : 0) + after.slice(0, block.newStart - 1).join("").length
|
|
7800
|
+
}));
|
|
7801
|
+
let reversed = afterText;
|
|
7802
|
+
for (const segment of [...segments].reverse()) reversed = reversed.slice(0, segment.offset) + segment.before + reversed.slice(segment.offset + segment.after.length);
|
|
7803
|
+
if (reversed !== beforeText) throw new Error("官方变更外的 BOM、行尾或末尾换行发生变化,证据不足");
|
|
7804
|
+
return segments;
|
|
7805
|
+
}
|
|
7806
|
+
/** @public @author ddj 2026年10月27号 @param evidence Raw original images. @returns Exact before and after UTF-8 storage text for guarded writes. */
|
|
7807
|
+
function evidenceText(evidence) {
|
|
7808
|
+
return {
|
|
7809
|
+
before: rawText(evidence.before),
|
|
7810
|
+
after: rawText(evidence.after)
|
|
7811
|
+
};
|
|
7812
|
+
}
|
|
7813
|
+
//#endregion
|
|
7814
|
+
//#region src/reviewCapture.ts
|
|
7815
|
+
/** Independent byte evidence; official diff remains the only display authority. @author ddj 2026年10月27号 */
|
|
7816
|
+
/** @public @author ddj 2026年10月27号 @param ctx Host lifecycle owner. @returns Scoped byte recorder; failures disable undo evidence only, never tool execution. */
|
|
7817
|
+
function installEvidence(ctx) {
|
|
7818
|
+
const chains = /* @__PURE__ */ new Map();
|
|
7819
|
+
const staged = /* @__PURE__ */ new Map();
|
|
7820
|
+
let stopped = false;
|
|
7821
|
+
ctx.on("tools/execute", aroundTool);
|
|
7822
|
+
ctx.on("session/event", onEvent);
|
|
7823
|
+
ctx.on("session/disposed", onDisposed);
|
|
7824
|
+
/** @private @author ddj 2026年10月27号 @param session Disposed owner. Drops optional in-memory evidence, never persisted official facts. */
|
|
7825
|
+
function onDisposed(session) {
|
|
7826
|
+
chains.delete(session.id);
|
|
7827
|
+
}
|
|
7828
|
+
/** @private @author ddj 2026年10月27号 @returns Current bounded capture footprint, excluding transient filesystem reads. */
|
|
7829
|
+
function footprint() {
|
|
7830
|
+
let bytes = 0;
|
|
7831
|
+
for (const group of [chains, staged]) for (const files of group.values()) for (const chain of files.values()) bytes += chain.before.bytes.length + chain.after.bytes.length;
|
|
7832
|
+
return bytes;
|
|
7833
|
+
}
|
|
7834
|
+
/** @private @author ddj 2026年10月27号 @param exec Allowed tool dispatch. @param next Original dispatch. @returns Original result unchanged, regardless of capture failure. */
|
|
7835
|
+
async function aroundTool(exec, next) {
|
|
7836
|
+
const session = exec.agent?.session;
|
|
7837
|
+
const path = exec.arguments?.file_path;
|
|
7838
|
+
if (stopped || !session?.id || !["edit", "write"].includes(exec.name) || typeof path !== "string") return next();
|
|
7839
|
+
let fs;
|
|
7840
|
+
let chain;
|
|
7841
|
+
let target;
|
|
7842
|
+
try {
|
|
7843
|
+
if (chains.size >= 16 && !chains.has(session.id) || footprint() >= 33554432) return next();
|
|
7844
|
+
fs = ctx.get("fs");
|
|
7845
|
+
target = await resolveTarget(ctx, session, path);
|
|
7846
|
+
const identity = fs.processPath(target);
|
|
7847
|
+
const before = await readRaw(fs, target, exec.signal);
|
|
7848
|
+
let files = chains.get(session.id);
|
|
7849
|
+
if (!files) {
|
|
7850
|
+
files = /* @__PURE__ */ new Map();
|
|
7851
|
+
chains.set(session.id, files);
|
|
7852
|
+
}
|
|
7853
|
+
if (files.size >= 64 && !files.has(identity)) return next();
|
|
7854
|
+
chain = files.get(identity);
|
|
7855
|
+
if (!chain) {
|
|
7856
|
+
chain = {
|
|
7857
|
+
before,
|
|
7858
|
+
after: before,
|
|
7859
|
+
busy: false,
|
|
7860
|
+
invalid: false
|
|
7861
|
+
};
|
|
7862
|
+
files.set(identity, chain);
|
|
7863
|
+
}
|
|
7864
|
+
if (chain.busy || chain.after.hash !== before.hash) chain.invalid = true;
|
|
7865
|
+
chain.busy = true;
|
|
7866
|
+
} catch {}
|
|
7867
|
+
try {
|
|
7868
|
+
return await next();
|
|
7869
|
+
} finally {
|
|
7870
|
+
if (chain) {
|
|
7871
|
+
try {
|
|
7872
|
+
chain.after = await readRaw(fs, target, exec.signal);
|
|
7873
|
+
} catch {
|
|
7874
|
+
chain.invalid = true;
|
|
7875
|
+
}
|
|
7876
|
+
chain.busy = false;
|
|
7877
|
+
}
|
|
7878
|
+
}
|
|
7879
|
+
}
|
|
7880
|
+
/** @private @author ddj 2026年10月27号 @param session Source session. @param event Published official turn. Freezes the chain before the next turn can mutate it. */
|
|
7881
|
+
function onEvent(session, event) {
|
|
7882
|
+
if (stopped || event.type !== "workspace/changes") return;
|
|
7883
|
+
const files = chains.get(session.id);
|
|
7884
|
+
chains.delete(session.id);
|
|
7885
|
+
if (!files || staged.size >= 128) return;
|
|
7886
|
+
staged.set(JSON.stringify([session.id, event.seq]), structuredClone(files));
|
|
7887
|
+
}
|
|
7888
|
+
/** @public @author ddj 2026年10月27号 @param cwd Official workspace. @param snapshot Committed official facts. Persists only independently validated, exact-correspondence byte chains. */
|
|
7889
|
+
function persist(cwd, snapshot) {
|
|
7890
|
+
const key = JSON.stringify([snapshot.sessionId, snapshot.seq]);
|
|
7891
|
+
const files = staged.get(key);
|
|
7892
|
+
staged.delete(key);
|
|
7893
|
+
if (!files || stopped) return;
|
|
7894
|
+
const db = reviewDb(cwd);
|
|
7895
|
+
for (const file of snapshot.files) {
|
|
7896
|
+
const chain = files.get(file.identity);
|
|
7897
|
+
if (!chain || chain.invalid || chain.busy) continue;
|
|
7898
|
+
try {
|
|
7899
|
+
const evidence = {
|
|
7900
|
+
before: chain.before,
|
|
7901
|
+
after: chain.after
|
|
7902
|
+
};
|
|
7903
|
+
evidenceBlocks(file.diff, evidence);
|
|
7904
|
+
db.prepare(`INSERT OR IGNORE INTO review_evidence(file_id,payload)
|
|
7905
|
+
SELECT id,? FROM review_files WHERE session_id=? AND turn=? AND seq=? AND identity=?`).run(JSON.stringify(evidence), snapshot.sessionId, snapshot.turn, snapshot.seq, file.identity);
|
|
7906
|
+
} catch (error) {
|
|
7907
|
+
log.warn("官方回滚证据不可用:" + String(error));
|
|
7908
|
+
}
|
|
7909
|
+
}
|
|
7910
|
+
}
|
|
7911
|
+
/** @public @author ddj 2026年10月27号 Release bounded in-memory images after capture has drained. */
|
|
7912
|
+
function dispose() {
|
|
7913
|
+
stopped = true;
|
|
7914
|
+
chains.clear();
|
|
7915
|
+
staged.clear();
|
|
7916
|
+
}
|
|
7917
|
+
return {
|
|
7918
|
+
persist,
|
|
7919
|
+
dispose
|
|
7920
|
+
};
|
|
7921
|
+
}
|
|
7922
|
+
/** @private @author ddj 2026年10月27号 @param fs Public filesystem. @param target Backend identity. @returns Raw bounded file or confirmed missing. @throws Any non-missing read error. */
|
|
7923
|
+
async function readRaw(fs, target, signal) {
|
|
7924
|
+
try {
|
|
7925
|
+
return rawReviewFile(await fs.readBytes(target, signal, 1048576));
|
|
7926
|
+
} catch (error) {
|
|
7927
|
+
const code = error.code;
|
|
7928
|
+
if (code === "ENOENT" || code === "FS_NOT_FOUND") return rawReviewFile(null);
|
|
7929
|
+
throw error;
|
|
7930
|
+
}
|
|
7931
|
+
}
|
|
7932
|
+
//#endregion
|
|
7635
7933
|
//#region src/search/query.ts
|
|
7636
7934
|
/**
|
|
7637
7935
|
* 规范化用户搜索文本。
|
|
@@ -12573,6 +12871,97 @@ function decodePage(bytes, info, offset, pendingCR, args, identity, signCursor)
|
|
|
12573
12871
|
};
|
|
12574
12872
|
}
|
|
12575
12873
|
//#endregion
|
|
12874
|
+
//#region src/reviewUndo.ts
|
|
12875
|
+
/** Guarded official undo with a durable write-intent journal. @author ddj 2026年10月27号 */
|
|
12876
|
+
const writes = /* @__PURE__ */ new Map();
|
|
12877
|
+
/** @public @author ddj 2026年10月27号 @param ctx Host capability. @param session Authenticated caller. @param cwd Workspace. @param request Frozen file/block version. @returns Durable decision; retries reconcile disk hashes and never blindly repeat a write. @throws Missing evidence, stale state, policy or uncertain write failure. */
|
|
12878
|
+
async function undoReview(ctx, session, cwd, request) {
|
|
12879
|
+
const detail = reviewDetail(cwd, request.id);
|
|
12880
|
+
const key = JSON.stringify([cwd, detail?.item.currentPath ?? request.id]);
|
|
12881
|
+
const operation = (writes.get(key) ?? Promise.resolve()).catch(() => void 0).then(() => runUndo(ctx, session, cwd, request));
|
|
12882
|
+
writes.set(key, operation);
|
|
12883
|
+
try {
|
|
12884
|
+
return await operation;
|
|
12885
|
+
} finally {
|
|
12886
|
+
if (writes.get(key) === operation) writes.delete(key);
|
|
12887
|
+
}
|
|
12888
|
+
}
|
|
12889
|
+
/** @private @author ddj 2026年10月27号 @param ctx Host. @param session Caller. @param cwd Workspace. @param request Frozen decision. @returns Serialized guarded mutation and journal reconciliation. @throws Conflicting bytes or decisions. */
|
|
12890
|
+
async function runUndo(ctx, session, cwd, request) {
|
|
12891
|
+
const db = reviewDb(cwd);
|
|
12892
|
+
const encoded = JSON.stringify(request);
|
|
12893
|
+
if (!request.operationId || request.operationId.length > 128) throw new Error("operationId 无效");
|
|
12894
|
+
const prior = db.prepare("SELECT * FROM review_ops WHERE operation_id=?").get(request.operationId);
|
|
12895
|
+
if (prior && prior.request !== encoded) throw new Error("operationId 已用于其他请求");
|
|
12896
|
+
if (prior?.state === "committed") return JSON.parse(prior.result);
|
|
12897
|
+
const detail = reviewDetail(cwd, request.id);
|
|
12898
|
+
if (!detail || !detail.item.current || detail.item.seq !== request.seq || detail.item.revision !== request.revision || detail.item.decision !== "pending") throw new Error("审查版本冲突,请刷新后重试");
|
|
12899
|
+
const fs = ctx.get("fs");
|
|
12900
|
+
if (!fs?.readBytes || !fs?.writeText || !fs?.stat) throw new Error("后端缺少受控回滚能力");
|
|
12901
|
+
const target = await resolveTarget(ctx, session, detail.item.currentPath);
|
|
12902
|
+
const info = await fs.stat(target);
|
|
12903
|
+
if (!info || info.type !== "file" || !info.version) throw new Error("文件不存在或缺少版本守卫,不能安全 Undo");
|
|
12904
|
+
const current = rawReviewFile(await fs.readBytes(target, void 0, 8388608));
|
|
12905
|
+
if ((await fs.stat(target))?.version !== info.version) throw new Error("文件在回滚读取期间变化,未修改文件");
|
|
12906
|
+
if (prior) {
|
|
12907
|
+
const journal = JSON.parse(prior.evidence);
|
|
12908
|
+
if (current.hash === journal.after) return finishUndo(cwd, request);
|
|
12909
|
+
if (current.hash !== journal.before) throw new Error("上次 Undo 状态不确定:磁盘既非写前也非写后,禁止重复执行");
|
|
12910
|
+
}
|
|
12911
|
+
if (db.prepare("SELECT operation_id FROM review_ops WHERE state='prepared' AND json_extract(request,'$.id')=? AND operation_id<>?").get(request.id, request.operationId)) throw new Error("存在未核对的 Undo,请使用原操作重试,不能创建另一项写入");
|
|
12912
|
+
const row = db.prepare("SELECT payload FROM review_evidence WHERE file_id=?").get(request.id);
|
|
12913
|
+
if (!row) throw new Error("该历史记录缺少可信原始字节证据,不能 Undo;差异仍可展示和 Keep");
|
|
12914
|
+
const evidence = JSON.parse(row.payload);
|
|
12915
|
+
const segments = evidenceBlocks(detail.diff, evidence);
|
|
12916
|
+
const texts = evidenceText(evidence);
|
|
12917
|
+
if (!evidence.before.present || !evidence.after.present) throw new Error("当前后端未提供受控原子删除/重建能力");
|
|
12918
|
+
const selected = detail.blocks.filter((block) => block.decision === "pending" && (request.scope !== "hunk" || block.id === request.blockId));
|
|
12919
|
+
if (!selected.length) throw new Error("差异块不存在或已处理");
|
|
12920
|
+
const rejected = new Set(detail.blocks.filter((block) => block.decision === "rejected").map((block) => block.id));
|
|
12921
|
+
const expected = reverseBlocks(texts.after, segments, rejected);
|
|
12922
|
+
if (rawReviewFile(Buffer.from(expected)).hash !== current.hash) throw new Error("当前文件已被后续编辑,版本与回滚证据冲突,未修改文件");
|
|
12923
|
+
const undo = /* @__PURE__ */ new Set([...rejected, ...selected.map((block) => block.id)]);
|
|
12924
|
+
const next = reverseBlocks(texts.after, segments, undo);
|
|
12925
|
+
const journal = {
|
|
12926
|
+
before: current.hash,
|
|
12927
|
+
after: rawReviewFile(Buffer.from(next)).hash
|
|
12928
|
+
};
|
|
12929
|
+
const latest = reviewDetail(cwd, request.id);
|
|
12930
|
+
if (!latest?.item.current || latest.item.revision !== request.revision || latest.item.decision !== "pending") throw new Error("审查版本在回滚校验期间变化,未修改文件");
|
|
12931
|
+
db.prepare("INSERT INTO review_ops VALUES(?,?,'prepared',NULL,?) ON CONFLICT(operation_id) DO NOTHING").run(request.operationId, encoded, JSON.stringify(journal));
|
|
12932
|
+
await fs.writeText(target, next, {
|
|
12933
|
+
kind: "replaceIfVersion",
|
|
12934
|
+
version: info.version
|
|
12935
|
+
}, void 0, policyOf(ctx, session));
|
|
12936
|
+
if (rawReviewFile(await fs.readBytes(target, void 0, 8388608)).hash !== journal.after) throw new Error("Undo 写后内容不确定,请保留原操作重试核对");
|
|
12937
|
+
return finishUndo(cwd, request);
|
|
12938
|
+
}
|
|
12939
|
+
/** @private @author ddj 2026年10月27号 @param text Captured result. @param segments Validated raw ranges. @param rejected Blocks to reverse. @returns Raw storage text preserving unselected blocks. */
|
|
12940
|
+
function reverseBlocks(text, segments, rejected) {
|
|
12941
|
+
let result = text;
|
|
12942
|
+
for (const segment of [...segments].reverse()) {
|
|
12943
|
+
if (!rejected.has(segment.id)) continue;
|
|
12944
|
+
result = result.slice(0, segment.offset) + segment.before + result.slice(segment.offset + segment.after.length);
|
|
12945
|
+
}
|
|
12946
|
+
return result;
|
|
12947
|
+
}
|
|
12948
|
+
/** @private @author ddj 2026年10月27号 @param cwd Workspace. @param request Frozen action. @returns Transactionally committed status, or a retryable prepared journal if the database fails. */
|
|
12949
|
+
function finishUndo(cwd, request) {
|
|
12950
|
+
const db = reviewDb(cwd);
|
|
12951
|
+
db.exec("BEGIN IMMEDIATE");
|
|
12952
|
+
try {
|
|
12953
|
+
if (db.prepare("UPDATE review_files SET revision=revision+1 WHERE id=? AND seq=? AND revision=? AND current=1 AND decision='pending'").run(request.id, request.seq, request.revision).changes !== 1) throw new Error("审查版本冲突,请核对已执行 Undo");
|
|
12954
|
+
changeBlocks(db, request);
|
|
12955
|
+
const result = { revision: request.revision + 1 };
|
|
12956
|
+
db.prepare("UPDATE review_ops SET state='committed',result=? WHERE operation_id=?").run(JSON.stringify(result), request.operationId);
|
|
12957
|
+
db.exec("COMMIT");
|
|
12958
|
+
return result;
|
|
12959
|
+
} catch (error) {
|
|
12960
|
+
if (db.isTransaction) db.exec("ROLLBACK");
|
|
12961
|
+
throw error;
|
|
12962
|
+
}
|
|
12963
|
+
}
|
|
12964
|
+
//#endregion
|
|
12576
12965
|
//#region src/reviewRpc.ts
|
|
12577
12966
|
/** @public @author ddj 2026年10月26号 @param ctx Host. @param registry Workspace cache. @returns Handlers resolving workspace from the live authenticated session, never caller cwd. */
|
|
12578
12967
|
function reviewHandlers(ctx, registry) {
|
|
@@ -12602,10 +12991,18 @@ function reviewHandlers(ctx, registry) {
|
|
|
12602
12991
|
};
|
|
12603
12992
|
},
|
|
12604
12993
|
"edrv.review.decide": async (args) => {
|
|
12605
|
-
|
|
12606
|
-
|
|
12607
|
-
|
|
12608
|
-
|
|
12994
|
+
const cwd = await workspace(args.sessionId);
|
|
12995
|
+
try {
|
|
12996
|
+
return {
|
|
12997
|
+
ok: true,
|
|
12998
|
+
...args.decision === "rejected" ? await undoReview(ctx, sessionOf(ctx, args.sessionId), cwd, args) : decideReview(cwd, args)
|
|
12999
|
+
};
|
|
13000
|
+
} catch (error) {
|
|
13001
|
+
return {
|
|
13002
|
+
ok: false,
|
|
13003
|
+
error: error instanceof Error ? error.message : String(error)
|
|
13004
|
+
};
|
|
13005
|
+
}
|
|
12609
13006
|
}
|
|
12610
13007
|
};
|
|
12611
13008
|
}
|
|
@@ -25249,7 +25646,11 @@ function createRuntime(ctx, config) {
|
|
|
25249
25646
|
* @param runtime Managers created by this instance.
|
|
25250
25647
|
*/
|
|
25251
25648
|
function attachRuntime(ctx, runtime) {
|
|
25252
|
-
const
|
|
25649
|
+
const evidence = installEvidence(ctx);
|
|
25650
|
+
const stopCapture = installCapture(ctx, (cwd, snapshot) => {
|
|
25651
|
+
evidence.persist(cwd, snapshot);
|
|
25652
|
+
return true;
|
|
25653
|
+
});
|
|
25253
25654
|
/** @author ddj 2026年09月28号 @param exec Tool execution. @param result Tool result. */
|
|
25254
25655
|
function onResult(exec, result) {
|
|
25255
25656
|
captureToolResult(ctx, runtime.registry, exec, result);
|
|
@@ -25268,6 +25669,7 @@ function attachRuntime(ctx, runtime) {
|
|
|
25268
25669
|
/** @author ddj 2026年09月28号 Release every manager even if another cleanup rejects. */
|
|
25269
25670
|
async function cleanup() {
|
|
25270
25671
|
await stopCapture();
|
|
25672
|
+
evidence.dispose();
|
|
25271
25673
|
runtime.fileVersions.dispose();
|
|
25272
25674
|
closeArchiveDbs();
|
|
25273
25675
|
disposeAllServers();
|