aura-basetools 0.13.3 → 0.14.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.
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
- # aura-basetools 0.13.3
1
+ # aura-basetools 0.14.0
2
2
 
3
- Byte-exact file tools for coding agents: read by intent, edit by intent, and write whole files without corrupting bytes. One engine shared by a standalone CLI and an MCP server.
3
+ Byte-exact file tools for coding agents: read by intent, edit by address, and write whole files without corrupting bytes. One engine shared by a standalone CLI and an MCP server.
4
4
 
5
- 面向 coding agent 的逐字节文件工具:按意图读取、按意图编辑、整文件写入;CLI 与 MCP 共用同一套引擎。
5
+ 面向 coding agent 的逐字节文件工具:按意图读取、按地址编辑、整文件写入;CLI 与 MCP 共用同一套引擎。
6
6
 
7
7
  ---
8
8
 
@@ -21,7 +21,7 @@ The engine is intentionally not a general file manager: it reads and edits UTF-8
21
21
  ### Install
22
22
 
23
23
  ```bash
24
- npm i -g aura-basetools@0.13.3
24
+ npm i -g aura-basetools@0.14.0
25
25
  aura --version
26
26
  ```
27
27
 
@@ -31,8 +31,6 @@ aura --version
31
31
  aura basetools read --file src/app.ts --mode map
32
32
  aura basetools edit --file src/app.ts --hunks '[{"target":"main","replace":"..."}]'
33
33
  aura basetools write --file generated.ts --content @body.ts
34
- aura basetools undo
35
- aura basetools transactions
36
34
  aura basetools mcp
37
35
  ```
38
36
 
@@ -77,10 +75,10 @@ The server exposes:
77
75
  | Tool | Purpose |
78
76
  |---|---|
79
77
  | `aura_basetools_read` | Intent-based views: `map`, `digest`, `full`, `around`, `lines`, `search`, `changed`, `auto` |
80
- | `aura_basetools_edit` | Anchored multi-hunk edits; `old` + `replace_all`; optional transaction |
78
+ | `aura_basetools_edit` | Anchored multi-hunk edits; `old` + `replace_all` |
81
79
  | `aura_basetools_write` | Whole-file create/overwrite with read-before-write and read-back verification |
82
- | `aura_basetools_undo` | Undo the latest or a named write transaction |
83
- | `aura_basetools_transactions` | List write transactions for the current workspace |
80
+
81
+ 0.14.0 removed the public `undo` / `transactions` tools (0 uses in 642 sessions); failure rollback remains internal and automatic.
84
82
 
85
83
  The older tool names (`aura_read`, `aura_edit`, `aura_write`, ...) are no longer exposed by MCP or DSH.
86
84
 
@@ -128,7 +126,7 @@ MIT.
128
126
  `aura-basetools` 是一个 Node.js(>=18)包,运行时零依赖,提供:
129
127
 
130
128
  - **CLI**:`aura basetools <verb>`
131
- - **MCP 服务**:stdio 上的五个工具
129
+ - **MCP 服务**:stdio 上的三个工具
132
130
  - **不依赖 DSH**:DeepSeek Harness 适配器是独立的可选包 `aura-basetools-dsh`
133
131
 
134
132
  它不是通用文件管理器:只处理 UTF-8 文本,保留字节级格式;如果被寻址区域在“授权读取”之后被改动,编辑会被拒绝。
@@ -136,7 +134,7 @@ MIT.
136
134
  ### 安装
137
135
 
138
136
  ```bash
139
- npm i -g aura-basetools@0.13.3
137
+ npm i -g aura-basetools@0.14.0
140
138
  aura --version
141
139
  ```
142
140
 
@@ -146,8 +144,6 @@ aura --version
146
144
  aura basetools read --file src/app.ts --mode map
147
145
  aura basetools edit --file src/app.ts --hunks '[{"target":"main","replace":"..."}]'
148
146
  aura basetools write --file generated.ts --content @body.ts
149
- aura basetools undo
150
- aura basetools transactions
151
147
  aura basetools mcp
152
148
  ```
153
149
 
@@ -192,10 +188,10 @@ npx -y --package aura-basetools aura basetools mcp
192
188
  | 工具 | 作用 |
193
189
  |---|---|
194
190
  | `aura_basetools_read` | `map` / `digest` / `full` / `around` / `lines` / `search` / `changed` / `auto` |
195
- | `aura_basetools_edit` | 多 hunk 锚点编辑;`old` + `replace_all`;可选事务 |
191
+ | `aura_basetools_edit` | 多 hunk 锚点编辑;`old` + `replace_all` |
196
192
  | `aura_basetools_write` | 整文件新建/覆写;覆写默认要求先读,写后回读校验 |
197
- | `aura_basetools_undo` | 撤销最近一次或指定写事务 |
198
- | `aura_basetools_transactions` | 列出当前工作区写事务 |
193
+
194
+ 0.14.0 移除了公开 `undo` / `transactions`(642 个会话中 0 次使用);失败回滚仍是内部自动行为。
199
195
 
200
196
  旧工具名(`aura_read`、`aura_edit`、`aura_write` 等)不再由 MCP/DSH 暴露。
201
197
 
package/bin/aura-mcp.mjs CHANGED
@@ -5,12 +5,10 @@
5
5
  * 为什么是它:Codex / Claude Code / Cursor / Zed 这些 agent 都吃 MCP —— 一次写壳,到处能用,
6
6
  * 且**不要求对方装 DSH**(本文件只依赖 node:fs 与本包的引擎)。
7
7
  *
8
- * 暴露五个工具:
8
+ * 暴露三个工具:
9
9
  * aura_basetools_read 七种取景(map/digest/full/around/lines/search/changed)+ 版本令牌
10
10
  * aura_basetools_edit 四层寻址 + expect + dry_run;逐字节写回 + 陈旧区域闸门 + 落盘后回读校验
11
- * aura_basetools_write 整份写入(对应内置 Write):新建或整文件替换,同走事务与回读校验
12
- * aura_basetools_undo 撤销最近一次/指定写事务(默认拒绝覆盖后续编辑)
13
- * aura_basetools_transactions 列出当前工作区写事务
11
+ * aura_basetools_write 整份写入(对应内置 Write):新建或整文件替换,同走原子写与回读校验
14
12
  *
15
13
  * 接法(写进各家的 MCP 配置即可):
16
14
  * codex: codex mcp add aura -- npx -y --package aura-basetools aura basetools mcp
@@ -21,9 +19,9 @@
21
19
  * `aura-basetools`;npx 在"多个 bin 且没有与包名同名的"时候会拒绝猜,直接报
22
20
  * `could not determine executable to run`(实测)。`--package` 明确指定包,`aura` 才是命令。
23
21
  */
24
- import { readFileSync } from 'node:fs'
22
+ import { readFileSync, realpathSync } from 'node:fs'
25
23
  import process from 'node:process'
