berry-agent 0.1.0-alpha.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.
- package/LICENSE +21 -0
- package/README.md +96 -0
- package/dist/.api-emit.stamp +1 -0
- package/dist/.build-meta.json +6 -0
- package/dist/agent/events.js +1 -0
- package/dist/agent/index.js +2 -0
- package/dist/agent/loop.js +139 -0
- package/dist/agent/queue.js +119 -0
- package/dist/agent/stream.js +62 -0
- package/dist/agent/tools-batch.js +221 -0
- package/dist/agent/types.js +8 -0
- package/dist/api/berry-agent-llm.d.ts +18 -0
- package/dist/api/berry-agent.d.ts +9 -0
- package/dist/api/surface.json +1632 -0
- package/dist/api/tsconfig.paths.json +12 -0
- package/dist/api/typebox-compile.d.ts +2 -0
- package/dist/api/typebox-value.d.ts +2 -0
- package/dist/api/typebox.d.ts +2 -0
- package/dist/browser/cdp.js +256 -0
- package/dist/browser/codes.js +31 -0
- package/dist/browser/discover.js +107 -0
- package/dist/browser/engine.js +169 -0
- package/dist/browser/index.js +17 -0
- package/dist/browser/install.js +342 -0
- package/dist/browser/page.js +321 -0
- package/dist/browser/service.js +158 -0
- package/dist/browser/tools.js +198 -0
- package/dist/browser/types.js +81 -0
- package/dist/channels/ask-queue.js +82 -0
- package/dist/channels/commands.js +129 -0
- package/dist/channels/engine/cell.js +143 -0
- package/dist/channels/engine/diff.js +143 -0
- package/dist/channels/engine/engine.js +416 -0
- package/dist/channels/engine/index.js +8 -0
- package/dist/channels/engine/input-keys.js +86 -0
- package/dist/channels/engine/input.js +596 -0
- package/dist/channels/engine/memory-io.js +71 -0
- package/dist/channels/engine/process-io.js +38 -0
- package/dist/channels/engine/types.js +17 -0
- package/dist/channels/engine/width.js +122 -0
- package/dist/channels/index.js +15 -0
- package/dist/channels/registry.js +56 -0
- package/dist/channels/sdk/admit.js +29 -0
- package/dist/channels/sdk/backend.js +102 -0
- package/dist/channels/sdk/cursor.js +56 -0
- package/dist/channels/sdk/index.js +20 -0
- package/dist/channels/sdk/jsonl.js +208 -0
- package/dist/channels/sdk/protocol.js +24 -0
- package/dist/channels/sdk/schema.js +91 -0
- package/dist/channels/sdk/wire-core.js +446 -0
- package/dist/channels/service.js +169 -0
- package/dist/channels/tui/autocomplete/autocomplete.js +42 -0
- package/dist/channels/tui/autocomplete/file-mentions.js +106 -0
- package/dist/channels/tui/autocomplete/popup.js +148 -0
- package/dist/channels/tui/autocomplete/provider.js +7 -0
- package/dist/channels/tui/autocomplete/token.js +68 -0
- package/dist/channels/tui/backend/ansi-rows.js +211 -0
- package/dist/channels/tui/backend/main-screen.js +207 -0
- package/dist/channels/tui/backend/osc.js +91 -0
- package/dist/channels/tui/backend/transcript.js +230 -0
- package/dist/channels/tui/backend/tui-backend.js +915 -0
- package/dist/channels/tui/editor/editor-model.js +543 -0
- package/dist/channels/tui/editor/editor-view.js +140 -0
- package/dist/channels/tui/editor/editor.js +275 -0
- package/dist/channels/tui/editor/undo-stack.js +35 -0
- package/dist/channels/tui/editor/visual-lines.js +126 -0
- package/dist/channels/tui/editor/word-nav.js +106 -0
- package/dist/channels/tui/history/history-viewer.js +403 -0
- package/dist/channels/tui/index.js +3 -0
- package/dist/channels/tui/layout.js +167 -0
- package/dist/channels/tui/markdown/blocks.js +111 -0
- package/dist/channels/tui/markdown/inline.js +75 -0
- package/dist/channels/tui/markdown/markdown.js +182 -0
- package/dist/channels/tui/memory/memory-viewer.js +540 -0
- package/dist/channels/tui/overlay/alt-screen.js +105 -0
- package/dist/channels/tui/overlay/overlay.js +57 -0
- package/dist/channels/tui/overlay/select-confirm.js +156 -0
- package/dist/channels/tui/panels/todo-panel.js +54 -0
- package/dist/channels/tui/panels/tool-progress-panel.js +93 -0
- package/dist/channels/tui/scroll/scroll-view.js +254 -0
- package/dist/channels/tui/status/status-line.js +76 -0
- package/dist/channels/tui/text.js +38 -0
- package/dist/channels/tui/theme.js +41 -0
- package/dist/channels/types.js +11 -0
- package/dist/channels/ui-core.js +209 -0
- package/dist/checkpoint/capture.js +45 -0
- package/dist/checkpoint/codes.js +33 -0
- package/dist/checkpoint/command.js +82 -0
- package/dist/checkpoint/gate.js +48 -0
- package/dist/checkpoint/index.js +20 -0
- package/dist/checkpoint/restore.js +172 -0
- package/dist/checkpoint/store.js +247 -0
- package/dist/checkpoint/types.js +4 -0
- package/dist/checkpoint/walk.js +146 -0
- package/dist/compaction/ccr-tools.js +112 -0
- package/dist/compaction/ccr.js +84 -0
- package/dist/compaction/codes.js +21 -0
- package/dist/compaction/index.js +18 -0
- package/dist/compaction/policy.js +200 -0
- package/dist/compaction/service.js +422 -0
- package/dist/compaction/slots.js +121 -0
- package/dist/compaction/types.js +10 -0
- package/dist/context/codes.js +40 -0
- package/dist/context/events.js +160 -0
- package/dist/context/index.js +13 -0
- package/dist/context/logger.js +136 -0
- package/dist/context/scope.js +182 -0
- package/dist/context/workspace.js +104 -0
- package/dist/contracts/agent-events.d.ts +73 -0
- package/dist/contracts/agent-events.js +16 -0
- package/dist/contracts/api.d.ts +249 -0
- package/dist/contracts/api.js +299 -0
- package/dist/contracts/approval.d.ts +37 -0
- package/dist/contracts/approval.js +9 -0
- package/dist/contracts/env-ref.d.ts +41 -0
- package/dist/contracts/env-ref.js +32 -0
- package/dist/contracts/errors.d.ts +56 -0
- package/dist/contracts/errors.js +266 -0
- package/dist/contracts/events.d.ts +70 -0
- package/dist/contracts/events.js +329 -0
- package/dist/contracts/index.d.ts +32 -0
- package/dist/contracts/index.js +26 -0
- package/dist/contracts/llm.d.ts +275 -0
- package/dist/contracts/llm.js +12 -0
- package/dist/contracts/messages.d.ts +63 -0
- package/dist/contracts/messages.js +65 -0
- package/dist/contracts/redact.d.ts +46 -0
- package/dist/contracts/redact.js +223 -0
- package/dist/contracts/tools.d.ts +197 -0
- package/dist/contracts/tools.js +33 -0
- package/dist/contracts/types.d.ts +302 -0
- package/dist/contracts/types.js +50 -0
- package/dist/contracts/ui.d.ts +122 -0
- package/dist/contracts/ui.js +15 -0
- package/dist/conversation/agent-service.js +61 -0
- package/dist/conversation/approval-wiring.js +82 -0
- package/dist/conversation/backoff.js +38 -0
- package/dist/conversation/codes.js +47 -0
- package/dist/conversation/control-tools.js +102 -0
- package/dist/conversation/control.js +210 -0
- package/dist/conversation/driver.js +922 -0
- package/dist/conversation/index.js +12 -0
- package/dist/conversation/model-visible.js +169 -0
- package/dist/conversation/open-tools.js +126 -0
- package/dist/conversation/reseed.js +132 -0
- package/dist/conversation/sessions.js +215 -0
- package/dist/conversation/todo.js +203 -0
- package/dist/conversation/types.js +35 -0
- package/dist/conversation/wiring.js +204 -0
- package/dist/credentials/codes.js +59 -0
- package/dist/credentials/commands.js +186 -0
- package/dist/credentials/env-ref.js +34 -0
- package/dist/credentials/index.js +28 -0
- package/dist/credentials/migration.js +18 -0
- package/dist/credentials/oauth.js +266 -0
- package/dist/credentials/refresh.js +152 -0
- package/dist/credentials/secrets.js +118 -0
- package/dist/credentials/types.js +30 -0
- package/dist/exec/bash.js +279 -0
- package/dist/exec/codes.js +37 -0
- package/dist/exec/env.js +99 -0
- package/dist/exec/environment.js +39 -0
- package/dist/exec/git-guard.js +623 -0
- package/dist/exec/index.js +24 -0
- package/dist/exec/registry.js +120 -0
- package/dist/exec/spawn.js +281 -0
- package/dist/exec/tail.js +77 -0
- package/dist/exec/types.js +5 -0
- package/dist/goal/codes.js +33 -0
- package/dist/goal/command.js +175 -0
- package/dist/goal/fold.js +121 -0
- package/dist/goal/gates.js +122 -0
- package/dist/goal/index.js +22 -0
- package/dist/goal/migration.js +48 -0
- package/dist/goal/service.js +501 -0
- package/dist/goal/todo-tool.js +174 -0
- package/dist/goal/types.js +1 -0
- package/dist/goal/update-tool.js +46 -0
- package/dist/host/approval-cmd.js +276 -0
- package/dist/host/assembly.js +1146 -0
- package/dist/host/boot-failures.js +104 -0
- package/dist/host/budget-advisory.js +74 -0
- package/dist/host/budget-broadcast.js +50 -0
- package/dist/host/builtins.js +54 -0
- package/dist/host/cli.js +544 -0
- package/dist/host/codes.js +120 -0
- package/dist/host/config-schema.js +195 -0
- package/dist/host/conversation-stack.js +654 -0
- package/dist/host/core-plugins.js +1685 -0
- package/dist/host/credentials-cmd.js +42 -0
- package/dist/host/disclosure.js +30 -0
- package/dist/host/dispatch.js +105 -0
- package/dist/host/doors-cmd.js +154 -0
- package/dist/host/dump-config.js +44 -0
- package/dist/host/hook-dispatch-guard.js +27 -0
- package/dist/host/import-gate.js +153 -0
- package/dist/host/index.js +54 -0
- package/dist/host/issue-session.js +241 -0
- package/dist/host/loader.js +372 -0
- package/dist/host/main.js +161 -0
- package/dist/host/manifest.js +381 -0
- package/dist/host/mcp-entry.js +53 -0
- package/dist/host/plugin-boot.js +952 -0
- package/dist/host/plugin-context.js +672 -0
- package/dist/host/plugin-install.js +480 -0
- package/dist/host/plugin-reload.js +124 -0
- package/dist/host/plugin-store.js +462 -0
- package/dist/host/plugin-tools.js +359 -0
- package/dist/host/plugin-uninstall.js +257 -0
- package/dist/host/plugins-cmd.js +302 -0
- package/dist/host/plugins-command.js +130 -0
- package/dist/host/plugins-config.js +195 -0
- package/dist/host/prompt-sections.js +133 -0
- package/dist/host/run-entry.js +544 -0
- package/dist/host/runtime.js +209 -0
- package/dist/host/scheduler-tick.js +294 -0
- package/dist/host/serve-daemon.js +501 -0
- package/dist/host/serve-entry.js +279 -0
- package/dist/host/session-anchor.js +41 -0
- package/dist/host/sessions-cmd.js +219 -0
- package/dist/host/sessions-face.js +47 -0
- package/dist/host/settings-store.js +112 -0
- package/dist/host/signals.js +69 -0
- package/dist/host/single-instance.js +105 -0
- package/dist/host/subagent-factory.js +291 -0
- package/dist/host/testkit/harness.js +283 -0
- package/dist/host/testkit/index.js +14 -0
- package/dist/host/testkit/matrix.js +302 -0
- package/dist/host/tool-policy-store.js +177 -0
- package/dist/host/triggers.js +212 -0
- package/dist/host/tui-entry.js +216 -0
- package/dist/host/webui-bridge.js +165 -0
- package/dist/issue/codes.js +38 -0
- package/dist/issue/filter.js +146 -0
- package/dist/issue/github.js +116 -0
- package/dist/issue/index.js +17 -0
- package/dist/issue/mount.js +70 -0
- package/dist/issue/poll.js +58 -0
- package/dist/issue/service.js +341 -0
- package/dist/issue/tools.js +70 -0
- package/dist/issue/types.js +34 -0
- package/dist/issue/webhook.js +126 -0
- package/dist/llm/codes.js +77 -0
- package/dist/llm/complete.js +196 -0
- package/dist/llm/events.js +15 -0
- package/dist/llm/index.js +24 -0
- package/dist/llm/inflight.js +65 -0
- package/dist/llm/model-id.js +58 -0
- package/dist/llm/provider-face.d.ts +32 -0
- package/dist/llm/provider-face.js +34 -0
- package/dist/llm/recovery.js +146 -0
- package/dist/llm/runtime.js +46 -0
- package/dist/llm/stream-fn.js +147 -0
- package/dist/lsp/codes.js +31 -0
- package/dist/lsp/connection.js +147 -0
- package/dist/lsp/frame.js +110 -0
- package/dist/lsp/index.js +16 -0
- package/dist/lsp/inject.js +58 -0
- package/dist/lsp/instance.js +264 -0
- package/dist/lsp/service.js +522 -0
- package/dist/lsp/tools.js +53 -0
- package/dist/lsp/types.js +201 -0
- package/dist/mcp/bridge.js +222 -0
- package/dist/mcp/codes.js +21 -0
- package/dist/mcp/index.js +19 -0
- package/dist/mcp/jsonrpc.js +230 -0
- package/dist/mcp/service.js +125 -0
- package/dist/mcp/tools.js +145 -0
- package/dist/mcp/types.js +108 -0
- package/dist/memory/cite.js +131 -0
- package/dist/memory/codes.js +50 -0
- package/dist/memory/command.js +71 -0
- package/dist/memory/consolidate.js +219 -0
- package/dist/memory/cycle.js +109 -0
- package/dist/memory/dao.js +878 -0
- package/dist/memory/diff.js +238 -0
- package/dist/memory/extract.js +181 -0
- package/dist/memory/fts.js +58 -0
- package/dist/memory/index.js +42 -0
- package/dist/memory/inject.js +354 -0
- package/dist/memory/merge.js +178 -0
- package/dist/memory/migration.js +132 -0
- package/dist/memory/pollution.js +59 -0
- package/dist/memory/port.js +385 -0
- package/dist/memory/review.js +201 -0
- package/dist/memory/scan.js +89 -0
- package/dist/memory/tools.js +508 -0
- package/dist/memory/types.js +140 -0
- package/dist/obs/codes.js +38 -0
- package/dist/obs/db.js +115 -0
- package/dist/obs/index.js +20 -0
- package/dist/obs/rollup.js +61 -0
- package/dist/obs/service.js +363 -0
- package/dist/obs/session-tools.js +282 -0
- package/dist/obs/session-view.js +225 -0
- package/dist/obs/tool.js +97 -0
- package/dist/obs/types.js +1 -0
- package/dist/persist/audit.js +63 -0
- package/dist/persist/aux.js +39 -0
- package/dist/persist/codes.js +35 -0
- package/dist/persist/index.js +30 -0
- package/dist/persist/load-history.js +50 -0
- package/dist/persist/migrations.js +37 -0
- package/dist/persist/paths.js +108 -0
- package/dist/persist/persistence.js +229 -0
- package/dist/persist/schema.js +99 -0
- package/dist/persist/secret-box.js +95 -0
- package/dist/persist/store.js +706 -0
- package/dist/persist/write-behind.js +238 -0
- package/dist/safety/approval.js +171 -0
- package/dist/safety/bwrap.js +123 -0
- package/dist/safety/codes.js +57 -0
- package/dist/safety/danger.js +505 -0
- package/dist/safety/gate.js +226 -0
- package/dist/safety/index.js +24 -0
- package/dist/safety/presets.js +73 -0
- package/dist/safety/roots.js +139 -0
- package/dist/safety/sandbox.js +214 -0
- package/dist/safety/seatbelt.js +88 -0
- package/dist/safety/sensitive.js +34 -0
- package/dist/safety/tool-policy.js +203 -0
- package/dist/safety/types.js +13 -0
- package/dist/scheduler/codes.js +54 -0
- package/dist/scheduler/cron-backend.js +135 -0
- package/dist/scheduler/engine.js +280 -0
- package/dist/scheduler/gates.js +60 -0
- package/dist/scheduler/index.js +25 -0
- package/dist/scheduler/migration.js +22 -0
- package/dist/scheduler/runner.js +171 -0
- package/dist/scheduler/schedule.js +193 -0
- package/dist/scheduler/service.js +283 -0
- package/dist/scheduler/tick.js +139 -0
- package/dist/scheduler/types.js +1 -0
- package/dist/sdk/http.js +865 -0
- package/dist/sdk/index.js +22 -0
- package/dist/sdk/mcp.js +289 -0
- package/dist/sdk/plugin-route-registry.js +126 -0
- package/dist/sdk/plugin-routes.js +149 -0
- package/dist/sdk/security.js +116 -0
- package/dist/sdk/types.js +32 -0
- package/dist/session/budget.js +159 -0
- package/dist/session/codes.js +40 -0
- package/dist/session/derive.js +180 -0
- package/dist/session/event-data.js +1 -0
- package/dist/session/fork.js +72 -0
- package/dist/session/import-gates.js +148 -0
- package/dist/session/index.js +18 -0
- package/dist/session/recover.js +103 -0
- package/dist/session/session.js +279 -0
- package/dist/session/snapshot.js +107 -0
- package/dist/skills/agents.js +242 -0
- package/dist/skills/codes.js +51 -0
- package/dist/skills/discovery.js +266 -0
- package/dist/skills/frontmatter.js +215 -0
- package/dist/skills/index.js +28 -0
- package/dist/skills/load.js +118 -0
- package/dist/skills/manage.js +201 -0
- package/dist/skills/registry.js +138 -0
- package/dist/skills/render.js +124 -0
- package/dist/skills/sections.js +171 -0
- package/dist/skills/types.js +21 -0
- package/dist/subagent/codes.js +50 -0
- package/dist/subagent/declarative.js +55 -0
- package/dist/subagent/index.js +19 -0
- package/dist/subagent/notify.js +51 -0
- package/dist/subagent/provide.js +7 -0
- package/dist/subagent/registry.js +149 -0
- package/dist/subagent/service.js +338 -0
- package/dist/subagent/surface.js +44 -0
- package/dist/subagent/tool.js +153 -0
- package/dist/subagent/types.js +25 -0
- package/dist/tools/apply-patch.js +164 -0
- package/dist/tools/codes.js +109 -0
- package/dist/tools/fs.js +501 -0
- package/dist/tools/index.js +17 -0
- package/dist/tools/observed.js +99 -0
- package/dist/tools/pipeline.js +176 -0
- package/dist/tools/protected-read.js +54 -0
- package/dist/tools/registry.js +250 -0
- package/dist/tools/search.js +451 -0
- package/dist/tools/worktree.js +327 -0
- package/dist/web/codes.js +37 -0
- package/dist/web/gate.js +43 -0
- package/dist/web/hygiene.js +195 -0
- package/dist/web/index.js +16 -0
- package/dist/web/service.js +207 -0
- package/dist/web/ssrf-guard.js +44 -0
- package/dist/web/tool.js +76 -0
- package/dist/web/types.js +10 -0
- package/dist/webui/assets/index-B-t2O8gh.js +22 -0
- package/dist/webui/assets/index-DOlgSZ9Q.css +2 -0
- package/dist/webui/index.html +14 -0
- package/dist/webui/index.js +14 -0
- package/dist/webui/server.js +549 -0
- package/dist/webui/types.js +38 -0
- package/examples/README.md +26 -0
- package/examples/minimal-code-plugin/entry.js +43 -0
- package/examples/minimal-code-plugin/package.json +12 -0
- package/examples/pure-skill-pack/package.json +14 -0
- package/examples/pure-skill-pack/skills/markdown-table/SKILL.md +32 -0
- package/package.json +86 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* checkpoint 回退执行(05 §5.3 批 15d——/rewind 两段事务的程序面真身)。
|
|
3
|
+
*
|
|
4
|
+
* preview(段一,零改动承诺):只读对账——当前 walk 域哈希清单 vs manifest
|
|
5
|
+
* 文件表,报恢复 N(内容异或缺席)/ 删除 M(manifest 外新文件)/ 不动 U
|
|
6
|
+
* (哈希同)。纯读面:不写不删不 fork。
|
|
7
|
+
*
|
|
8
|
+
* restore(段二,三步序):
|
|
9
|
+
* ① pre-rewind 保底快照先行——把「即将被回退覆盖的现状」先拍下来
|
|
10
|
+
* (rewind 自身可回退——痕迹可清算);
|
|
11
|
+
* ② 文件恢复——逐 manifest 条目 tmp+rename 原子写(哈希同则不写——
|
|
12
|
+
* 保 mtime;缺席父目录 mkdir recursive)+ walk 域内真恢复(manifest
|
|
13
|
+
* 外文件删除 + 空目录自底向上清剪、根不删)。中途崩溃重跑同
|
|
14
|
+
* manifest 幂等收敛(v1 诚实边界:无整事务原子性,靠逐文件原子 +
|
|
15
|
+
* 重跑收敛);
|
|
16
|
+
* ③ fork(upToSeq = manifest.boundarySeq)——旧史保留,新会话净边界起跑
|
|
17
|
+
* (05 §5.0 净边界纪律:不破「不在进行中 turn 中间切」)。fork 被
|
|
18
|
+
* veto 时文件已恢复(序是保底→恢复→fork 的规范序)——回执诚实报
|
|
19
|
+
* vetoReason,痕迹链完整。
|
|
20
|
+
*
|
|
21
|
+
* 错误分码:manifest 缺席 = NOT_FOUND;仓坏形 = STORE_CORRUPT(blob 读
|
|
22
|
+
* 侧 fail-loud 直通透传);恢复执行 IO 失败 = RESTORE_FAILED(保底快照
|
|
23
|
+
* 在场,重跑收敛)。
|
|
24
|
+
*/
|
|
25
|
+
import { mkdir, readdir, rename, rm, rmdir, writeFile } from 'node:fs/promises';
|
|
26
|
+
import { dirname, join } from 'node:path';
|
|
27
|
+
import { BaseError } from '../contracts/index.js';
|
|
28
|
+
import { createCapture } from './capture.js';
|
|
29
|
+
import { contentHash } from './store.js';
|
|
30
|
+
import { readWorkspaceFile, walkWorkspaceFiles } from './walk.js';
|
|
31
|
+
/** 恢复执行 IO 失败折 RESTORE_FAILED(保底快照在场——重跑同 manifest 收敛) */
|
|
32
|
+
function restoreFailed(detail) {
|
|
33
|
+
return new BaseError('CHECKPOINT_RESTORE_FAILED', `[CHECKPOINT_RESTORE_FAILED] 恢复执行失败:${detail}`);
|
|
34
|
+
}
|
|
35
|
+
/** 当前 walk 域哈希清单(preview/restore 共用对账基线——单遍读+哈希) */
|
|
36
|
+
async function currentHashes(root) {
|
|
37
|
+
const walked = await walkWorkspaceFiles(root);
|
|
38
|
+
const out = new Map();
|
|
39
|
+
for (const file of walked) {
|
|
40
|
+
out.set(file.path, contentHash(await readWorkspaceFile(file.absPath)));
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
/** 逐文件 tmp+rename 原子写(与 store.atomicWrite 同 idiom——恢复面独立成文) */
|
|
45
|
+
async function atomicWriteFile(absPath, content) {
|
|
46
|
+
try {
|
|
47
|
+
await mkdir(dirname(absPath), { recursive: true });
|
|
48
|
+
const tmp = `${absPath}.rewind-tmp-${process.pid}-${Math.random().toString(36).slice(2, 10)}`;
|
|
49
|
+
await writeFile(tmp, content);
|
|
50
|
+
await rename(tmp, absPath);
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
throw restoreFailed(`写 ${absPath}(${err instanceof Error ? err.message : String(err)})`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** 空目录自底向上清剪(删除步的后置——根自身不删;非空/竞态吞错继续) */
|
|
57
|
+
async function pruneEmptyDirs(root, dir) {
|
|
58
|
+
let entries;
|
|
59
|
+
try {
|
|
60
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
// 先清子目录再判本层(只含空目录的目录也该清);符号链目录 Dirent 谓词
|
|
66
|
+
// 恒 false 不入——不 rmdir 间接层
|
|
67
|
+
for (const entry of [...entries].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))) {
|
|
68
|
+
if (!entry.isDirectory())
|
|
69
|
+
continue;
|
|
70
|
+
await pruneEmptyDirs(root, join(dir, entry.name));
|
|
71
|
+
}
|
|
72
|
+
if (dir === root)
|
|
73
|
+
return; // 根不删(删根 = 删工作区本身)
|
|
74
|
+
try {
|
|
75
|
+
await rmdir(dir);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// 非空(gitignore 域内文件等)或竞态——保留,重跑幂等
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** 段一:preview(只读对账——恢复 N / 删除 M / 不动 U;不写不删不 fork) */
|
|
82
|
+
export async function previewRewind(store, id) {
|
|
83
|
+
const manifest = await store.loadManifest(id); // NOT_FOUND / STORE_CORRUPT 直通
|
|
84
|
+
const current = await currentHashes(manifest.workspaceRoot);
|
|
85
|
+
const manifestMap = new Map(manifest.files.map((f) => [f.path, f.hash]));
|
|
86
|
+
let restoreCount = 0;
|
|
87
|
+
let deleteCount = 0;
|
|
88
|
+
let untouchedCount = 0;
|
|
89
|
+
for (const [path, hash] of current) {
|
|
90
|
+
const want = manifestMap.get(path);
|
|
91
|
+
if (want === undefined)
|
|
92
|
+
deleteCount += 1;
|
|
93
|
+
else if (want === hash)
|
|
94
|
+
untouchedCount += 1;
|
|
95
|
+
else
|
|
96
|
+
restoreCount += 1;
|
|
97
|
+
}
|
|
98
|
+
// manifest 有而当前缺席(删过/现为 gitignore 域外)——同入恢复账
|
|
99
|
+
for (const f of manifest.files) {
|
|
100
|
+
if (!current.has(f.path))
|
|
101
|
+
restoreCount += 1;
|
|
102
|
+
}
|
|
103
|
+
return { manifest, restoreCount, deleteCount, untouchedCount };
|
|
104
|
+
}
|
|
105
|
+
/** 段二:restore 三步序(保底快照 → 文件恢复 → fork) */
|
|
106
|
+
export async function restoreRewind(deps, id) {
|
|
107
|
+
const manifest = await deps.store.loadManifest(id); // NOT_FOUND / STORE_CORRUPT 直通
|
|
108
|
+
/* ---- ① pre-rewind 保底快照(rewind 自身可回退——痕迹可清算) ---- */
|
|
109
|
+
const snapshotOwner = deps.invokingSessionId ?? manifest.sessionId;
|
|
110
|
+
const boundary = (deps.session !== undefined && deps.invokingSessionId !== undefined
|
|
111
|
+
? deps.session.contextOf(deps.invokingSessionId)?.lastClosedBoundary
|
|
112
|
+
: undefined) ?? -1;
|
|
113
|
+
const capture = createCapture(deps.store, { now: deps.now, newId: deps.newId });
|
|
114
|
+
const preRewind = await capture({
|
|
115
|
+
sessionId: snapshotOwner,
|
|
116
|
+
boundarySeq: boundary,
|
|
117
|
+
workspaceRoot: manifest.workspaceRoot,
|
|
118
|
+
trigger: 'pre-rewind',
|
|
119
|
+
});
|
|
120
|
+
/* ---- ② 文件恢复(walk 域内真恢复:改/补 manifest 条目 + 删域外文件 + 清空目录) ---- */
|
|
121
|
+
const current = await currentHashes(manifest.workspaceRoot);
|
|
122
|
+
const manifestMap = new Map(manifest.files.map((f) => [f.path, f.hash]));
|
|
123
|
+
let restoredCount = 0;
|
|
124
|
+
let deletedCount = 0;
|
|
125
|
+
let untouchedCount = 0;
|
|
126
|
+
// ②a 逐 manifest 条目:哈希同不动(保 mtime);异/缺席读 blob 原子写回
|
|
127
|
+
for (const entry of manifest.files) {
|
|
128
|
+
if (current.get(entry.path) === entry.hash) {
|
|
129
|
+
untouchedCount += 1;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const content = await deps.store.readBlob(entry.hash); // STORE_CORRUPT fail-loud 直通
|
|
133
|
+
await atomicWriteFile(join(manifest.workspaceRoot, ...entry.path.split('/')), content);
|
|
134
|
+
restoredCount += 1;
|
|
135
|
+
}
|
|
136
|
+
// ②b walk 域内 manifest 外文件删除(真恢复——不留快照后新增物)
|
|
137
|
+
for (const path of current.keys()) {
|
|
138
|
+
if (manifestMap.has(path))
|
|
139
|
+
continue;
|
|
140
|
+
try {
|
|
141
|
+
await rm(join(manifest.workspaceRoot, ...path.split('/')), { force: true });
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
throw restoreFailed(`删 ${path}(${err instanceof Error ? err.message : String(err)})`);
|
|
145
|
+
}
|
|
146
|
+
deletedCount += 1;
|
|
147
|
+
}
|
|
148
|
+
// ②c 空目录自底向上清剪(删除步后置;根不删)
|
|
149
|
+
await pruneEmptyDirs(manifest.workspaceRoot, manifest.workspaceRoot);
|
|
150
|
+
/* ---- ③ fork(旧史保留——新会话净边界起跑;veto 不回滚文件恢复) ---- */
|
|
151
|
+
const outcome = await deps.fork.fork(manifest.sessionId, {
|
|
152
|
+
upToSeq: manifest.boundarySeq,
|
|
153
|
+
title: `rewind:${manifest.id}`,
|
|
154
|
+
});
|
|
155
|
+
return outcome.status === 'forked'
|
|
156
|
+
? {
|
|
157
|
+
id: manifest.id,
|
|
158
|
+
preRewindId: preRewind.id,
|
|
159
|
+
restoredCount,
|
|
160
|
+
deletedCount,
|
|
161
|
+
untouchedCount,
|
|
162
|
+
forkedSessionId: outcome.sessionId,
|
|
163
|
+
}
|
|
164
|
+
: {
|
|
165
|
+
id: manifest.id,
|
|
166
|
+
preRewindId: preRewind.id,
|
|
167
|
+
restoredCount,
|
|
168
|
+
deletedCount,
|
|
169
|
+
untouchedCount,
|
|
170
|
+
vetoReason: outcome.reason,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* checkpoint 快照仓(05 §5.3 批 15d——纯文件域存储面,零 SQLite 表不占迁移号)。
|
|
3
|
+
*
|
|
4
|
+
* 布局:`<dataDir>/data/checkpoint/` 下——
|
|
5
|
+
* blobs/<hash 前二>/<sha256> 内容寻址 blob(跨 workspace 共享天然去重;
|
|
6
|
+
* tmp+rename 原子写;写前查在场跳过——同内容
|
|
7
|
+
* 零重写)
|
|
8
|
+
* manifests/<id>.json manifest 目录面(files 即恢复清单)
|
|
9
|
+
*
|
|
10
|
+
* 坏形两分(02 §5.3 批 15d 四码语义):id 缺席/坏形 = CHECKPOINT_NOT_FOUND
|
|
11
|
+
* (/rewind 作用目标守卫);manifest JSON 坏形、blob 缺席或哈希不符 =
|
|
12
|
+
* CHECKPOINT_STORE_CORRUPT(fail-loud 宁拒不误读)。id/hash 是路径段——
|
|
13
|
+
* 白名单字符校验防路径注入(/rewind 的 id 来自用户输入面)。
|
|
14
|
+
*
|
|
15
|
+
* 裁剪(每次捕获后由 capture 侧调 prune):per workspace 保最新
|
|
16
|
+
* CHECKPOINT_RETENTION_PER_WORKSPACE 份 manifest(trigger 两形同计,capturedAt
|
|
17
|
+
* 降序、同刻 id 决胜);随后 blob 引用计数 GC——扫全仓在册 manifest 收集引用
|
|
18
|
+
* 哈希(跨 workspace 共享不误杀),无引用才删。
|
|
19
|
+
*/
|
|
20
|
+
import { createHash } from 'node:crypto';
|
|
21
|
+
import { mkdir, readdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
22
|
+
import { join } from 'node:path';
|
|
23
|
+
import { BaseError } from '../contracts/index.js';
|
|
24
|
+
import { CHECKPOINT_RETENTION_PER_WORKSPACE } from './types.js';
|
|
25
|
+
/** manifest id 合法形(自产 uuid 子集;用户输入面的路径段白名单) */
|
|
26
|
+
const MANIFEST_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
|
|
27
|
+
/** blob 哈希合法形(sha256 小写 hex 64 位) */
|
|
28
|
+
const BLOB_HASH_RE = /^[0-9a-f]{64}$/;
|
|
29
|
+
/** 内容哈希(sha256 hex——内容寻址与一致性校验的单源) */
|
|
30
|
+
export function contentHash(content) {
|
|
31
|
+
return createHash('sha256').update(content).digest('hex');
|
|
32
|
+
}
|
|
33
|
+
/** 快照仓相对布局(blo 前二分桶 = 目录爆炸防线) */
|
|
34
|
+
function blobPath(baseDir, hash) {
|
|
35
|
+
return join(baseDir, 'blobs', hash.slice(0, 2), hash);
|
|
36
|
+
}
|
|
37
|
+
/** manifest 坏形折 STORE_CORRUPT(宁拒不误读——字段缺失/型不符全收) */
|
|
38
|
+
function corrupt(detail) {
|
|
39
|
+
return new BaseError('CHECKPOINT_STORE_CORRUPT', `[CHECKPOINT_STORE_CORRUPT] 快照仓坏形:${detail}`);
|
|
40
|
+
}
|
|
41
|
+
/** manifest JSON 形校验(loadManifest/listManifests 共用判据) */
|
|
42
|
+
function validateManifest(raw, source) {
|
|
43
|
+
if (typeof raw !== 'object' || raw === null)
|
|
44
|
+
throw corrupt(`${source} 非 JSON 对象`);
|
|
45
|
+
const m = raw;
|
|
46
|
+
const id = m.id;
|
|
47
|
+
const sessionId = m.sessionId;
|
|
48
|
+
const boundarySeq = m.boundarySeq;
|
|
49
|
+
const workspaceRoot = m.workspaceRoot;
|
|
50
|
+
const capturedAt = m.capturedAt;
|
|
51
|
+
const trigger = m.trigger;
|
|
52
|
+
const files = m.files;
|
|
53
|
+
if (typeof id !== 'string' || !MANIFEST_ID_RE.test(id))
|
|
54
|
+
throw corrupt(`${source} id 坏形`);
|
|
55
|
+
if (typeof sessionId !== 'string' || sessionId === '')
|
|
56
|
+
throw corrupt(`${source} sessionId 坏形`);
|
|
57
|
+
if (typeof boundarySeq !== 'number' || !Number.isInteger(boundarySeq) || boundarySeq < -1)
|
|
58
|
+
throw corrupt(`${source} boundarySeq 坏形`);
|
|
59
|
+
if (typeof workspaceRoot !== 'string' || workspaceRoot === '')
|
|
60
|
+
throw corrupt(`${source} workspaceRoot 坏形`);
|
|
61
|
+
if (typeof capturedAt !== 'number' || !Number.isFinite(capturedAt))
|
|
62
|
+
throw corrupt(`${source} capturedAt 坏形`);
|
|
63
|
+
if (trigger !== 'mutation' && trigger !== 'pre-rewind')
|
|
64
|
+
throw corrupt(`${source} trigger 坏形`);
|
|
65
|
+
if (!Array.isArray(files))
|
|
66
|
+
throw corrupt(`${source} files 非数组`);
|
|
67
|
+
const entries = files.map((f, i) => {
|
|
68
|
+
if (typeof f !== 'object' || f === null)
|
|
69
|
+
throw corrupt(`${source} files[${i}] 非对象`);
|
|
70
|
+
const e = f;
|
|
71
|
+
if (typeof e.path !== 'string' || e.path === '')
|
|
72
|
+
throw corrupt(`${source} files[${i}].path 坏形`);
|
|
73
|
+
if (typeof e.hash !== 'string' || !BLOB_HASH_RE.test(e.hash))
|
|
74
|
+
throw corrupt(`${source} files[${i}].hash 坏形`);
|
|
75
|
+
if (typeof e.bytes !== 'number' || !Number.isInteger(e.bytes) || e.bytes < 0)
|
|
76
|
+
throw corrupt(`${source} files[${i}].bytes 坏形`);
|
|
77
|
+
return { path: e.path, hash: e.hash, bytes: e.bytes };
|
|
78
|
+
});
|
|
79
|
+
return {
|
|
80
|
+
id,
|
|
81
|
+
sessionId,
|
|
82
|
+
boundarySeq,
|
|
83
|
+
workspaceRoot,
|
|
84
|
+
capturedAt,
|
|
85
|
+
trigger: trigger,
|
|
86
|
+
files: entries,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/** tmp+rename 原子写(tmp 落同目录保 rename 同卷;写后即终形) */
|
|
90
|
+
async function atomicWrite(absPath, data) {
|
|
91
|
+
await mkdir(join(absPath, '..'), { recursive: true });
|
|
92
|
+
const tmp = `${absPath}.tmp-${process.pid}-${Math.random().toString(36).slice(2, 10)}`;
|
|
93
|
+
await writeFile(tmp, data);
|
|
94
|
+
await rename(tmp, absPath);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* 开快照仓(装配根/命令面一次性开——baseDir 惰性建:首次写 blob/manifest 时
|
|
98
|
+
* mkdir recursive,空仓读面按空集处理不诊断)。
|
|
99
|
+
*/
|
|
100
|
+
export function openCheckpointStore(dataDir) {
|
|
101
|
+
const baseDir = join(dataDir, 'data', 'checkpoint');
|
|
102
|
+
return {
|
|
103
|
+
baseDir,
|
|
104
|
+
async writeBlob(hash, content) {
|
|
105
|
+
if (!BLOB_HASH_RE.test(hash))
|
|
106
|
+
throw corrupt(`blob 哈希坏形:${hash}`);
|
|
107
|
+
const target = blobPath(baseDir, hash);
|
|
108
|
+
try {
|
|
109
|
+
await readFile(target);
|
|
110
|
+
return; // 在场即跳过——内容寻址共享,同内容零重写
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// 缺席——落盘(走原子写)
|
|
114
|
+
}
|
|
115
|
+
await atomicWrite(target, content);
|
|
116
|
+
},
|
|
117
|
+
async readBlob(hash) {
|
|
118
|
+
if (!BLOB_HASH_RE.test(hash))
|
|
119
|
+
throw corrupt(`blob 哈希坏形:${hash}`);
|
|
120
|
+
let content;
|
|
121
|
+
try {
|
|
122
|
+
content = await readFile(blobPath(baseDir, hash));
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
throw corrupt(`blob 缺席:${hash}`);
|
|
126
|
+
}
|
|
127
|
+
if (contentHash(content) !== hash)
|
|
128
|
+
throw corrupt(`blob 哈希不符:${hash}`);
|
|
129
|
+
return content;
|
|
130
|
+
},
|
|
131
|
+
async saveManifest(manifest) {
|
|
132
|
+
// id 白名单校验(防路径注入——manifest id 亦是路径段)
|
|
133
|
+
if (!MANIFEST_ID_RE.test(manifest.id))
|
|
134
|
+
throw corrupt(`manifest id 坏形:${manifest.id}`);
|
|
135
|
+
await atomicWrite(join(baseDir, 'manifests', `${manifest.id}.json`), JSON.stringify(manifest, null, 2));
|
|
136
|
+
},
|
|
137
|
+
async loadManifest(id) {
|
|
138
|
+
if (!MANIFEST_ID_RE.test(id))
|
|
139
|
+
throw notFound(id);
|
|
140
|
+
let text;
|
|
141
|
+
try {
|
|
142
|
+
text = await readFile(join(baseDir, 'manifests', `${id}.json`), 'utf8');
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
throw notFound(id);
|
|
146
|
+
}
|
|
147
|
+
let raw;
|
|
148
|
+
try {
|
|
149
|
+
raw = JSON.parse(text);
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
throw corrupt(`manifest ${id} JSON 解析失败(${err instanceof Error ? err.message : String(err)})`);
|
|
153
|
+
}
|
|
154
|
+
return validateManifest(raw, `manifest ${id}`);
|
|
155
|
+
},
|
|
156
|
+
async listManifests() {
|
|
157
|
+
let names;
|
|
158
|
+
try {
|
|
159
|
+
names = await readdir(join(baseDir, 'manifests'));
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return []; // 空仓/仓未建——空集不诊断
|
|
163
|
+
}
|
|
164
|
+
const loaded = [];
|
|
165
|
+
for (const name of names) {
|
|
166
|
+
if (!name.endsWith('.json'))
|
|
167
|
+
continue;
|
|
168
|
+
loaded.push(await this.loadManifest(name.slice(0, -5)));
|
|
169
|
+
}
|
|
170
|
+
// capturedAt 降序(同刻 id 决胜——确定性序;最新在前)
|
|
171
|
+
loaded.sort((a, b) => b.capturedAt - a.capturedAt || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
172
|
+
return loaded;
|
|
173
|
+
},
|
|
174
|
+
async deleteManifest(id) {
|
|
175
|
+
if (!MANIFEST_ID_RE.test(id))
|
|
176
|
+
return; // 坏形 id 无文件可删——幂等 no-op
|
|
177
|
+
await rm(join(baseDir, 'manifests', `${id}.json`), { force: true });
|
|
178
|
+
},
|
|
179
|
+
async listBlobHashes() {
|
|
180
|
+
const hashes = [];
|
|
181
|
+
let prefixDirs;
|
|
182
|
+
try {
|
|
183
|
+
prefixDirs = await readdir(join(baseDir, 'blobs'));
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return []; // 空仓——空集
|
|
187
|
+
}
|
|
188
|
+
for (const prefix of prefixDirs.sort()) {
|
|
189
|
+
let names = [];
|
|
190
|
+
try {
|
|
191
|
+
names = await readdir(join(baseDir, 'blobs', prefix));
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
continue; // 竞态消失——跳过
|
|
195
|
+
}
|
|
196
|
+
for (const name of names.sort()) {
|
|
197
|
+
if (BLOB_HASH_RE.test(name))
|
|
198
|
+
hashes.push(name);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return hashes;
|
|
202
|
+
},
|
|
203
|
+
async deleteBlob(hash) {
|
|
204
|
+
if (!BLOB_HASH_RE.test(hash))
|
|
205
|
+
return;
|
|
206
|
+
await rm(blobPath(baseDir, hash), { force: true });
|
|
207
|
+
},
|
|
208
|
+
async prune() {
|
|
209
|
+
/* ---- 段一:per workspace 保留帽(trigger 两形同计) ---- */
|
|
210
|
+
const all = await this.listManifests();
|
|
211
|
+
const byWorkspace = new Map();
|
|
212
|
+
for (const m of all) {
|
|
213
|
+
const bucket = byWorkspace.get(m.workspaceRoot);
|
|
214
|
+
if (bucket !== undefined)
|
|
215
|
+
bucket.push(m);
|
|
216
|
+
else
|
|
217
|
+
byWorkspace.set(m.workspaceRoot, [m]);
|
|
218
|
+
}
|
|
219
|
+
let removedManifests = 0;
|
|
220
|
+
for (const [, bucket] of byWorkspace) {
|
|
221
|
+
// listManifests 已 capturedAt 降序——保前 N 删余
|
|
222
|
+
for (const stale of bucket.slice(CHECKPOINT_RETENTION_PER_WORKSPACE)) {
|
|
223
|
+
await this.deleteManifest(stale.id);
|
|
224
|
+
removedManifests += 1;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/* ---- 段二:blob 引用计数 GC(扫全仓在册 manifest——跨 workspace 共享不误杀) ---- */
|
|
228
|
+
const referenced = new Set();
|
|
229
|
+
for (const m of await this.listManifests()) {
|
|
230
|
+
for (const f of m.files)
|
|
231
|
+
referenced.add(f.hash);
|
|
232
|
+
}
|
|
233
|
+
let removedBlobs = 0;
|
|
234
|
+
for (const hash of await this.listBlobHashes()) {
|
|
235
|
+
if (referenced.has(hash))
|
|
236
|
+
continue;
|
|
237
|
+
await this.deleteBlob(hash);
|
|
238
|
+
removedBlobs += 1;
|
|
239
|
+
}
|
|
240
|
+
return { removedManifests, removedBlobs };
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
/** 回退点缺席折 NOT_FOUND(已裁剪或坏 id) */
|
|
245
|
+
function notFound(id) {
|
|
246
|
+
return new BaseError('CHECKPOINT_NOT_FOUND', `[CHECKPOINT_NOT_FOUND] 回退点不存在或已裁剪:${id}`);
|
|
247
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* checkpoint 工作区遍历(05 §5.3 批 15d——快照/回退的 walk 域定界)。
|
|
3
|
+
*
|
|
4
|
+
* 遍历语义与 skills 发现(discovery.ts)/检索族(tools/search.ts)三副本
|
|
5
|
+
* 同笔同判(04 遍历语义条款):尊重 .gitignore——逐目录读规则按所在目录
|
|
6
|
+
* 前缀化挂匹配器(根 .gitignore 前导 / 保留——锚定语义不降级);剪枝
|
|
7
|
+
* node_modules/.git;**不跟随符号链**(Dirent.isDirectory 对符号链 false
|
|
8
|
+
* ——目录链不入、文件链同样跳过:快照域不收间接层,恢复面才可精确对账)。
|
|
9
|
+
*
|
|
10
|
+
* 与技能面的遍历差异:技能面跟随符号链目录(realpath 去重防环);本面
|
|
11
|
+
* 不跟随——快照的诚实 = 只拍实文件域,间接层由所指文件自身入册。
|
|
12
|
+
* 点文件(.env 等)入册:快照承诺的是工作区状态全恢复,非 git 索引面。
|
|
13
|
+
*
|
|
14
|
+
* 实现与 skills/discovery.ts 同语义独立成文(07 §2 词面独立律——三副本
|
|
15
|
+
* 各自实现,判据漂移由回归测试锁)。
|
|
16
|
+
*/
|
|
17
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
18
|
+
import { join, relative, resolve, sep } from 'node:path';
|
|
19
|
+
import ignore from 'ignore';
|
|
20
|
+
import { BaseError } from '../contracts/index.js';
|
|
21
|
+
import { CHECKPOINT_WALK_FILE_CAP } from './types.js';
|
|
22
|
+
/** 遍历常量剪枝(与 skills/检索族同表语义:依赖目录与 git 元数据) */
|
|
23
|
+
const PRUNE_DIRS = new Set(['node_modules', '.git']);
|
|
24
|
+
/** 平台路径分隔符归一为 /(gitignore 模式语义在 posix 路径上) */
|
|
25
|
+
function toPosix(p) {
|
|
26
|
+
return p.split(sep).join('/');
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 单行 gitignore 模式前缀化(锚定判据——与 skills/检索族三副本同笔同判)。
|
|
30
|
+
* 注释/空行丢弃;`!` 否定与 `\!`/`\#` 转义保留;模式体去尾随目录标记斜杠
|
|
31
|
+
* 后含斜杠或带前导 / = 锚定本层精确匹配;纯 basename 模式前缀化插目录
|
|
32
|
+
* 通配前缀(`**`+`/`)保深层同配。根 .gitignore 前导 / 保留(git 语义)。
|
|
33
|
+
*/
|
|
34
|
+
function prefixIgnorePattern(line, prefix) {
|
|
35
|
+
const trimmed = line.trim();
|
|
36
|
+
if (!trimmed)
|
|
37
|
+
return null;
|
|
38
|
+
if (trimmed.startsWith('#') && !trimmed.startsWith('\\#'))
|
|
39
|
+
return null;
|
|
40
|
+
let pattern = line;
|
|
41
|
+
let negated = false;
|
|
42
|
+
if (pattern.startsWith('!')) {
|
|
43
|
+
negated = true;
|
|
44
|
+
pattern = pattern.slice(1);
|
|
45
|
+
}
|
|
46
|
+
else if (pattern.startsWith('\\!')) {
|
|
47
|
+
pattern = pattern.slice(1);
|
|
48
|
+
}
|
|
49
|
+
const rooted = pattern.startsWith('/');
|
|
50
|
+
if (rooted)
|
|
51
|
+
pattern = pattern.slice(1);
|
|
52
|
+
if (!prefix) {
|
|
53
|
+
const passed = rooted ? `/${pattern}` : pattern;
|
|
54
|
+
return negated ? `!${passed}` : passed;
|
|
55
|
+
}
|
|
56
|
+
const body = pattern.endsWith('/') ? pattern.slice(0, -1) : pattern;
|
|
57
|
+
const anchored = rooted || body.includes('/');
|
|
58
|
+
const prefixed = anchored ? `${prefix}${pattern}` : `${prefix}**/${pattern}`;
|
|
59
|
+
return negated ? `!${prefixed}` : prefixed;
|
|
60
|
+
}
|
|
61
|
+
/** 读 dir 下 .gitignore 并按所在目录前缀化挂上匹配器(嵌套规则只作用本子树) */
|
|
62
|
+
async function addIgnoreRules(matcher, dir, rootDir) {
|
|
63
|
+
let content;
|
|
64
|
+
try {
|
|
65
|
+
content = await readFile(join(dir, '.gitignore'), 'utf8');
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return; // 无 .gitignore 或读失败——本目录无额外规则
|
|
69
|
+
}
|
|
70
|
+
const prefix = toPosix(relative(rootDir, dir));
|
|
71
|
+
const prefixed = prefix ? `${prefix}/` : '';
|
|
72
|
+
const patterns = content
|
|
73
|
+
.split(/\r?\n/)
|
|
74
|
+
.map((line) => prefixIgnorePattern(line, prefixed))
|
|
75
|
+
.filter((line) => line !== null);
|
|
76
|
+
if (patterns.length > 0)
|
|
77
|
+
matcher.add(patterns);
|
|
78
|
+
}
|
|
79
|
+
/** 路径 gitignore 判定(目录双测带/不带尾斜杠——兼容 `dir/` 与 `dir` 两种写法) */
|
|
80
|
+
function ignoredPath(matcher, root, fullPath, isDir = false) {
|
|
81
|
+
const rel = toPosix(relative(root, fullPath));
|
|
82
|
+
if (rel === '')
|
|
83
|
+
return false; // 根自身不判
|
|
84
|
+
return isDir ? matcher.ignores(rel) || matcher.ignores(`${rel}/`) : matcher.ignores(rel);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* 枚举工作区 walk 域内全部实文件(确定性序:目录序 × 名称字典序——manifest
|
|
88
|
+
* files 的稳定形态)。读文件内容与哈希不在本面(capture/preview 各自单遍
|
|
89
|
+
* 读+哈希——避免「walk 读一遍、写 blob 再读一遍」的双读)。
|
|
90
|
+
*
|
|
91
|
+
* 非常规文件(FIFO/socket/device)静默跳过——快照域只收常规实文件;符号
|
|
92
|
+
* 链(目录/文件两形)跳过。常规文件读失败(权限/竞态)抛
|
|
93
|
+
* CHECKPOINT_CAPTURE_FAILED——fail-closed:读不了的文件进不了快照,
|
|
94
|
+
* 放行变异即伪承诺。文件数超帽(缺省 CHECKPOINT_WALK_FILE_CAP)同码
|
|
95
|
+
* fail-closed(防误指巨型目录炸快照)。
|
|
96
|
+
*/
|
|
97
|
+
export async function walkWorkspaceFiles(root, fileCap = CHECKPOINT_WALK_FILE_CAP) {
|
|
98
|
+
const files = [];
|
|
99
|
+
const absRoot = resolve(root);
|
|
100
|
+
await walkDir(absRoot, absRoot, ignore(), files, fileCap);
|
|
101
|
+
return files;
|
|
102
|
+
}
|
|
103
|
+
/** 实际递归(点目录入册、依赖目录剪枝、符号链不跟随、gitignore 逐目录挂规则) */
|
|
104
|
+
async function walkDir(dir, root, matcher, files, fileCap) {
|
|
105
|
+
await addIgnoreRules(matcher, dir, root);
|
|
106
|
+
let entries;
|
|
107
|
+
try {
|
|
108
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
throw new BaseError('CHECKPOINT_CAPTURE_FAILED', `[CHECKPOINT_CAPTURE_FAILED] 工作区目录读取失败:${dir}(${err instanceof Error ? err.message : String(err)})`);
|
|
112
|
+
}
|
|
113
|
+
// 名称字典序——确定性遍历序(manifest files 稳定形态的锚)
|
|
114
|
+
const sorted = [...entries].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
115
|
+
for (const entry of sorted) {
|
|
116
|
+
// 符号链不跟随(目录/文件两形同判——Dirent 谓词对符号链恒 false)
|
|
117
|
+
if (entry.isSymbolicLink())
|
|
118
|
+
continue;
|
|
119
|
+
const fullPath = join(dir, entry.name);
|
|
120
|
+
if (entry.isDirectory()) {
|
|
121
|
+
if (PRUNE_DIRS.has(entry.name))
|
|
122
|
+
continue; // 依赖目录与 git 元数据剪枝
|
|
123
|
+
if (ignoredPath(matcher, root, fullPath, true))
|
|
124
|
+
continue;
|
|
125
|
+
await walkDir(fullPath, root, matcher, files, fileCap);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (!entry.isFile())
|
|
129
|
+
continue; // FIFO/socket/device 等——非常规文件不入快照域
|
|
130
|
+
if (ignoredPath(matcher, root, fullPath))
|
|
131
|
+
continue;
|
|
132
|
+
if (files.length >= fileCap) {
|
|
133
|
+
throw new BaseError('CHECKPOINT_CAPTURE_FAILED', `[CHECKPOINT_CAPTURE_FAILED] 工作区文件数超快照帽(>${fileCap},根 ${root})——疑似误指巨型目录,fail-closed 拒拍。`);
|
|
134
|
+
}
|
|
135
|
+
files.push({ path: toPosix(relative(root, fullPath)), absPath: fullPath });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/** 读单文件字节(读失败折 CHECKPOINT_CAPTURE_FAILED——fail-closed) */
|
|
139
|
+
export async function readWorkspaceFile(absPath) {
|
|
140
|
+
try {
|
|
141
|
+
return await readFile(absPath);
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
throw new BaseError('CHECKPOINT_CAPTURE_FAILED', `[CHECKPOINT_CAPTURE_FAILED] 工作区文件读取失败:${absPath}(${err instanceof Error ? err.message : String(err)})`);
|
|
145
|
+
}
|
|
146
|
+
}
|