Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,5 @@ node dist/cli/index.js verify --path fixtures/product/trust-loop/review --target
**需要 API Key 吗?** 离线核验无需环境变量或密钥。运行 `gui --state-dir <目录>` 可指定报告存储位置。

[使用指南](docs/user-guide.md) · [构建桌面应用](docs/build.md) · [隐私与备份](docs/privacy.md) · [贡献指南](CONTRIBUTING.md)

修复前的持久备份、并发编辑保全和中断后的核对步骤见[修复与恢复](docs/repair-recovery.md)。工作台会区分完成历史和保留的未完成材料。
34 changes: 34 additions & 0 deletions docs/repair-recovery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# 修复备份与中断恢复

`fix --apply` 只修改已审核计划里的文件。开始前停止编辑和运行同一工作区;它不是抵御同一OS用户恶意进程的沙箱,也不是多应用协同编辑协议。

```sh
skillsync fix --apply --plan /absolute/path/plan.json --yes --backup \
--backup-dir /absolute/private/repair-backups --format json
skillsync backup-check --input /absolute/private/repair-backups/slot-00/operation-id
```

CLI默认使用 `~/.skillsync/repair-backups`。工作台使用其私有状态目录中的 `repair-backups`;迁移工作台时复制整个状态目录。备份只留在本机,不上传。历史摘要导出不包含备份路径或源文件内容。

备份目录要求私有权限和当前用户所有权(POSIX);不会自动更改已有目录权限。最多保留16次操作,达到上限先验证并归档,或指定新的私有目录;程序不自动清理旧材料。单文件16 MiB,准备快照和修复输出各最多64 MiB。备份还可能保留捕获的原文件和失败的新版本,不应只按快照大小估算整个目录占用。

## 提交和冲突

准备阶段保存 `files/` 和清单,包含相对路径、字节、原权限及SHA-256。每个目标在提交前再次核对;真正发布时先把实际旧文件保留到 `captured/`,再使用不覆盖已有目录项的操作发布新文件。回滚同样先核对当前内容,遇到更晚的编辑不会强制盖回旧版本,失败的新版本可保留在 `rollback/`。

这要求备份和目标在同一文件系统且支持文件硬链接;不支持时会拒绝修复,请选择同卷的私有备份目录。发布存在一个短暂的文件名切换窗口,读者应在修复期间停写/停用相关文件。若进程恰在捕获后中断,原文件仍在备份内,但可能需要人工恢复;不能把这种状态称为应用成功。

`backup-check`只读核验 `files/` 冻结快照,不认证作者,也不证明之后没有编辑。`captured/` 可能保存比计划更新的内容,或被仍持有原文件的其他编辑器继续写入;应对照检查,不能盲目以冻结快照覆盖它。

## 恢复顺序

1. 停止修复和编辑,保留原工作区、整个备份目录及原计划。结果未确认时不要再应用一次。
2. 用 `backup-check` 检查清单和冻结快照,核对回执中的 `planDigest`。旧临时目录备份不含新清单时应保留并人工核对,不能伪造为已验证的新备份。
3. 复制原工作区到一个新目录;比较 `files/`、`captured/`、`rollback/` 与当前文件。只在新副本中恢复选定版本,再运行只读核验。
4. 新副本确认正确后,再由用户决定切换。程序不重置原目录,不自动重放Skill,不删除未知材料。

## 未完成历史

报告数据和索引分别以完整文件发布;索引只有在写入并同步后才发布。中断、存储不足或索引失败的材料保留在状态目录,历史栏显示“未完成或无法读取”,不计入完成记录,也不会自动变成可应用计划。它们仍占用容量,这是保护恢复材料的设计;请先归档核对,再使用新的状态目录。

文件同步改善突然退出时的恢复条件,但不是对断电、介质损坏或所有文件系统的零丢失保证;Windows的Node接口不提供相同的目录同步能力。唯一资料仍应有独立、加密并验证过的备份。
2 changes: 2 additions & 0 deletions docs/workbench-history.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,3 +208,5 @@ Existing history comparison remains separately readonly, with its own concurrenc
A page leaving for navigation also releases its report string and hides the old result view. Returning from the browser cache cannot display that prior report as the current result; its immutable history record is retained and can be reopened explicitly. Form path nodes are not cleared by status polling.