26
- import { pathToFileURL } from 'node:url'
24
+ import { fileURLToPath, pathToFileURL } from 'node:url'
27
25
  import {
28
26
  openProvider,
29
27
  readEnvelope,
@@ -32,8 +30,6 @@ import {
32
30
  // 多目标(fs-contract-v2)的引擎绑定:与单目标同一处(`lib/ops.mjs`),bin 里不另找引擎。
33
31
  readManyWith,
34
32
  editManyEnvelope,
35
- undoEnvelope,
36
- transactionsEnvelope,
37
33
  assertReadTargets,
38
34
  assertEditTargets,
39
35
  assertTargetShape,
@@ -81,11 +77,6 @@ const HUNK_SCHEMA = {
81
77
  type: 'object',
82
78
  properties: {
83
79
  target: { type: 'string', description: '符号名,如 `toTodoList` 或 `function:apply`;跳过全部文本匹配' },
84
- intent: {
85
- type: 'string',
86
- description:
87
- '模糊意图地址:符号名片段、短语或近似原文(如 `parseConfig`、`config loader`、`// retry the upload`),由引擎确定性地落成一个精确区间;歧义时报候选并拒绝(不猜)。单独使用(不与 target/lines/line_cue/anchor/old/context/handle 同用);可用 near 与 expect',
88
- },
89
80
  lines: {
90
81
  oneOf: [
91
82
  { type: 'array' },
@@ -137,7 +128,7 @@ const HUNK_SCHEMA = {
137
128
  replace_all: {
138
129
  type: 'boolean',
139
130
  description:
140
- '与 old 同用:替换全部非重叠出现(从左到右),不再要求唯一。不能与 target/intent/lines/line_cue/anchor/context/handle/near 同用;读取后出现次数变了会整体拒绝。',
131
+ '与 old 同用:替换全部非重叠出现(从左到右),不再要求唯一。不能与 target/lines/line_cue/anchor/context/handle/near 同用;读取后出现次数变了会整体拒绝。',
141
132
  },
142
133
  eol: {
143
134
  type: 'string',
@@ -198,14 +189,13 @@ const TOOLS = [
198
189
  {
199
190
  name: 'aura_basetools_edit',
200
191
  description:
201
- 'Edit or create a UTF-8 text file with hunks in one call. Address each hunk by intent (fuzzy name/phrase), target (symbol), lines, line_cue, anchor, or old (verbatim content + optional context). With `old`, set `replace_all: true` to replace every non-overlapping occurrence. Writes back byte-for-byte (BOM and per-line endings preserved), refuses a hunk whose addressed region changed since your last read, and reads the file back to verify. All hunks must locate or nothing is written.',
192
+ 'Edit or create a UTF-8 text file with hunks in one call. Address each hunk by target (symbol), lines, line_cue, anchor, or old (verbatim content + optional context). With `old`, set `replace_all: true` to replace every non-overlapping occurrence. Writes back byte-for-byte (BOM and per-line endings preserved), refuses a hunk whose addressed region changed since your last read, and reads the file back to verify. All hunks must locate or nothing is written.',
202
193
  inputSchema: {
203
194
  type: 'object',
204
195
  properties: {
205
196
  file_path: { type: 'string' },
206
197
  hunks: { type: 'array', items: HUNK_SCHEMA },
207
198
  dry_run: { type: 'boolean', description: '只定位不落盘' },
208
- include_transaction: { type: 'boolean', description: '在回执里返回 transaction id(默认不返回,保持输出稳定)' },
209
199
  require_observation: {
210
200
  type: 'boolean',
211
201
  description: '严格新鲜度:要求这一版文件在本会话里被读过(或由本工具写过);否则 PATCH_UNOBSERVED。create 不受影响;dry_run 同样执行这条判决。',
@@ -230,7 +220,7 @@ const TOOLS = [
230
220
  {
231
221
  name: 'aura_basetools_write',
232
222
  description:
233
- 'Write a whole UTF-8 text file at once (create or overwrite), matching a plain Write tool. Preserves an existing file\'s BOM and line endings, goes through the same transaction/undo path as aura_basetools_edit, and reads the file back to verify bytes. Prefer aura_basetools_edit for partial changes.',
223
+ 'Write a whole UTF-8 text file at once (create or overwrite), matching a plain Write tool. Preserves an existing file\'s BOM and line endings, goes through the same atomic write path as aura_basetools_edit, and reads the file back to verify bytes. Prefer aura_basetools_edit for partial changes.',
234
224
  inputSchema: {
235
225
  type: 'object',
236
226
  properties: {
@@ -247,28 +237,10 @@ const TOOLS = [
247
237
  enum: ['auto', 'preserve', 'lf', 'crlf'],
248
238
  description: '覆写时的换行风格:auto(默认)适配文件已有风格;preserve 按 content 原样写;lf/crlf 强制。新建文件始终按 content 原样写(非 auto 会被拒绝)。',
249
239
  },
250
- include_transaction: { type: 'boolean', description: '在回执里返回 transaction id(默认不返回)' },
251
240
  },
252
241
  required: ['file_path', 'content'],
253
242
  },
254
243
  },
255
- {
256
- name: 'aura_basetools_undo',
257
- description:
258
- 'Undo the latest committed write transaction, or the transaction with the given id. Refuses if a file changed after the transaction; pass force=true to override. Restores the exact bytes captured before the write.',
259
- inputSchema: {
260
- type: 'object',
261
- properties: {
262
- id: { type: 'string', description: '事务 id;省略则撤销最近一条 committed 事务' },
263
- force: { type: 'boolean', description: '即使文件在事务后又被改过也强制恢复' },
264
- },
265
- },
266
- },
267
- {
268
- name: 'aura_basetools_transactions',
269
- description: 'List write transactions for the current workspace (id, status, file count, time).',
270
- inputSchema: { type: 'object', properties: {} },
271
- },
272
244
  ]
273
245
 
274
246
  /**
@@ -351,7 +323,6 @@ async function editTargetsCall(provider, args) {
351
323
  return await editManyEnvelope(provider, {
352
324
  targets,
353
325
  dryRun: args.dry_run === true,
354
- includeTransaction: args.include_transaction === true,
355
326
  ...(args.require_observation === true ? { requireObservation: true } : {}),
356
327
  })
357
328
  }
@@ -436,7 +407,6 @@ async function callTool(provider, name, args = {}) {
436
407
  hunks: Array.isArray(args.hunks) ? args.hunks : [],
437
408
  dryRun: args.dry_run === true,
438
409
  create: creating ? args.create : undefined,
439
- includeTransaction: args.include_transaction === true,
440
410
  ...(args.require_observation === true ? { requireObservation: true } : {}),
441
411
  })
442
412
  }
@@ -446,13 +416,10 @@ async function callTool(provider, name, args = {}) {
446
416
  content: args.content,
447
417
  dryRun: args.dry_run === true,
448
418
  noOverwrite: args.no_overwrite === true,
449
- includeTransaction: args.include_transaction === true,
450
419
  allowUnobserved: args.allow_unobserved === true,
451
420
  ...(typeof args.eol === 'string' ? { eol: args.eol } : {}),
452
421
  })
453
422
  }
454
- if (name === 'aura_basetools_undo') return await undoEnvelope(provider, { id: args.id, force: args.force === true })
455
- if (name === 'aura_basetools_transactions') return transactionsEnvelope(provider)
456
423
  return { ok: false, error: `unknown tool ${JSON.stringify(name)}`, code: 'UNKNOWN_TOOL' }
457
424
  }
458
425
 
@@ -561,6 +528,25 @@ export async function serve() {
561
528
 
562
529
  // 直接运行本文件(`aura-mcp`,package.json 里声明的第二个 bin)也要起服务,与 `aura basetools mcp` 等价。
563
530
  // 用 pathToFileURL 比手拼 file:// 靠得住(Windows 盘符就是那种手拼会踩的地方)。
564
- if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href) {
531
+ /**
532
+ * 直接执行本文件才起 stdio 服务。
533
+ *
534
+ * 不能只比 `import.meta.url === pathToFileURL(argv[1])`:macOS 的临时目录
535
+ * `/var/folders/...` 是 `/private/var/folders/...` 的符号链接,Node 给模块的 URL 与
536
+ * argv 的写法可能一侧已 realpath、另一侧没有 ⇒ 服务静默退出,客户端看到 0 个工具
537
+ * (post-publish / split-package 在 macOS 上正是这条)。两边都过 realpath 再比。
538
+ */
539
+ function isDirectRun() {
540
+ const invoked = process.argv[1]
541
+ if (invoked === undefined) return false
542
+ if (import.meta.url === pathToFileURL(invoked).href) return true
543
+ try {
544
+ return realpathSync.native(fileURLToPath(import.meta.url)) === realpathSync.native(invoked)
545
+ } catch {
546
+ return false
547
+ }
548
+ }
549
+
550
+ if (isDirectRun()) {
565
551
  await serve()
566
552
  }
package/bin/aura.mjs CHANGED
@@ -6,8 +6,6 @@
6
6
  * aura basetools edit --file F (--hunks '<json>' | --hunks-file P) [--dry-run] [--create <文本|@文件>]
7
7
  * aura basetools read --targets '[...]|@spec.json' 一次读多个目标(与 --file 互斥)
8
8
  * aura basetools edit --targets '[...]|@spec.json' 一次改多个文件(与 --file 互斥;任一规划失败 ⇒ 零写入)
9
- * aura basetools undo [--id <事务 id>] [--force] 撤销最近一次/指定写事务
10
- * aura basetools transactions 列出当前工作区写事务
11
9
  * aura basetools mcp
12
10
  * aura --help | --version
13
11
  *
@@ -27,8 +25,6 @@ import {
27
25
  // 多目标(fs-contract-v2)的引擎绑定:与单目标同一处(`lib/ops.mjs`),bin 里不另找引擎。
28
26
  readManyWith,
29
27
  editManyEnvelope,
30
- undoEnvelope,
31
- transactionsEnvelope,
32
28
  capabilitiesEnvelope,
33
29
  writeEnvelope,
34
30
  assertReadTargets,
@@ -74,7 +70,7 @@ let command = argv[0]
74
70
  * 有了这份名单,“下一个是已知布尔旗标”才当没带值,**其它以 `--` 开头的东西都当值**。
75
71
  * 代价是名单要维护;收益是那个静默丢值的路径从根上消失,而不是靠叫用户改写法。
76
72
  */
77
- const BOOLEAN_FLAGS = new Set(['pretty', 'dry-run', 'plan', 'transaction', 'force', 'no-overwrite', 'replace-all', 'require-observation', 'allow-unobserved', 'json'])
73
+ const BOOLEAN_FLAGS = new Set(['pretty', 'dry-run', 'plan', 'no-overwrite', 'replace-all', 'require-observation', 'allow-unobserved', 'json'])
78
74
  const isBooleanFlag = (token) => typeof token === 'string' && BOOLEAN_FLAGS.has(token.replace(/^--/, ''))
79
75
 
80
76
  /**
@@ -90,15 +86,13 @@ const COMMAND_FLAGS = {
90
86
  bool: ['pretty'],
91
87
  },
92
88
  edit: {
93
- value: ['cwd', 'file', 'targets', 'hunks', 'hunks-file', 'create', 'target', 'intent', 'lines', 'start-line', 'end-line', 'line-cue', 'anchor', 'old', 'context', 'handle', 'line', 'position', 'replace', 'expect', 'near', 'eol'],
94
- bool: ['pretty', 'dry-run', 'plan', 'transaction', 'replace-all', 'require-observation'],
89
+ value: ['cwd', 'file', 'targets', 'hunks', 'hunks-file', 'create', 'target', 'lines', 'start-line', 'end-line', 'line-cue', 'anchor', 'old', 'context', 'handle', 'line', 'position', 'replace', 'expect', 'near', 'eol'],
90
+ bool: ['pretty', 'dry-run', 'plan', 'replace-all', 'require-observation'],
95
91
  },
96
92
  write: {
97
93
  value: ['cwd', 'file', 'content', 'eol'],
98
- bool: ['pretty', 'dry-run', 'plan', 'transaction', 'no-overwrite', 'allow-unobserved'],
94
+ bool: ['pretty', 'dry-run', 'plan', 'no-overwrite', 'allow-unobserved'],
99
95
  },
100
- undo: { value: ['cwd', 'id'], bool: ['pretty', 'force'] },
101
- transactions: { value: ['cwd'], bool: ['pretty'] },
102
96
  doctor: { value: ['cwd'], bool: ['pretty', 'json'] },
103
97
  capabilities: { value: ['cwd'], bool: ['pretty'] },
104
98
  mcp: { value: ['cwd'], bool: ['pretty'] },
@@ -177,20 +171,20 @@ const USAGE = [
177
171
  " aura basetools edit --file <路径> (--hunks '<json>' | --hunks-file <路径>) [--dry-run|--plan] [--eol auto|preserve|lf|crlf] [--require-observation]",
178
172
  ' aura basetools edit --file <路径> --lines <a,b> --replace <文本> [--expect <文本>] [--near <n>]',
179
173
  ' aura basetools edit --file <路径> --old <原文> --replace <文本> --replace-all # 改全部非重叠出现',
180
- " (只改一处时不必包成 JSON:--target / --intent / --lines / --start-line+--end-line / --line-cue / --anchor 任选其一)",
174
+ " (只改一处时不必包成 JSON:--target / --lines / --start-line+--end-line / --line-cue / --anchor 任选其一)",
181
175
  // 句柄(fs-contract-v2 §5):读侧发的柄 + 三种定位写法。下面三行是三种**用法**,不是三种键。
182
176
  ' aura basetools edit --file <路径> --handle <h…> --replace <文本> # 整段替换:区间由柄定,不靠转述',
183
177
  ' aura basetools edit --file <路径> --handle <h…> --line <段内行号> --replace <文本> # 只换区间内第 k 行(1-based,从区间首行数起)',
184
178
  ' aura basetools edit --file <路径> --handle <h…> --position before|after --replace <文本> # 在区间边界插入,区间本身不动',
185
179
  " (--line / --position 是从属键:只有与 --handle 同时给才成立,单独给不构成一次编辑)",
186
180
  ' aura basetools mcp 启动 MCP stdio 服务',
187
- ' aura basetools write --file <路径> --content <文本|@文件> [--no-overwrite] [--dry-run|--plan] [--transaction] [--allow-unobserved] [--eol auto|preserve|lf|crlf]',
181
+ ' aura basetools write --file <路径> --content <文本|@文件> [--no-overwrite] [--dry-run|--plan] [--allow-unobserved] [--eol auto|preserve|lf|crlf]',
188
182
  ' (覆写默认要求先读过这一版;--allow-unobserved 才允许盲写)',
189
183
  ' aura basetools doctor [--json] 自检:Node/版本/MCP 握手/缓存与锁目录可写',
190
184
  ' aura basetools capabilities [--pretty] 能力清单:读模式/地址档/语言/限制(JSON)',
191
185
  ' aura --help | --version',
192
186
  '',
193
- 'hunks 形状:[{"target":"符号名"|"intent":"模糊意图(符号片段/短语/近似原文)"|"lines":[a,b]|"line_cue":"唯一词"|"anchor":"记得的原文",',
187
+ 'hunks 形状:[{"target":"符号名"|"lines":[a,b]|"line_cue":"唯一词"|"anchor":"记得的原文",',
194
188
  ' "replace":"替换文本","expect":"该处应是什么","near":行号}]',
195
189
  '',
196
190
  '例:',
@@ -311,7 +305,7 @@ if (command === '--version' || command === '-v') emit({ ok: true, version: VERSI
311
305
  * 旧顶层动词只给**迁移提示**,不再作为别名执行——保留别名等于保留未来的撞名。
312
306
  * 只提示、不执行:`aura basetools read` 不会偷偷改文件。
313
307
  */
314
- const MOVED_COMMANDS = new Set(['read', 'outline', 'edit', 'write', 'undo', 'transactions', 'mcp', 'doctor', 'capabilities'])
308
+ const MOVED_COMMANDS = new Set(['read', 'outline', 'edit', 'write', 'mcp', 'doctor', 'capabilities'])
315
309
  if (!namespaced && MOVED_COMMANDS.has(command)) {
316
310
  emit(
317
311
  {
@@ -356,26 +350,12 @@ if (command === 'write') {
356
350
  content,
357
351
  dryRun: has('dry-run') || has('plan'),
358
352
  noOverwrite: has('no-overwrite'),
359
- includeTransaction: has('transaction'),
360
353
  allowUnobserved: has('allow-unobserved'),
361
354
  ...(typeof eol === 'string' ? { eol } : {}),
362
355
  })
363
356
  emit(env, env.ok ? 0 : 2)
364
357
  }
365
358
 
366
- if (command === 'undo') {
367
- const env = await undoEnvelope(provider, {
368
- id: typeof flag('id') === 'string' ? flag('id') : undefined,
369
- force: has('force'),
370
- })
371
- emit(env, env.ok ? 0 : 2)
372
- }
373
-
374
- if (command === 'transactions') {
375
- const env = transactionsEnvelope(provider)
376
- emit(env, 0)
377
- }
378
-
379
359
  if (command === 'doctor') {
380
360
  /**
381
361
  * `aura basetools doctor`:把“装完能不能用”的机械检查放在 CLI 自己身上。
@@ -492,7 +472,6 @@ if (command === 'edit') {
492
472
  const env = await editManyEnvelope(provider, {
493
473
  targets: spec.targets,
494
474
  dryRun: has('dry-run') || has('plan') || spec.dryRun,
495
- includeTransaction: has('transaction'),
496
475
  ...(has('require-observation') ? { requireObservation: true } : {}),
497
476
  })
498
477
  emit(env, env.ok ? 0 : 2)
@@ -509,8 +488,6 @@ if (command === 'edit') {
509
488
  */
510
489
  const flat = flatHunk({
511
490
  target: flag('target'),
512
- // 意图地址(T16):`--intent 'config loader'` 一句话定位一处改动,不要求符号全名或原文。
513
- intent: flag('intent'),
514
491
  lines: flag('lines'),
515
492
  start_line: flag('start-line'),
516
493
  end_line: flag('end-line'),
@@ -593,7 +570,7 @@ if (command === 'edit') {
593
570
  {
594
571
  ok: false,
595
572
  error:
596
- 'edit 需要 --hunks/--hunks-file,或平铺一组地址旗标(--target / --intent / --lines / --start-line+--end-line / --line-cue / --anchor / --handle(配 --line 或 --position),外加 --replace),或 --create <整份内容>',
573
+ 'edit 需要 --hunks/--hunks-file,或平铺一组地址旗标(--target / --lines / --start-line+--end-line / --line-cue / --anchor / --handle(配 --line 或 --position),外加 --replace),或 --create <整份内容>',
597
574
  code: 'USAGE',
598
575
  },
599
576
  1,
@@ -607,7 +584,6 @@ if (command === 'edit') {
607
584
  hunks: hunks ?? [],
608
585
  dryRun: has('dry-run') || has('plan'),
609
586
  create,
610
- includeTransaction: has('transaction'),
611
587
  ...(has('require-observation') ? { requireObservation: true } : {}),
612
588
  })
613
589
  emit(env, env.ok ? 0 : 2)
package/lib/ops.mjs CHANGED
@@ -13,7 +13,7 @@ import fs from 'node:fs'
13
13
  import { fileURLToPath } from 'node:url'
14
14
  import { createNodeFsProvider, DEFAULT_MAX_TEXT_BYTES } from './fs-provider.mjs'
15
15
  import { withWorkspaceLock } from './workspace-lock.mjs'
16
- import { createTransaction, listTransactions, undoTransaction } from './transaction-store.mjs'
16
+ import { createTransaction } from './transaction-store.mjs'
17
17
  import { createDiskStore } from './observation-store.mjs'
18
18
 
19
19
  /**
@@ -93,8 +93,8 @@ export function capabilitiesEnvelope() {
93
93
  addressKinds: [...new Set([...engineArgs.FLAT_ADDRESS_KEYS, 'old', 'context', 'position'])],
94
94
  options: ['replace_all', 'require_observation'],
95
95
  writeSafety: {
96
- cli: ['transaction', 'undo', 'workspace-lock', 'stale-region-gate', 'byte-verify'],
97
- mcp: ['transaction', 'undo', 'workspace-lock', 'stale-region-gate', 'byte-verify'],
96
+ cli: ['workspace-lock', 'stale-region-gate', 'byte-verify'],
97
+ mcp: ['workspace-lock', 'stale-region-gate', 'byte-verify'],
98
98
  dsh: ['stale-region-gate', 'byte-verify'],
99
99
  },
100
100
  },
@@ -103,14 +103,14 @@ export function capabilitiesEnvelope() {
103
103
  operations: ['create', 'overwrite'],
104
104
  options: ['eol'],
105
105
  writeSafety: {
106
- cli: ['transaction', 'undo', 'workspace-lock', 'stale-region-gate', 'observation-gate', 'byte-verify'],
107
- mcp: ['transaction', 'undo', 'workspace-lock', 'stale-region-gate', 'observation-gate', 'byte-verify'],
108
- // DSH 壳没有事务/undo/工作区锁;它经过引擎的区域闸、严格新鲜度与回读校验。
106
+ cli: ['workspace-lock', 'stale-region-gate', 'observation-gate', 'byte-verify'],
107
+ mcp: ['workspace-lock', 'stale-region-gate', 'observation-gate', 'byte-verify'],
108
+ // DSH 壳没有工作区锁;它经过引擎的区域闸、严格新鲜度与回读校验。
109
109
  dsh: ['stale-region-gate', 'observation-gate', 'byte-verify'],
110
110
  },
111
111
  },
112
112
  ],
113
- commands: ['basetools read', 'basetools edit', 'basetools write', 'basetools undo', 'basetools transactions', 'basetools mcp', 'basetools doctor', 'basetools capabilities', '--version', '--help'],
113
+ commands: ['basetools read', 'basetools edit', 'basetools write', 'basetools mcp', 'basetools doctor', 'basetools capabilities', '--version', '--help'],
114
114
  langs,
115
115
  langsCount: langs.length,
116
116
  limits: {
@@ -489,12 +489,9 @@ export async function editEnvelope(provider, request) {
489
489
  // 事务需要工作区根与真实文件系统;测试/嵌入式 provider 没有 root 时保持原来的引擎行为。
490
490
  if (provider.root === undefined) return editEnvelopeInner(provider, request)
491
491
  /**
492
- * 预演不建事务:否则 `--dry-run --transaction` 会提交一条 0 文件的 committed 事务,
493
- * 把 `aura undo`(最近一次 committed)顶掉——用户以为撤销了真实编辑,实际 restored:0。
494
- * 预演也不写盘,不需要工作区锁。
492
+ * 预演不建事务、不写盘,也不需要工作区锁;0.14.0 起事务日志只用于失败回滚。
495
493
  */
496
494
  if (request.dryRun === true) return editEnvelopeInner(provider, request)
497
- const includeTransaction = request.includeTransaction === true
498
495
  try {
499
496
  return await withWorkspaceLock(provider.root, async () => {
500
497
  const tx = createTransaction(provider.root, 'aura_basetools_edit')
@@ -508,10 +505,10 @@ export async function editEnvelope(provider, request) {
508
505
  error.rollback = rollback
509
506
  throw error
510
507
  }
511
- return includeTransaction ? { ...env, result: { ...env.result, transaction: tx.summary() } } : env
508
+ return env
512
509
  }
513
- const rollback = await tx.rollback(provider)
514
- return includeTransaction ? { ...env, transaction: tx.summary(), rollback } : env
510
+ await tx.rollback(provider)
511
+ return env
515
512
  })
516
513
  } catch (error) {
517
514
  return transactionError(error)
@@ -523,7 +520,6 @@ export async function editManyEnvelope(provider, request) {
523
520
  if (provider.root === undefined) return editManyEnvelopeInner(provider, request)
524
521
  // 与单目标同一条:预演不建事务、不写盘,不能顶掉真正的最近 committed 事务。
525
522
  if (request.dryRun === true) return editManyEnvelopeInner(provider, request)
526
- const includeTransaction = request.includeTransaction === true
527
523
  try {
528
524
  return await withWorkspaceLock(provider.root, async () => {
529
525
  const tx = createTransaction(provider.root, 'aura_basetools_edit')
@@ -538,14 +534,13 @@ export async function editManyEnvelope(provider, request) {
538
534
  error.rollback = rollback
539
535
  throw error
540
536
  }
541
- return includeTransaction ? { ...batch, transaction: tx.summary() } : batch
537
+ return batch
542
538
  }
543
539
  const rollback = await tx.rollback(provider)
544
540
  const finalBatch = writePath.rebuildAfterRollback(batch, rollback)
545
541
  return {
546
542
  ...finalBatch,
547
543
  ok: false,
548
- ...(includeTransaction ? { transaction: tx.summary() } : {}),
549
544
  }
550
545
  } catch (error) {
551
546
  const rollback = await tx.rollback(provider)
@@ -579,7 +574,6 @@ function compactWriteReceipt(env, content) {
579
574
  dryRun,
580
575
  ...(result.write_verify === undefined ? {} : { write_verify: result.write_verify }),
581
576
  ...(result.unverified_region === true ? { unverified_region: true } : {}),
582
- ...(result.transaction === undefined ? {} : { transaction: result.transaction }),
583
577
  },
584
578
  }
585
579
  }
@@ -600,7 +594,7 @@ function compactWriteReceipt(env, content) {
600
594
  * `preserve` 按 content 原样写(与内置 Write 的“给什么写什么”对齐),`lf` / `crlf` 强制统一。
601
595
  * 新建文件永远按 content 原样写;显式传非 `auto` 的 eol 会被拒绝(不做静默忽略)。
602
596
  */
603
- export async function writeEnvelope(provider, { file, content, dryRun = false, noOverwrite = false, includeTransaction = false, allowUnobserved = false, eol = 'auto' } = {}) {
597
+ export async function writeEnvelope(provider, { file, content, dryRun = false, noOverwrite = false, allowUnobserved = false, eol = 'auto' } = {}) {
604
598
  if (typeof file !== 'string' || file.length === 0) {
605
599
  return { ok: false, error: 'write needs a file path', code: 'WRITE_BAD_ARGS' }
606
600
  }
@@ -627,7 +621,7 @@ export async function writeEnvelope(provider, { file, content, dryRun = false, n
627
621
  if (dryRun) {
628
622
  return { ok: true, result: { path: displayPath, operation: 'create', bytes: Buffer.byteLength(content, 'utf8'), dryRun: true } }
629
623
  }
630
- return compactWriteReceipt(await editEnvelope(provider, { file, create: content, includeTransaction }), content)
624
+ return compactWriteReceipt(await editEnvelope(provider, { file, create: content }), content)
631
625
  }
632
626
  return compactWriteReceipt(
633
627
  await editEnvelope(provider, {
@@ -635,7 +629,6 @@ export async function writeEnvelope(provider, { file, content, dryRun = false, n
635
629
  wholeFile: content,
636
630
  eol,
637
631
  dryRun,
638
- includeTransaction,
639
632
  requireObservation: allowUnobserved !== true,
640
633
  skipStaleGate: allowUnobserved === true,
641
634
  toolName: 'aura_basetools_write',
@@ -646,17 +639,3 @@ export async function writeEnvelope(provider, { file, content, dryRun = false, n
646
639
  return transactionError(error)
647
640
  }
648
641
  }
649
-
650
- export async function undoEnvelope(provider, { id, force } = {}) { try {
651
- return {
652
- ok: true,
653
- ...(await withWorkspaceLock(provider.root, () => undoTransaction(provider.root, provider, { id, force: force === true }))),
654
- }
655
- } catch (error) {
656
- return transactionError(error)
657
- }
658
- }
659
-
660
- export function transactionsEnvelope(provider) {
661
- return { ok: true, transactions: listTransactions(provider.root) }
662
- }
@@ -1,17 +1,16 @@
1
1
  /**
2
- * transaction-store.mjs —— 写事务日志、回滚与 undo。
2
+ * transaction-store.mjs —— 写事务日志与**失败回滚**(内部实现,不再暴露 undo/transactions 工具)。
3
3
  *
4
4
  * 一次写事务在 `~/.aura/transactions/<workspace-key>/<id>/` 下留下:
5
5
  * · `meta.json` —— 事务状态与每个文件的原始 mode/版本/备份名;
6
6
  * · `originals/<n>.bin` —— 写前原始字节(逐字节,含 BOM/行尾)。
7
7
  *
8
8
  * 事务的生命周期:
9
- * pending → committed(写成功,可 undo)
9
+ * pending → committed(写成功;日志仅用于内部回滚,不再提供 undo)
10
10
  * → rolled_back(写失败,已恢复原字节)
11
11
  * → recovery_required(写失败且恢复也失败;meta 里留着可手工恢复的路径)
12
- * committed → undone(undo 成功)
13
12
  *
14
- * 回滚/undo 前会比一次 `afterVersion`:文件在事务之后又被别人改过就拒绝覆盖(`--force` 才强制)。
13
+ * 回滚前会比一次 `afterVersion`:文件在事务之后又被别人改过就拒绝覆盖。
15
14
  * 这不是安全边界;它只保证遵守本协议的进程不会静默覆盖并发写。
16
15
  */
17
16
  import fs from 'node:fs'
@@ -262,21 +261,6 @@ function readAllMeta(root) {
262
261
  return out.sort((a, b) => String(b.createdAt).localeCompare(String(a.createdAt)))
263
262
  }
264
263
 
265
- /** 取最近一条可 undo 的 committed 事务。 */
266
- export function latestCommitted(root) {
267
- return readAllMeta(root).find((meta) => meta.status === 'committed') ?? null
268
- }
269
-
270
- export function listTransactions(root) {
271
- return readAllMeta(root).map((meta) => ({
272
- id: meta.id,
273
- tool: meta.tool,
274
- createdAt: meta.createdAt,
275
- status: meta.status,
276
- files: Array.isArray(meta.files) ? meta.files.length : 0,
277
- dir: meta.dir,
278
- }))
279
- }
280
264
 
281
265
  function prune(root, keep = KEEP) {
282
266
  const metas = readAllMeta(root).filter((meta) => meta.status !== 'pending')
@@ -285,66 +269,3 @@ function prune(root, keep = KEEP) {
285
269
  }
286
270
  }
287
271
 
288
- /**
289
- * 撤销一条 committed 事务。
290
- *
291
- * 任何文件在当前时间点的版本与事务提交后的版本不一致 ⇒ 默认拒绝,避免覆盖后续编辑;
292
- * `force: true` 才强制恢复旧字节。
293
- */
294
- export async function undoTransaction(root, provider, options = {}) {
295
- const metas = readAllMeta(root)
296
- const meta = options.id ? metas.find((m) => m.id === options.id) : metas.find((m) => m.status === 'committed')
297
- if (meta === undefined) {
298
- throw Object.assign(new Error(options.id ? `transaction not found: ${options.id}` : 'no committed transaction to undo'), {
299
- code: 'TRANSACTION_NOT_FOUND',
300
- })
301
- }
302
- if (meta.status !== 'committed') {
303
- throw Object.assign(new Error(`transaction ${meta.id} is ${meta.status}, not committed`), {
304
- code: 'TRANSACTION_NOT_UNDOABLE',
305
- status: meta.status,
306
- })
307
- }
308
- const force = options.force === true
309
- /**
310
- * 先做一次**只读预检**:任何文件在事务后又被改过 ⇒ 整个 undo 拒绝。
311
- * 旧顺序会先把还能恢复的文件写回去,再发现某个文件冲突——那就是一次部分 undo;
312
- * 对一个以“不会静默改错”为卖点的工具,部分恢复比拒绝更坏。
313
- */
314
- const plans = []
315
- const conflicts = []
316
- for (const entry of meta.files) {
317
- if (!entry.written) continue
318
- let current
319
- try { current = provider.stat(entry.path) } catch { current = undefined }
320
- if (!force && entry.afterVersion !== null && current?.version !== entry.afterVersion) {
321
- conflicts.push({ path: entry.path, reason: 'changed since this transaction', expected: entry.afterVersion, actual: current?.version ?? null })
322
- }
323
- plans.push(entry)
324
- }
325
- if (conflicts.length > 0) {
326
- throw Object.assign(new Error(`transaction ${meta.id} cannot be undone: ${conflicts.length} file(s) changed after it`), {
327
- code: 'TRANSACTION_STALE',
328
- transaction: meta.id,
329
- conflicts,
330
- })
331
- }
332
- let restored = 0
333
- for (const entry of plans) {
334
- try {
335
- if (entry.existed) {
336
- writeBytesAtomic(entry.realPath, fs.readFileSync(path.join(meta.dir, entry.backup)), entry.mode ?? 0o600)
337
- } else {
338
- fs.rmSync(entry.realPath, { force: true })
339
- }
340
- restored += 1
341
- } catch (error) {
342
- conflicts.push({ path: entry.path, reason: String(error?.message ?? error) })
343
- }
344
- }
345
- meta.status = conflicts.length === 0 ? 'undone' : 'recovery_required'
346
- meta.undoneAt = new Date().toISOString()
347
- meta.undoConflicts = conflicts
348
- saveMeta(meta)
349
- return { transaction: meta.id, status: meta.status, restored, conflicts, dir: meta.dir }
350
- }
@@ -126,12 +126,12 @@ export function normalizeReadRequest(args, totalLines = undefined) {
126
126
  * 导出它是为了不再出现第二份:守卫要拿它算“合法键集合”,而如果有两份名单,
127
127
  * 新增一种地址写法时必有一处漏改——那正是本项目反复出现的形状。
128
128
  */
129
- export const FLAT_ADDRESS_KEYS = ['target', 'intent', 'lines', 'start_line', 'end_line', 'line_cue', 'anchor', 'handle', 'old']
129
+ export const FLAT_ADDRESS_KEYS = ['target', 'lines', 'start_line', 'end_line', 'line_cue', 'anchor', 'handle', 'old']
130
130
 
131
131
  /**
132
132
  * `targets` 元素(单目标参数)的合法键。
133
133
  *
134
- * 与顶层工具键**分开**:`targets` 本身、批量级的 `include_transaction` 都不是元素参数;
134
+ * 与顶层工具键**分开**:`targets` 本身不是元素参数;
135
135
  * 放进元素里只会被静默丢掉。三面(CLI / MCP / DSH)共用这一份,未知键一律
136
136
  * `ARGS_UNKNOWN_KEY`,不再只校验顶层。
137
137
  */
@@ -140,7 +140,7 @@ export const MT_READ_ELEMENT_KEYS = [
140
140
  ]
141
141
  export const MT_EDIT_ELEMENT_KEYS = [
142
142
  'file_path', 'hunks', 'create', 'dry_run', 'require_observation',
143
- 'target', 'intent', 'lines', 'start_line', 'end_line', 'line_cue', 'anchor', 'old', 'context',
143
+ 'target', 'lines', 'start_line', 'end_line', 'line_cue', 'anchor', 'old', 'context',
144
144
  'replace', 'replace_all', 'eol', 'expect', 'near', 'handle', 'line', 'position',
145
145
  ]
146
146
 
@@ -23,28 +23,24 @@ import { indexFile, locateWith, normSpace } from './anchor-core.mjs'
23
23
  /**
24
24
  * 定位一份源码里的改动点。
25
25
  *
26
- * 五层寻址,按"模型要提供什么"从强到弱排列。每层覆盖上一层够不到的场景,
26
+ * 四层寻址,按"模型要提供什么"从强到弱排列。每层覆盖上一层够不到的场景,
27
27
  * 但**都不要求模型复现字节**——这是它们与 `edit` 的根本差别:
28
28
  *
29
- * 1. `intent` —— 一段模糊意图(符号名片段 / 短语 / 近似原文)。引擎按固定次序收敛:
30
- * 精确符号 → 符号子串 → 文本锚点;收敛不了就报歧义并给候选,绝不猜。
31
- * 2. `target` —— 符号名。不读文本,对内容记错免疫。适合改整个函数/类/类型。
32
- * 3. `lines` —— 行号区间。模型刚用 `read` 看过文件,输出就是 `<行号>: <文本>`,
29
+ * 1. `target` —— 符号名。不读文本,对内容记错免疫。适合改整个函数/类/类型。
30
+ * 2. `lines` —— 行号区间。模型刚用 `read` 看过文件,输出就是 `<行号>: <文本>`,
33
31
  * `grep` 也给行号——**位置是它本来就知道的信息**,我却一直只让它给内容。
34
32
  * 把行号纳入地址空间后,符号体内的任意一行都变得可精确寻址。
35
- * 4. `lineCue` —— 一个词。当模型记不住行号时,说"就在有 resolvePolicy 的那一行"。
33
+ * 3. `lineCue` —— 一个词。当模型记不住行号时,说"就在有 resolvePolicy 的那一行"。
36
34
  * 只接受**全文件唯一**的线索;不唯一则报歧义并给出候选行号,绝不猜。
37
- * 5. `anchor` —— 记得的原文。五级降级匹配,容错性最好但要求模型复现内容。
35
+ * 4. `anchor` —— 记得的原文。五级降级匹配,容错性最好但要求模型复现内容。
38
36
  *
39
- * `intent` 与其余四层**互斥**(由 `planHunks` 判):同时给两个地址时谁说了算没有正确答案。
40
- * 前两层偏**精确**(要么命中要么报错),后三层偏**容错**。
41
- * 调用方可以传 `assert` 做内容校验,五层都适用——那才是防止"指到错的行"的那道闸。
37
+ * 前两层偏**精确**(要么命中要么报错),后两层偏**容错**。
38
+ * 调用方可以传 `assert` 做内容校验,四层都适用——那才是防止"指到错的行"的那道闸。
42
39
  * 判据是**包含且唯一**:这段文字必须在整个文件里只出现一处,且该处落在受寻址区间内
43
40
  * (短引文例外,仍要求与区间整段相等;推导见 {@link ASSERT_RELAX_MIN_CHARS})。
44
41
  *
45
42
  * @param {string} source - 源码全文
46
43
  * @param {object} request - 定位请求
47
- * @param {string} [request.intent] - 模糊意图地址(符号名片段 / 短语 / 近似原文)
48
44
  * @param {string} [request.target] - 符号名(`name` / `kind:name` / `Outer/Inner` / `name#n`)
49
45
  * @param {{start:number,end:number}} [request.lines] - 1-based 闭区间行号
50
46
  * @param {string} [request.lineCue] - 一个词的线索,必须全文件唯一
@@ -644,53 +640,14 @@ function assertVerdict(lines, startLine, endLine, tier, actual, assertText) {
644
640
  }
645
641
  }
646
642
 
647
- /**
648
- * 意图地址(T16)的符号子串匹配:这个候选符号离 `near` 有多远。
649
- *
650
- * 判据只有一条:**`near` 落在符号的跨度里就是 0**,否则取到两端中较近的那一端。
651
- * 与 `resolveTarget` 的 `|startLine - near|` 刻意不同:`intent` 匹配的是"大概想改哪个符号",
652
- * 调用方给的 `near` 常常是**符号体内**的一行(它刚从 read 回执里抄来),拿起点算距离会把
653
- * 一个明显在说的符号判成第二近。跨度包含 `near` ⇒ 距离 0,是这里唯一能自洽的口径。
654
- *
655
- * @param - 符号投影(含 startLine / endLine)
656
- * @param near - 1-based 行号提示
657
- * @returns 非负整数距离
658
- */
659
- function symbolDistanceFrom(symbol, near) {
660
- if (near >= symbol.startLine && near <= symbol.endLine) return 0
661
- return Math.min(Math.abs(near - symbol.startLine), Math.abs(near - symbol.endLine))
662
- }
663
643
 
664
644
  /**
665
- * 多个子串命中里,`near` 能否**严格**收敛到一个(唯一最小值)。
645
+ * 定位一个区间。四层降级:符号身份 → 行号 → 唯一词 → 文本锚点。
666
646
  *
667
- * 不唯一(并列最小)⇒ null:那正是"该报歧义"的形状,猜测在这里比拒绝贵得多。
668
- * @returns 命中的符号,或 null(near 没给 / 并列)
669
- */
670
- function nearestSymbolBySpan(symbols, near) {
671
- let best = null
672
- let bestDistance = Infinity
673
- let ties = 0
674
- for (const symbol of symbols) {
675
- const distance = symbolDistanceFrom(symbol, near)
676
- if (distance < bestDistance) {
677
- bestDistance = distance
678
- best = symbol
679
- ties = 1
680
- } else if (distance === bestDistance) ties += 1
681
- }
682
- return ties === 1 ? best : null
683
- }
684
-
685
- /**
686
- * 定位一个区间。五层降级:意图 → 符号身份 → 行号 → 唯一词 → 文本锚点。
687
- *
688
- * `intent`(T16)是**最弱但最自然**的一层:调用方给一个符号名片段、短语或近似原文,
689
- * 引擎先当符号名解析(精确/限定写法),再按符号名子串匹配,最后才落到文本匹配。
690
647
  * 每一档都**确定性地**收敛到一个区间;收敛不了就报歧义并给候选,不猜。
691
648
  *
692
649
  * @param source - 文件全文
693
- * @param request - `intent` / `target` / `lines` / `lineCue` / `anchor` 五选一,外加 `near` 与 `assert`
650
+ * @param request - `target` / `lines` / `lineCue` / `anchor` 四选一,外加 `near` 与 `assert`
694
651
  * @returns 命中时 `{ok: true, startLine, endLine, tier, actual}`;未命中时 `{ok: false, code, message, ...}`。
695
652
  * `actual` 是该区间的原文,供调用方核对或展示。
696
653
  */
@@ -761,125 +718,6 @@ export function locate(source, request = {}) {
761
718
  return { ok: false, code: 'LOCATOR_LINE_SHARED', message, shared }
762
719
  }
763
720
 
764
- // ── 第 0 层:意图地址(T16)。调用方给一段模糊的意图串,引擎确定性地落成精确区间。
765
- //
766
- // 解析次序(每一档都不猜,落不下来就报歧义并给候选):
767
- // ① 已知语言时,先当符号名解析(`resolveTarget` 的精确 / 限定写法)——命中就算 symbol 档;
768
- // ② 符号名没对上(或对上多个)⇒ 按符号名**子串**匹配(先区分大小写,再忽略大小写);
769
- // ③ 一个符号都没命中(或语言未知)⇒ 落到文本匹配(`locateWith`,与 `anchor` 同一份引擎)。
770
- //
771
- // 顺序上刻意在 `target` 之前:`intent` 与其余地址字段**互斥**(`PATCH_INTENT_CONFLICT`,
772
- // 由 `planHunks` 判),所以两者同时到达这里是不可能的;放在最前只是把"intent 自成一层"写明。
773
- if (typeof request.intent === 'string' && request.intent.trim().length > 0) {
774
- const intent = request.intent.trim()
775
- const near = request.near ?? null
776
- const label = clipReceiptLabel(filePath === null ? 'this file' : filePath, RECEIPT_LABEL_CHARS)
777
- /** 跨度覆盖不了符号体时的拒绝——与 `target=` 那一支**同一句话、同一个码**(不是第二套说法)。 */
778
- const spanIncomplete = (hit) => ({
779
- ok: false,
780
- code: 'LOCATOR_SPAN_INCOMPLETE',
781
- message: `cannot address "${hit.name}" by intent in ${label}: the span does not cover the body — ${hit.spanNote}; address it with lines= or line_cue= instead`,
782
- hint: directoryHint(source, lang),
783
- })
784
- if (lang !== null) {
785
- const index = indexStructure(source, lang)
786
- /**
787
- * 精确/限定符号:先**不带 near** 解析,拿到唯一命中或完整候选。
788
- * 带 near 交给 `resolveTarget` 会用“起点距离”,与 intent 档的“跨度包含 near 记 0”冲突,
789
- * 实测会把 near 落在第一个长函数体内的情况选到第二个同名函数上。歧义时这里用
790
- * `nearestSymbolBySpan` 按跨度收敛;解析不了再落到子串匹配。
791
- */
792
- let exact = null
793
- try {
794
- exact = resolveTarget(index, intent, null)
795
- } catch (error) {
796
- if (error.code === 'STRUCT_AMBIGUOUS') {
797
- const candidates = Array.isArray(error.candidates) ? error.candidates : []
798
- if (near !== null && near !== undefined) {
799
- const chosen = nearestSymbolBySpan(candidates, near)
800
- if (chosen !== null) {
801
- if (chosen.spanComplete === false) return spanIncomplete(chosen)
802
- return done(chosen.startLine, chosen.endLine, 'intent~symbol', true)
803
- }
804
- }
805
- return failure('LOCATOR_INTENT_AMBIGUOUS', `intent ${JSON.stringify(intent)}: ${error.message}`, candidates)
806
- }
807
- // STRUCT_NOT_FOUND / 其它:符号名没猜中不是终点,落到子串匹配(下面)。
808
- }
809
- if (exact !== null) {
810
- if (exact.spanComplete === false) return spanIncomplete(exact)
811
- const shared = sharedLineContent(source, index, exact, lang)
812
- if (shared !== null) return sharedLineRefusal(exact, shared, 'intent')
813
- return done(exact.startLine, exact.endLine, 'intent~symbol')
814
- }
815
- // 用原始符号表而不是 `listSymbols` 投影:整行区间是否越界要看 token,投影里没有。
816
- const symbols = index.symbols
817
- let matches = symbols.filter((s) => s.name.includes(intent))
818
- if (matches.length === 0) {
819
- const lower = intent.toLowerCase()
820
- matches = symbols.filter((s) => s.name.toLowerCase().includes(lower))
821
- }
822
- if (matches.length === 1) {
823
- const only = matches[0]
824
- if (only.spanComplete === false) return spanIncomplete(only)
825
- const shared = sharedLineContent(source, index, only, lang)
826
- if (shared !== null) return sharedLineRefusal(only, shared, 'intent')
827
- return done(only.startLine, only.endLine, 'intent~symbol')
828
- }
829
- if (matches.length > 1) {
830
- if (near !== null && near !== undefined) {
831
- const chosen = nearestSymbolBySpan(matches, near)
832
- if (chosen !== null) {
833
- if (chosen.spanComplete === false) return spanIncomplete(chosen)
834
- const shared = sharedLineContent(source, index, chosen, lang)
835
- if (shared !== null) return sharedLineRefusal(chosen, shared, 'intent')
836
- return done(chosen.startLine, chosen.endLine, 'intent~symbol', true)
837
- }
838
- }
839
- const why =
840
- near === null || near === undefined
841
- ? `intent ${JSON.stringify(intent)} matched ${matches.length} symbols — pick one with target=<name>, or add near=<line>`
842
- : `intent ${JSON.stringify(intent)} matched ${matches.length} symbols and near=${near} did not single one out`
843
- return failure(
844
- 'LOCATOR_INTENT_AMBIGUOUS',
845
- why,
846
- matches.map((s) => ({
847
- name: s.name,
848
- kind: s.kind,
849
- startLine: s.startLine,
850
- endLine: s.endLine,
851
- enclosing: s.enclosing,
852
- })),
853
- )
854
- }
855
- }
856
- // ── ③ 文本兜底:与 `anchor` 同一份引擎(`locateWith`);tier 里带上它命中的那一档
857
- try {
858
- const loc = locateWith(indexFile(source), intent, near)
859
- return done(loc.start + 1, loc.end, `intent~text~${loc.tier}`, loc.byNear === true)
860
- } catch (error) {
861
- if (error.code === 'PATCH_AMBIGUOUS') {
862
- /**
863
- * 行候选统一成 `{line, text}`(`failure` 只认这一形状才会印出行号与原文):
864
- * `locateWith` 抛的是 `number[]`,直接递下去会印成 `line undefined`。
865
- */
866
- const listed = (Array.isArray(error.candidates) ? error.candidates : [])
867
- .filter((n) => typeof n === 'number' && n >= 1 && n <= lines.length)
868
- .map((n) => ({ line: n, text: lines[n - 1].slice(0, 80) }))
869
- return failure('LOCATOR_INTENT_AMBIGUOUS', `intent ${JSON.stringify(intent)}: ${error.message}`, listed)
870
- }
871
- if (error.code === 'PATCH_NOT_FOUND' || error.code === 'PATCH_EMPTY_ANCHOR') {
872
- return {
873
- ok: false,
874
- code: 'LOCATOR_INTENT_NOT_FOUND',
875
- message: `no symbol or text matches intent ${JSON.stringify(intent)} in ${label}; try target=<name>, lines=, or line_cue=`,
876
- // 符号目录是实测支撑的恢复路径(与 anchor 失败同一条):只有认识这门语言时才给得出。
877
- ...(lang === null ? {} : { hint: directoryHint(source, lang) }),
878
- }
879
- }
880
- return failure(error.code ?? 'LOCATOR_FAILED', error.message, error.candidates)
881
- }
882
- }
883
721
 
884
722
  // ── 第 1 层:符号身份。不读文本,因此对内容记错免疫。
885
723
  if (typeof request.target === 'string' && request.target.trim().length > 0) {
@@ -1028,7 +866,7 @@ export function locate(source, request = {}) {
1028
866
  }
1029
867
  }
1030
868
 
1031
- return { ok: false, code: 'LOCATOR_NO_REQUEST', message: 'provide one of: intent, target, lines, lineCue, anchor' }
869
+ return { ok: false, code: 'LOCATOR_NO_REQUEST', message: 'provide one of: target, lines, lineCue, anchor' }
1032
870
  }
1033
871
 
1034
872
  /**
@@ -2577,35 +2577,6 @@ function locateByHandle(lines, hunk, parsed, ctx) {
2577
2577
  })
2578
2578
  }
2579
2579
 
2580
- /**
2581
- * `intent` 与哪些字段**不能同时出现**(T16)。
2582
- *
2583
- * `intent` 是**自成一体的一层地址**:它由引擎决定落到哪个精确区间。若同时给出任何别的地址,
2584
- * 谁说了算就没有正确答案——挑一个执行就是一次静默换语义(调用方写的两个地址指向不同地方时,
2585
- * 改错的那次不会有任何提示)。所以参数面直接拒绝,而不是挑一个。
2586
- *
2587
- * 名单与 {@link planHunks} 的地址判据同源:`target` / `lines`(含别名 `range`)/ `start_line` /
2588
- * `end_line` / `line_cue` / `anchor` / `old` / `context` / `handle`。
2589
- * `near` / `expect` / `replace` **不在**名单里:前两个是修饰,第三个是动作。
2590
- *
2591
- * 「在场」的判据逐字段照搬它自己那一层的:字符串看非空(`anchor` 看长度、其余看 trim),
2592
- * 坐标看 `!== undefined && !== null`。判据写宽或写窄都会让某个形状在参数面与引擎里得到两种结论。
2593
- *
2594
- * @returns 冲突的字段名(按上面的固定顺序);没有冲突时为空数组
2595
- */
2596
- function intentAddressConflicts(hunk) {
2597
- const clashes = []
2598
- if (typeof hunk.target === 'string' && hunk.target.trim().length > 0) clashes.push('target')
2599
- for (const key of ['lines', 'range', 'start_line', 'end_line']) {
2600
- if (hunk[key] !== undefined && hunk[key] !== null) clashes.push(key)
2601
- }
2602
- if (typeof hunk.line_cue === 'string' && hunk.line_cue.trim().length > 0) clashes.push('line_cue')
2603
- if (typeof hunk.anchor === 'string' && hunk.anchor.length > 0) clashes.push('anchor')
2604
- if (oldAddressOf(hunk) !== null) clashes.push('old')
2605
- if (typeof hunk.context === 'string' && hunk.context.trim().length > 0) clashes.push('context')
2606
- if (typeof hunk.handle === 'string' && hunk.handle.trim().length > 0) clashes.push('handle')
2607
- return clashes
2608
- }
2609
2580
 
2610
2581
  /**
2611
2582
  * 规划所有 hunk:逐个定位 → expect 核对 → 重叠检查 → 倒序拼接。
@@ -2661,7 +2632,7 @@ export function planHunks(content, hunks, filePath = null, options = {}) {
2661
2632
  *
2662
2633
  * 它不是第六种地址,而是 `old` 的一种作用范围;因此只认 `old` + 动作字段(`replace` /
2663
2634
  * `expect` / `eol`)。把 `target`/`lines`/`context`/`near` 混进来意味着"既想全改又想收敛到一处",
2664
- * 没有正确答案 —— 参数面直接拒绝,与 `intent` 的互斥同一条理由。
2635
+ * 没有正确答案 —— 参数面直接拒绝,与地址字段互斥同一条理由。
2665
2636
  */
2666
2637
  const replaceAll = hunk.replace_all
2667
2638
  if (replaceAll !== undefined && typeof replaceAll !== 'boolean') {
@@ -2673,7 +2644,7 @@ export function planHunks(content, hunks, filePath = null, options = {}) {
2673
2644
  if (replaceAll === true) {
2674
2645
  const clashes = []
2675
2646
  if (hasTarget) clashes.push('target')
2676
- if (typeof hunk.intent === 'string' && hunk.intent.trim().length > 0) clashes.push('intent')
2647
+
2677
2648
  for (const key of ['lines', 'range', 'start_line', 'end_line']) {
2678
2649
  if (hunk[key] !== undefined && hunk[key] !== null) clashes.push(key)
2679
2650
  }
@@ -2708,23 +2679,6 @@ export function planHunks(content, hunks, filePath = null, options = {}) {
2708
2679
  )
2709
2680
  }
2710
2681
  }
2711
- /**
2712
- * 意图地址(T16):`intent` 是**自成一体**的一层地址,与其余地址字段互斥。
2713
- *
2714
- * 冲突在**定位之前**就拒(参数面没成形,与 `PATCH_NO_ADDRESS` 同族),而不是在引擎里挑一个:
2715
- * 调用方写了两个地址说明它自己也没定,工具替它选一次就是一次看不见的换语义。
2716
- * `near` / `expect` / `replace` 不在此列——前两个是修饰,第三个是动作。
2717
- */
2718
- const hasIntent = typeof hunk.intent === 'string' && hunk.intent.trim().length > 0
2719
- if (hasIntent) {
2720
- const clashed = intentAddressConflicts(hunk)
2721
- if (clashed.length > 0) {
2722
- throw new PatchError(
2723
- `\`intent\` is a complete address by itself — it cannot be combined with ${clashed.join(', ')} (near / expect / replace are fine); drop either intent or those fields — nothing was written`,
2724
- 'PATCH_INTENT_CONFLICT',
2725
- )
2726
- }
2727
- }
2728
2682
  /** 句柄的形状在这里判(不对就抛错);`{handle, old}` 那一支的 `old` 仍然要命中区间内部。 */
2729
2683
  const parsedHandle = handleAddressOf(hunk)
2730
2684
  /**
@@ -2752,9 +2706,9 @@ export function planHunks(content, hunks, filePath = null, options = {}) {
2752
2706
  'PATCH_NO_ADDRESS',
2753
2707
  )
2754
2708
  }
2755
- if (oldText === null && parsedHandle === null && !hasTarget && range === undefined && !hasCue && !hasAnchor && !hasIntent) {
2709
+ if (oldText === null && parsedHandle === null && !hasTarget && range === undefined && !hasCue && !hasAnchor) {
2756
2710
  throw new PatchError(
2757
- gaveLineAddress(hunk) ? LINE_ADDRESS_HINT : 'each hunk needs one of: old, intent, target, lines, line_cue, anchor',
2711
+ gaveLineAddress(hunk) ? LINE_ADDRESS_HINT : 'each hunk needs one of: old, target, lines, line_cue, anchor',
2758
2712
  'PATCH_NO_ADDRESS',
2759
2713
  )
2760
2714
  }
@@ -2776,29 +2730,16 @@ export function planHunks(content, hunks, filePath = null, options = {}) {
2776
2730
  ? locateAllByOld(lines, hunk, oldText, filePath === null ? 'this file' : filePath, handleCtx)
2777
2731
  : [locateByOld(lines, hunk, oldText, filePath === null ? 'this file' : filePath, handleCtx)]
2778
2732
  } else {
2779
- /**
2780
- * 意图地址(T16)单独走一条请求:只带 `intent` / `near` / `expect` / `filePath`。
2781
- * 冲突字段在上面已经拒了,这里刻意**不再把其余地址字段递下去**——即便哪天冲突判据放宽,
2782
- * 引擎也不该从第二个地址里"顺手"拿到位置(那正是静默换语义的来路)。
2783
- */
2784
- const request = hasIntent
2785
- ? {
2786
- intent: hunk.intent,
2787
- ...(hunk.near === undefined ? {} : { near: hunk.near }),
2788
- ...(hunk.expect === undefined || hunk.expect === null ? {} : { assert: hunk.expect }),
2789
- ...(filePath === null ? {} : { filePath }),
2790
- ...(options.allowLoneCr === true ? { allowLoneCr: true } : {}),
2791
- }
2792
- : {
2793
- ...(hasTarget ? { target: hunk.target } : {}),
2794
- ...(range === undefined ? {} : { lines: range }),
2795
- ...(hunk.line_cue === undefined ? {} : { lineCue: hunk.line_cue }),
2796
- ...(hasTarget || range !== undefined || hunk.line_cue !== undefined ? {} : { anchor: hunk.anchor }),
2797
- ...(hunk.near === undefined ? {} : { near: hunk.near }),
2798
- ...(hunk.expect === undefined || hunk.expect === null ? {} : { assert: hunk.expect }),
2799
- ...(filePath === null ? {} : { filePath }),
2800
- ...(options.allowLoneCr === true ? { allowLoneCr: true } : {}),
2801
- }
2733
+ const request = {
2734
+ ...(hasTarget ? { target: hunk.target } : {}),
2735
+ ...(range === undefined ? {} : { lines: range }),
2736
+ ...(hunk.line_cue === undefined ? {} : { lineCue: hunk.line_cue }),
2737
+ ...(hasTarget || range !== undefined || hunk.line_cue !== undefined ? {} : { anchor: hunk.anchor }),
2738
+ ...(hunk.near === undefined ? {} : { near: hunk.near }),
2739
+ ...(hunk.expect === undefined || hunk.expect === null ? {} : { assert: hunk.expect }),
2740
+ ...(filePath === null ? {} : { filePath }),
2741
+ ...(options.allowLoneCr === true ? { allowLoneCr: true } : {}),
2742
+ }
2802
2743
  const found = locate(content, request)
2803
2744
  if (!found.ok) {
2804
2745
  throw new PatchError(found.message, found.code, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aura-basetools",
3
- "version": "0.13.3",
3
+ "version": "0.14.0",
4
4
  "description": "Byte-exact read, edit and write tools for coding agents. Standalone CLI and MCP server; no DSH required. 面向 coding agent 的逐字节读写工具;独立 CLI / MCP,可选 DSH 适配器。",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -179,7 +179,7 @@ say('B2 能力清单:capabilities 从引擎常量派生')
179
179
  check(caps.ok === true && caps.langsCount === caps.langs?.length, `langsCount=${caps.langsCount}`, JSON.stringify(caps.langs?.length))
180
180
  check(caps.langsCount === 65, '语言表 65 种(与 LANGS 同源)', String(caps.langsCount))
181
181
  check(readTool?.modes?.includes('auto') && readTool.modes.includes('map') && readTool.modes.includes('changed'), '读模式含 auto/map/changed', JSON.stringify(readTool?.modes))
182
- check(editTool?.addressKinds?.includes('intent') && editTool.addressKinds.includes('target') && editTool.addressKinds.includes('handle'), '编辑地址档含 intent/target/handle', JSON.stringify(editTool?.addressKinds))
182
+ check(editTool?.addressKinds?.includes('target') && editTool.addressKinds.includes('handle'), '编辑地址档含 target/handle', JSON.stringify(editTool?.addressKinds))
183
183
  check(editTool?.options?.includes('replace_all'), '编辑选项含 replace_all', JSON.stringify(editTool?.options))
184
184
  check(editTool?.options?.includes('require_observation'), '编辑选项含 require_observation', JSON.stringify(editTool?.options))
185
185
  check(caps.limits?.multiTargetsMax === 32 && caps.limits?.defaultPruneBudget === 8192 && caps.limits?.contentBudget === 7492, '限制与常量一致', JSON.stringify(caps.limits))
@@ -385,7 +385,7 @@ say('F MCP:initialize / tools/list / tools/call')
385
385
  })()
386
386
  if (manifest !== null) check(byId.get(1)?.result?.serverInfo?.version === manifest.version, `serverInfo.version 跟包一致(${manifest.version})`, String(byId.get(1)?.result?.serverInfo?.version))
387
387
  const tools = byId.get(2)?.result?.tools ?? []
388
- check(tools.length === 5 && tools.some((t) => t.name === 'aura_basetools_read') && tools.some((t) => t.name === 'aura_basetools_edit') && tools.some((t) => t.name === 'aura_basetools_write') && tools.some((t) => t.name === 'aura_basetools_undo') && tools.some((t) => t.name === 'aura_basetools_transactions'), `tools/list 五个工具(${tools.map((t) => t.name).join(',')}`)
388
+ check(tools.length === 3 && tools.some((t) => t.name === 'aura_basetools_read') && tools.some((t) => t.name === 'aura_basetools_edit') && tools.some((t) => t.name === 'aura_basetools_write'), `tools/list 三个工具(${tools.map((t) => t.name).join(',')}`)
389
389
  const readEnv = JSON.parse(byId.get(3)?.result?.content?.[0]?.text ?? '{}')
390
390
  check(readEnv.ok === true && Array.isArray(readEnv.symbols ?? []), 'aura_basetools_read 经 MCP 可用', JSON.stringify(readEnv).slice(0, 120))
391
391
  const editEnv = JSON.parse(byId.get(4)?.result?.content?.[0]?.text ?? '{}')
@@ -1537,49 +1537,9 @@ say('N provider:mode / symlink / hardlink / range / 上限 / cache')
1537
1537
  }
1538
1538
  }
1539
1539
 
1540
- say('O 写事务:journal / undo / 多目标回滚')
1540
+ say('O 写失败回滚与锁(0.14.0 已移除公开 undo/transactions)')
1541
1541
  {
1542
- const txFile = path.join(tmp, 'tx-single.txt')
1543
- const txOriginal = 'one\ntwo\n'
1544
- fs.writeFileSync(txFile, txOriginal)
1545
- const txEdit = json(run(['basetools', 'edit', '--file', txFile, '--lines', '1', '--replace', 'ONE', '--transaction']))
1546
- check(txEdit.ok === true && typeof txEdit.result?.transaction?.id === 'string', 'O1 单目标编辑返回 transaction id', JSON.stringify(txEdit.result?.transaction ?? txEdit.code))
1547
- check(fs.readFileSync(txFile, 'utf8') === 'ONE\ntwo\n', 'O1 编辑已落盘')
1548
- const txId = txEdit.result?.transaction?.id
1549
- const txUndo = json(run(['basetools', 'undo', '--id', String(txId)]))
1550
- check(txUndo.ok === true && txUndo.status === 'undone', 'O2 undo 成功', JSON.stringify(txUndo))
1551
- check(fs.readFileSync(txFile, 'utf8') === txOriginal, 'O2 undo 恢复原始字节')
1552
-
1553
- const a = path.join(tmp, 'tx-a.txt')
1554
- const b = path.join(tmp, 'tx-b.txt')
1555
- fs.writeFileSync(a, 'alpha\n')
1556
- fs.writeFileSync(b, 'beta\n')
1557
- const targets = [
1558
- { file_path: a, hunks: [{ lines: [1, 1], replace: 'ALPHA' }] },
1559
- { file_path: b, hunks: [{ lines: [1, 1], replace: 'BETA' }] },
1560
- ]
1561
- const multi = json(run(['basetools', 'edit', '--targets', JSON.stringify(targets), '--transaction']))
1562
- check(multi.ok === true && typeof multi.transaction?.id === 'string', 'O3 多目标编辑返回 transaction id', JSON.stringify(multi.transaction ?? multi.code))
1563
- check(fs.readFileSync(a, 'utf8') === 'ALPHA\n' && fs.readFileSync(b, 'utf8') === 'BETA\n', 'O3 两个文件都已落盘')
1564
- const multiUndo = json(run(['basetools', 'undo', '--id', String(multi.transaction?.id)]))
1565
- check(multiUndo.ok === true && multiUndo.restored === 2, 'O4 多目标 undo 恢复两个文件', JSON.stringify(multiUndo))
1566
- check(fs.readFileSync(a, 'utf8') === 'alpha\n' && fs.readFileSync(b, 'utf8') === 'beta\n', 'O4 两个文件都回到原始字节')
1567
-
1568
- const list = json(run(['basetools', 'transactions']))
1569
- check(list.ok === true && Array.isArray(list.transactions) && list.transactions.length > 0, 'O5 transactions 列出当前工作区事务')
1570
-
1571
- // 事务后又被改过 ⇒ 默认拒绝 undo,force 才覆盖
1572
- const guardFile = path.join(tmp, 'tx-guard.txt')
1573
- fs.writeFileSync(guardFile, 'v0\n')
1574
- const guardEdit = json(run(['basetools', 'edit', '--file', guardFile, '--lines', '1', '--replace', 'v1', '--transaction']))
1575
- const guardId = guardEdit.result?.transaction?.id
1576
- fs.writeFileSync(guardFile, 'external\n')
1577
- const blocked = json(run(['basetools', 'undo', '--id', String(guardId)]))
1578
- check(blocked.ok === false && blocked.code === 'TRANSACTION_STALE', 'O6 事务后文件被外部改过 ⇒ undo 默认拒绝', JSON.stringify({ code: blocked.code, error: blocked.error }))
1579
- const forced = json(run(['basetools', 'undo', '--id', String(guardId), '--force']))
1580
- check(forced.ok === true && fs.readFileSync(guardFile, 'utf8') === 'v0\n', 'O6 force 后仍可恢复')
1581
-
1582
- // O7 多目标:一个成功、一个写失败 ⇒ 成功那个也必须回滚
1542
+ // O7 多目标:一个成功、一个写失败 ⇒ 成功那个也必须回滚(内部事务日志仍保留)
1583
1543
  const okFile = path.join(tmp, 'tx-rollback-ok.txt')
1584
1544
  const hardBase = path.join(tmp, 'tx-rollback-hard-base.txt')
1585
1545
  const hardFile = path.join(tmp, 'tx-rollback-hard.txt')
@@ -1594,31 +1554,13 @@ say('O 写事务:journal / undo / 多目标回滚')
1594
1554
  { file_path: okFile, hunks: [{ lines: [1, 1], replace: 'ALPHA' }] },
1595
1555
  { file_path: hardFile, hunks: [{ lines: [1, 1], replace: 'BETA' }] },
1596
1556
  ]
1597
- const rollback = json(run(['basetools', 'edit', '--targets', JSON.stringify(rollbackTargets), '--transaction']))
1598
- check(rollback.ok === false && rollback.transaction?.status === 'rolled_back', 'O7 多目标部分失败 ⇒ transaction rolled_back', JSON.stringify({ ok: rollback.ok, tx: rollback.transaction, rollback: rollback.rollback }))
1557
+ const rollback = json(run(['basetools', 'edit', '--targets', JSON.stringify(rollbackTargets)]))
1558
+ check(rollback.ok === false && rollback.rollback?.status === 'rolled_back' && rollback.rolled_back === true, 'O7 多目标部分失败 ⇒ 自动回滚', JSON.stringify({ ok: rollback.ok, rollback: rollback.rollback }))
1599
1559
  check(rollback.rollback?.restored === 1 && fs.readFileSync(okFile, 'utf8') === 'alpha\n', 'O7 已落地的那个文件被恢复')
1600
1560
  check(fs.readFileSync(hardBase, 'utf8') === 'beta\n' && fs.readFileSync(hardFile, 'utf8') === 'beta\n', 'O7 硬链接目标未被改动')
1601
- // 回执必须等于**回滚后的最终磁盘状态**,不能继续报 landed/applied。
1602
1561
  check(rollback.applied === 0 && rollback.landed_paths?.length === 0 && rollback.unchanged === true, 'O7 回滚后 applied=0 / landed_paths=[] / unchanged=true', JSON.stringify({ applied: rollback.applied, landed_paths: rollback.landed_paths, unchanged: rollback.unchanged }))
1603
1562
  check(rollback.rolled_back === true && rollback.edits?.filter((e) => e.outcome === 'rolled_back').every((e) => e.landed === false), 'O7 逐项 outcome=rolled_back 且 landed=false', JSON.stringify(rollback.edits?.map((e) => ({ outcome: e.outcome, landed: e.landed }))))
1604
1563
  check(!/applied [1-9]|landed [1-9]/.test(rollback.summary ?? ''), 'O7 汇总行不再出现 applied N / landed N', rollback.summary)
1605
-
1606
- // 不带 --transaction 也必须能看出“已回滚”:回执事实不依赖是否索要事务 id。
1607
- // 用一组**新文件**:上一轮回滚后观测账本里还留着“写下去的那版”,同一文件立刻重试会先被
1608
- // 区域闸拦下(那是另一条保护,别把它混进回执断言)。
1609
- const okFile2 = path.join(tmp, 'tx-rollback-ok2.txt')
1610
- const hardBase2 = path.join(tmp, 'tx-rollback-hard-base2.txt')
1611
- const hardFile2 = path.join(tmp, 'tx-rollback-hard2.txt')
1612
- fs.writeFileSync(okFile2, 'alpha\n')
1613
- fs.writeFileSync(hardBase2, 'beta\n')
1614
- fs.linkSync(hardBase2, hardFile2)
1615
- const rollbackTargets2 = [
1616
- { file_path: okFile2, hunks: [{ lines: [1, 1], replace: 'ALPHA' }] },
1617
- { file_path: hardFile2, hunks: [{ lines: [1, 1], replace: 'BETA' }] },
1618
- ]
1619
- const rollbackNoTx = json(run(['basetools', 'edit', '--targets', JSON.stringify(rollbackTargets2)]))
1620
- check(rollbackNoTx.ok === false && rollbackNoTx.rollback?.status === 'rolled_back' && rollbackNoTx.rolled_back === true, 'O7 include_transaction=false 仍返回 rollback/rolled_back', JSON.stringify({ rollback: rollbackNoTx.rollback, rolled_back: rollbackNoTx.rolled_back }))
1621
- check(rollbackNoTx.applied === 0 && rollbackNoTx.landed_paths?.length === 0 && fs.readFileSync(okFile2, 'utf8') === 'alpha\n', 'O7 不索要事务 id 时回执仍与最终磁盘一致', JSON.stringify({ applied: rollbackNoTx.applied, landed_paths: rollbackNoTx.landed_paths }))
1622
1564
  }
1623
1565
 
1624
1566
  // O8 锁:同一工作区已有写事务时,第二个编辑必须超时拒绝
@@ -4,7 +4,7 @@
4
4
  * 覆盖:
5
5
  * A 新建:真正建盘、回执 operation=create、dry-run 不建盘、事务可撤销(撤销后文件消失)
6
6
  * B 覆写:整份替换、BOM 与文件既有行尾保留、有无尾换行精确、no-overwrite、dry-run、
7
- * 事务 + undo 逐字节恢复、`--content @file`
7
+ * `--content @file`(0.14.0 移除公开事务/undo)
8
8
  * C MCP:aura_basetools_write 出现在 tools/list,create/overwrite/no_overwrite/dry_run/坏参数都走同一引擎
9
9
  * D 能力清单:commands 与 tools 都包含 write
10
10
  *
@@ -55,7 +55,7 @@ process.on('exit', () => {
55
55
  })
56
56
 
57
57
  // ── A 新建 ────────────────────────────────────────────────────────────────
58
- say('A 新建:整份建盘,dry-run 不建盘,事务可撤销')
58
+ say('A 新建:整份建盘,dry-run 不建盘')
59
59
  {
60
60
  const created = json(run(['basetools', 'write', '--file', 'created.txt', '--content', 'alpha\nbeta\n']))
61
61
  check(created.ok === true && created.result?.operation === 'create', 'A1 新建回执 operation=create', JSON.stringify(created).slice(0, 160))
@@ -66,13 +66,6 @@ say('A 新建:整份建盘,dry-run 不建盘,事务可撤销')
66
66
  check(dry.ok === true && dry.result?.operation === 'create' && dry.result?.dryRun === true && dry.result?.bytes === 1, 'A4 新建 dry-run 回执给 create/bytes,但不落盘', JSON.stringify(dry).slice(0, 160))
67
67
  check(fs.existsSync(file('dry-new.txt')) === false, 'A5 新建 dry-run 之后文件仍不存在')
68
68
 
69
- const tx = json(run(['basetools', 'write', '--file', 'tx-created.txt', '--content', 'gone\n', '--transaction']))
70
- const txId = tx.result?.transaction?.id
71
- check(tx.ok === true && typeof txId === 'string' && tx.result.transaction.status === 'committed', 'A6 --transaction 返回 committed 事务', JSON.stringify(tx.result?.transaction))
72
- const undone = json(run(['basetools', 'undo', '--id', txId]))
73
- check(undone.ok === true && undone.status === 'undone', 'A7 undo 成功', JSON.stringify(undone).slice(0, 160))
74
- check(fs.existsSync(file('tx-created.txt')) === false, 'A8 撤销“新建”后文件消失(不留下空文件)')
75
-
76
69
  const missingFile = json(run(['basetools', 'write', '--content', 'x']))
77
70
  const missingContent = json(run(['basetools', 'write', '--file', 'x.txt']))
78
71
  check(missingFile.ok === false && missingFile.code === 'USAGE', 'A9 缺 --file ⇒ USAGE', JSON.stringify(missingFile).slice(0, 140))
@@ -119,14 +112,6 @@ say('B 覆写:整份替换,BOM / 既有行尾 / 尾换行精确')
119
112
  const newNoOverwrite = json(run(['basetools', 'write', '--file', 'brand-new.txt', '--content', 'ok\n', '--no-overwrite']))
120
113
  check(newNoOverwrite.ok === true, 'B11 no-overwrite 对不存在的文件照常创建')
121
114
 
122
- fs.writeFileSync(file('tx-restore.txt'), Buffer.concat([BOM, Buffer.from('orig\r\nbytes\r\n', 'utf8')]))
123
- prime('tx-restore.txt')
124
- const original = fs.readFileSync(file('tx-restore.txt'))
125
- const tx = json(run(['basetools', 'write', '--file', 'tx-restore.txt', '--content', 'changed\n', '--transaction']))
126
- check(!fs.readFileSync(file('tx-restore.txt')).equals(original), 'B12 覆写真的改了盘')
127
- const undone = json(run(['basetools', 'undo', '--id', tx.result?.transaction?.id]))
128
- check(undone.ok === true && fs.readFileSync(file('tx-restore.txt')).equals(original), 'B13 undo 逐字节恢复覆写前的原文(含 BOM/CRLF)')
129
-
130
115
  fs.writeFileSync(file('content-source.txt'), 'from @file\nsecond line\n')
131
116
  const atFile = json(run(['basetools', 'write', '--file', 'from-at.txt', '--content', '@' + file('content-source.txt')]))
132
117
  check(atFile.ok === true && fs.readFileSync(file('from-at.txt'), 'utf8') === fs.readFileSync(file('content-source.txt'), 'utf8'), 'B14 --content @file 读入整份内容')
@@ -225,7 +210,7 @@ say('C MCP:aura_basetools_write 声明面与调用面')
225
210
 
226
211
  const tools = byId.get(2)?.result?.tools ?? []
227
212
  const writeTool = tools.find((tool) => tool.name === 'aura_basetools_write')
228
- check(tools.length === 5 && writeTool !== undefined, `C1 tools/list 五个工具且含 aura_basetools_write(${tools.map((tool) => tool.name).join(',')})`)
213
+ check(tools.length === 3 && writeTool !== undefined, `C1 tools/list 三个工具且含 aura_basetools_write(${tools.map((tool) => tool.name).join(',')})`)
229
214
  check(JSON.stringify(writeTool?.inputSchema?.required) === JSON.stringify(['file_path', 'content']), 'C2 aura_basetools_write schema 必填 file_path + content', JSON.stringify(writeTool?.inputSchema?.required))
230
215
  check(envOf(3).ok === true && envOf(3).result?.operation === 'create', 'C3 经 MCP 新建成功', JSON.stringify(envOf(3)).slice(0, 160))
231
216
  check(fs.readFileSync(file('mcp-created.txt'), 'utf8') === 'mcp\ncreate\n', 'C4 MCP 新建的内容真的落盘')