Local source validation:766 tests passed,1 optional Docker-reference test skipped; type checking,lint and build passed. Native synthetic Chrome390px covered refresh/current ID, stale cancel refusal, apply-in-progress refresh/refusal, explicit safe fixture fix, server restart/old nonce denial and browser return with the old report hidden. Seven immutable history files stayed byte-identical. The Skill script was never executed; Provider calls0. Formal release and external business acceptance remain separate.

修复备份位于私有状态目录内的 `repair-backups/`,由独立的16次操作上限管理,不计入核验报告的64MiB/120文件计数。历史索引失败后保留的材料会显示为未完成,不计作核验成功。恢复与旧临时备份兼容边界见[修复与恢复](repair-recovery.md)。
2 changes: 2 additions & 0 deletions src/cli/commands/fix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export type FixApplyOptions = {
yes: boolean;
approveReviewRequired?: boolean;
backup?: boolean;
backupDirectory?: string;
};

function selectIssues(issues: Issue[], issueIds: string[] | undefined): Issue[] {
Expand Down Expand Up @@ -112,6 +113,7 @@ export async function runFixApply(options: FixApplyOptions): Promise<ApplyReceip
yes: options.yes,
approveReviewRequired: options.approveReviewRequired,
backup: options.backup,
backupDirectory: options.backupDirectory,
});
}

Expand Down
12 changes: 10 additions & 2 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#!/usr/bin/env node
import { inspectRepairBackup } from "../domain/repair-backups.js";

import { realpathSync } from "node:fs";
import { pathToFileURL } from "node:url";
Expand Down Expand Up @@ -192,6 +193,7 @@ type FixCliOptions = {
yes?: boolean;
approveReviewRequired?: boolean;
backup?: boolean;
backupDir?: string;
format?: string;
};

Expand Down Expand Up @@ -350,6 +352,10 @@ export function createCli(io: CliIO = defaultCliIO): Command {
io.writeOut(renderExplain(issue, options.format));
});

program.command("backup-check").description("Read-only checksum validation of one retained repair backup.")
.requiredOption("--input <path>", "Retained repair backup directory")
.action(async (options: { input: string }) => { io.writeOut(JSON.stringify(await inspectRepairBackup(options.input), null, 2)); });

program
.command("fix")
.description("Plan or explicitly apply fixes for verification Issues.")
Expand All @@ -362,15 +368,16 @@ export function createCli(io: CliIO = defaultCliIO): Command {
.option("--output <path>", "Write the generated ActionPlan JSON to this new file")
.option("--yes", "Confirm the explicit ActionPlan apply")
.option("--approve-review-required", "Approve review-required ActionPlan changes")
.option("--backup", "Retain backups while applying the ActionPlan")
.option("--backup", "Retain private backups while applying the ActionPlan")
.option("--backup-dir <path>", "Private repair backup directory (default: ~/.skillsync/repair-backups)")
.option("--format <format>", "Output format: text or json", "text")
.action(async (options: FixCliOptions) => {
if (options.apply) {
if (typeof options.plan !== "string") {
throw new Error("fix --apply requires --plan <plan-file>.");
}
if (options.path || options.target || options.policy || options.issue || options.output) {
throw new Error("fix --apply accepts only --plan, --yes, --approve-review-required, --backup, and --format.");
throw new Error("fix --apply accepts only --plan, --yes, --approve-review-required, --backup, --backup-dir, and --format.");
}
if (options.format !== "text" && options.format !== "json") {
throw new Error(`Unsupported fix output format: ${options.format}`);
Expand All @@ -381,6 +388,7 @@ export function createCli(io: CliIO = defaultCliIO): Command {
yes: options.yes === true,
approveReviewRequired: options.approveReviewRequired,
backup: options.backup,
backupDirectory: options.backupDir,
} satisfies FixApplyOptions);
io.writeOut(renderFixApply(receipt, options.format));
} catch (error: unknown) {
Expand Down
Loading
Loading