@thincoder/core 0.9.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.
Files changed (203) hide show
  1. package/abort-provenance.mjs +116 -0
  2. package/advisor/citations.mjs +139 -0
  3. package/advisor/compaction.mjs +174 -0
  4. package/advisor/convergence.mjs +80 -0
  5. package/advisor/history.mjs +77 -0
  6. package/advisor/loop.mjs +288 -0
  7. package/advisor/messages.mjs +299 -0
  8. package/advisor/notice.mjs +141 -0
  9. package/advisor/project-context.mjs +197 -0
  10. package/advisor/repos.mjs +150 -0
  11. package/advisor/run.mjs +190 -0
  12. package/advisor/truncate.mjs +57 -0
  13. package/advisor.mjs +281 -0
  14. package/agent/child-marks.mjs +24 -0
  15. package/agent/completion.mjs +145 -0
  16. package/agent/dispatch.mjs +493 -0
  17. package/agent/family-tools.mjs +174 -0
  18. package/agent/helpers.mjs +412 -0
  19. package/agent/post-turn.mjs +70 -0
  20. package/agent/record-results.mjs +174 -0
  21. package/agent/relay-prefix.mjs +39 -0
  22. package/agent/run-stages.mjs +244 -0
  23. package/agent/setup-reminders.mjs +199 -0
  24. package/agent/setup.mjs +234 -0
  25. package/agent/spawn-child.mjs +258 -0
  26. package/agent/suspension.mjs +240 -0
  27. package/agent/write-gate.mjs +87 -0
  28. package/agent-tools/advisor-async.mjs +481 -0
  29. package/agent-tools/advisor-settle.mjs +240 -0
  30. package/agent-tools/advisor.mjs +280 -0
  31. package/agent-tools/async-discard.mjs +143 -0
  32. package/agent-tools/async-settle.mjs +299 -0
  33. package/agent-tools/batch-segment.mjs +265 -0
  34. package/agent-tools/child-permission.mjs +45 -0
  35. package/agent-tools/consult.mjs +471 -0
  36. package/agent-tools/design-token.mjs +117 -0
  37. package/agent-tools/digest-budget.mjs +76 -0
  38. package/agent-tools/eng.mjs +102 -0
  39. package/agent-tools/escalate-async.mjs +302 -0
  40. package/agent-tools/goal.mjs +119 -0
  41. package/agent-tools/panel-blocks.mjs +24 -0
  42. package/agent-tools/parent-channel.mjs +231 -0
  43. package/agent-tools/plan.mjs +86 -0
  44. package/agent-tools/read-history.mjs +309 -0
  45. package/agent-tools/recent-changes.mjs +24 -0
  46. package/agent-tools/review-facts.mjs +31 -0
  47. package/agent-tools/settings.mjs +268 -0
  48. package/agent-tools/skill.mjs +63 -0
  49. package/agent-tools/spawn-gates.mjs +109 -0
  50. package/agent-tools/subagent-actions.mjs +496 -0
  51. package/agent-tools/subagent-async.mjs +456 -0
  52. package/agent-tools/subagent-panel.mjs +160 -0
  53. package/agent-tools/subagent-run.mjs +208 -0
  54. package/agent-tools/subagent-scheduler.mjs +446 -0
  55. package/agent-tools/subagent-spawn.mjs +478 -0
  56. package/agent-tools/subagent.mjs +419 -0
  57. package/agent-tools/task.mjs +87 -0
  58. package/agent-tools/timer.mjs +46 -0
  59. package/agent-tools/verify.mjs +295 -0
  60. package/agent-tools.mjs +23 -0
  61. package/agent.mjs +430 -0
  62. package/auto-think.mjs +115 -0
  63. package/compress-form.mjs +24 -0
  64. package/config-io.mjs +277 -0
  65. package/config-migrate.mjs +178 -0
  66. package/config-presets.mjs +49 -0
  67. package/config.mjs +419 -0
  68. package/context.mjs +495 -0
  69. package/conventions.mjs +223 -0
  70. package/embedding.mjs +120 -0
  71. package/escape.mjs +152 -0
  72. package/expand-home.mjs +16 -0
  73. package/explore-distill.mjs +152 -0
  74. package/generate-title.mjs +123 -0
  75. package/git/checkpoint.mjs +448 -0
  76. package/git/gitmem.mjs +100 -0
  77. package/history-window.mjs +179 -0
  78. package/hooks.mjs +108 -0
  79. package/i18n.mjs +106 -0
  80. package/index-bin.mjs +48 -0
  81. package/index-discover.mjs +176 -0
  82. package/ledger-cmd.mjs +209 -0
  83. package/ledger-db.mjs +86 -0
  84. package/ledger-surface.mjs +76 -0
  85. package/ledger.mjs +202 -0
  86. package/log.mjs +195 -0
  87. package/manifest.mjs +338 -0
  88. package/markdown.mjs +106 -0
  89. package/mcp/helpers.mjs +51 -0
  90. package/mcp/transport-http.mjs +248 -0
  91. package/mcp/transport-stdio.mjs +140 -0
  92. package/mcp/transport-ws.mjs +122 -0
  93. package/mcp.mjs +295 -0
  94. package/memory/code-index.mjs +219 -0
  95. package/memory/code-sync.mjs +427 -0
  96. package/memory/core.mjs +318 -0
  97. package/memory/delete.mjs +242 -0
  98. package/memory/docs.mjs +431 -0
  99. package/memory/file-walk.mjs +109 -0
  100. package/memory/origin.mjs +24 -0
  101. package/memory/scan.mjs +177 -0
  102. package/memory/schema.mjs +460 -0
  103. package/memory.mjs +21 -0
  104. package/model-ref.mjs +66 -0
  105. package/model-specs.mjs +277 -0
  106. package/package.json +32 -0
  107. package/peer-domains.mjs +265 -0
  108. package/peer-instances.mjs +178 -0
  109. package/permission.mjs +79 -0
  110. package/process-probe.mjs +315 -0
  111. package/prompt-files.mjs +113 -0
  112. package/prompt-overlays.mjs +78 -0
  113. package/prompts/advisor-design.md +43 -0
  114. package/prompts/advisor-round1.md +41 -0
  115. package/prompts/advisor-round2.md +46 -0
  116. package/prompts/advisor-round3.md +42 -0
  117. package/prompts/common.md +158 -0
  118. package/prompts/consult-base.md +19 -0
  119. package/prompts/discipline-engineering.md +123 -0
  120. package/prompts/discipline-normal.md +206 -0
  121. package/prompts/persona-coder.md +21 -0
  122. package/prompts/persona-eng-coder.md +41 -0
  123. package/prompts/persona-eng-designer.md +80 -0
  124. package/prompts/persona-engineering.md +160 -0
  125. package/prompts/persona-explore.md +15 -0
  126. package/prompts/persona-normal.md +35 -0
  127. package/prompts/persona-plan.md +27 -0
  128. package/provider/anthropic.mjs +225 -0
  129. package/provider/core.mjs +491 -0
  130. package/provider/errors.mjs +101 -0
  131. package/provider/google.mjs +257 -0
  132. package/provider/index.mjs +7 -0
  133. package/provider/list-models.mjs +163 -0
  134. package/provider/normalize.mjs +81 -0
  135. package/provider/rate.mjs +168 -0
  136. package/provider/responses.mjs +495 -0
  137. package/provider/retry.mjs +88 -0
  138. package/provider/sse.mjs +264 -0
  139. package/provider/wait-status.mjs +59 -0
  140. package/proxy.mjs +274 -0
  141. package/rules.mjs +53 -0
  142. package/session-gc.mjs +248 -0
  143. package/session-guard.mjs +59 -0
  144. package/session-lifecycle.mjs +305 -0
  145. package/session-migrate.mjs +48 -0
  146. package/session-rename.mjs +38 -0
  147. package/session-segments.mjs +99 -0
  148. package/session-slot-write.mjs +168 -0
  149. package/session-slots-manifest.mjs +264 -0
  150. package/session-slots.mjs +298 -0
  151. package/session-store.mjs +441 -0
  152. package/session.mjs +244 -0
  153. package/skills.mjs +234 -0
  154. package/text-budget.mjs +79 -0
  155. package/token-ttl.mjs +285 -0
  156. package/tool-docs/apply_patch.md +15 -0
  157. package/tool-docs/bash.md +38 -0
  158. package/tool-docs/delete.md +13 -0
  159. package/tool-docs/edit.md +30 -0
  160. package/tool-docs/execute.md +21 -0
  161. package/tool-docs/fetch.md +12 -0
  162. package/tool-docs/file_ops.md +17 -0
  163. package/tool-docs/get_current_time.md +8 -0
  164. package/tool-docs/git.md +54 -0
  165. package/tool-docs/glob.md +11 -0
  166. package/tool-docs/grep.md +19 -0
  167. package/tool-docs/hashline_edit.md +14 -0
  168. package/tool-docs/insert_after.md +15 -0
  169. package/tool-docs/lint.md +10 -0
  170. package/tool-docs/ls.md +12 -0
  171. package/tool-docs/lsp.md +10 -0
  172. package/tool-docs/process.md +10 -0
  173. package/tool-docs/question.md +16 -0
  174. package/tool-docs/read.md +20 -0
  175. package/tool-docs/read_image.md +8 -0
  176. package/tool-docs/tree.md +14 -0
  177. package/tool-docs/wait_for.md +22 -0
  178. package/tool-docs/websearch.md +16 -0
  179. package/tool-docs/write.md +11 -0
  180. package/tools/bash.mjs +276 -0
  181. package/tools/edit-batch.mjs +204 -0
  182. package/tools/edit-diff.mjs +388 -0
  183. package/tools/exec-run.mjs +43 -0
  184. package/tools/execute.mjs +243 -0
  185. package/tools/file.mjs +464 -0
  186. package/tools/git-checkpoint.mjs +143 -0
  187. package/tools/git-ext.mjs +173 -0
  188. package/tools/git.mjs +415 -0
  189. package/tools/glob-dialect.mjs +130 -0
  190. package/tools/index.mjs +76 -0
  191. package/tools/linter.mjs +120 -0
  192. package/tools/lsp.mjs +335 -0
  193. package/tools/ops.mjs +293 -0
  194. package/tools/patch.mjs +290 -0
  195. package/tools/question.mjs +26 -0
  196. package/tools/repomap.mjs +314 -0
  197. package/tools/search.mjs +248 -0
  198. package/tools/shared.mjs +467 -0
  199. package/tools/tree.mjs +81 -0
  200. package/tools/web.mjs +224 -0
  201. package/tools/write-path.mjs +191 -0
  202. package/traces/trace-store.mjs +303 -0
  203. package/undo-stack.mjs +47 -0
@@ -0,0 +1,388 @@
1
+ /**
2
+ * edit-diff.mjs — edit 工具的行级 diff 内核(权威语义 = EDIT.md——2026-09-04 拆出)。
3
+ *
4
+ * edit 语义(权威 = EDIT.md §2-§5——2026-09-08 语义升级 D1/D2/D3 已并入):两种定位形态(互斥)——
5
+ * ① 按行号改(D1/F1):line(单行)/ startLine+endLine(1-based 闭区间)→ 直接替换该
6
+ * 行/行范围为 new_string(无需 old_string——实现落点 edit-batch.mjs applyLineEdit);
7
+ * 阶段 2 删行形态(EDIT.md §5——约束):省略 new_string = 删除该行/范围(有界意图);
8
+ * ② 内容定位:old_string = 变化区**当前内容**(单次匹配——匹配档位:逐字 → P15.11 唯一
9
+ * 空白差异窗口 → D2/F2 模糊匹配(行级 normalize 后 ≥90% 行相等——edit-batch.mjs
10
+ * findFuzzyWindow));new_string = 该区的**期望结果**。判定序:
11
+ * 0. 分支 0(EDIT.md §4——单行精确替换):old 单行 && new 单行 && old 全文唯一 &&
12
+ * new 非空 → **就地替换该行**(行数不变——EOL 由调用方 joinWithEol 恢复);
13
+ * 1. 零重叠(old 每一行都不出现在 new 行集中)→ **替换即删**(D3/F3,2026-09-08——
14
+ * breaking:old 行整体删除、new 取而代之——旧行不再保留;新增行用 insert_after);
15
+ * 2. 有公共行(≥1)→ 行级 LCS——公共行保留、差异行增删;
16
+ * 3. new 与 old 行级完全一致 → 原样替换(no-op 语义——仍报成功)。
17
+ * 内容形态空 new_string(纯删除意图)→ 显式报错(不静默——先于分支 0——单行替换永不
18
+ * 成删除);行号形态显式空串 → 同样显式错(防误删——模板落空 ≠ 删行意图;EDIT.md
19
+ * §5 空串 vs 省略矩阵);**省略 new_string = 删行/范围**(阶段 2 命名删行形态);
20
+ * 分支 0 只在 computeEditEntry 条目判定层(壳/桥/批量自动继承)——
21
+ * applyPatchLines(纯 diff 层)语义不动。old/new 行数各上限 1000(超限报错)。
22
+ *
23
+ * 行尾权威 = EDIT-TOOL-EOL-DESIGN.md:判定/应用在 normalizeEOL 后的 LF 域计算,
24
+ * 写回由调用方 joinWithEol(原文) 恢复原行尾。
25
+ * 模块拆分(file.mjs ≤500 硬限):file.mjs 只留工具壳与转发——单形态执行体与前置校验
26
+ * 分支整段迁出至本模块(模块拆分写优先纪律:先迁后删、逻辑体不变、wiring 导入)。
27
+ * edit-diff.mjs ↔ edit-batch.mjs 循环引用(2026-09-08——D1/D2 实现落点 edit-batch):
28
+ * 两侧仅函数声明(提升初始化)、仅调用期使用——ESM 循环下安全(同 file.mjs 先例)。
29
+ */
30
+ import { readFile } from "node:fs/promises"
31
+ import {
32
+ resolveInCwd, normalizeEOL, joinWithEol, gitDiffOne, autoSyntaxCheck, findCandidates,
33
+ } from "./shared.mjs"
34
+ // file.mjs ↔ edit-diff.mjs 循环引用:两侧导入的都是函数声明(提升初始化),
35
+ // 仅在调用期使用——ESM 循环下安全(无模块求值期取值)。
36
+ // 写盘经单一写路径点(§2.13.5)——本档不再直调 fs。
37
+ import { appendWriteContext } from "./file.mjs"
38
+ import { writeThroughPath, lastWriteOf, isDirty } from "./write-path.mjs"
39
+ // D1/D2 落点(EDIT.md §6——edit-batch.mjs):按行号改 + 模糊匹配纯函数。
40
+ import { applyLineEdit, findFuzzyWindow, FUZZY_MATCH_NOTE } from "./edit-batch.mjs"
41
+
42
+ export const MAX_DIFF_LINES = 1000
43
+ export const REGION_TOO_LARGE = "edit region too large — narrow the change"
44
+ export const EMPTY_NEW_STRING = "empty new_string — for deletion, keep the context lines you want to preserve in both old_string and new_string"
45
+ export const EMPTY_NEW_STRING_LINE = "empty new_string with line-based targeting is an explicit error — OMIT new_string to delete the line/range (deleting by number is explicit intent; an empty replacement is a mistake, not a delete)"
46
+ export const EDIT_ARGS_MUTEX = "edits array is mutually exclusive with top-level old_string/new_string/line/startLine/endLine — a top-level path is allowed (default for entries without their own path); provide each change's targeting (old_string or line range) and new_string inside its edits entry"
47
+
48
+ /** D1(2026-09-08):条目带按行号改参数(line / startLine / endLine 任一)。 */
49
+ export function hasLineParams(entry) {
50
+ return entry.line !== undefined || entry.startLine !== undefined || entry.endLine !== undefined
51
+ }
52
+
53
+ /** 删行结果文本(EDIT.md——删行返回文本;out.deleted 才调用):单行 "line N" / 范围 "lines N-M"。
54
+ * 删行 lineShift = -(范围行数)——end = start - 1 - lineShift。 */
55
+ export function deleteTarget(out) {
56
+ const start = out.editStartLine
57
+ const end = start - 1 - out.lineShift
58
+ return start === end ? `line ${start}` : `lines ${start}-${end}`
59
+ }
60
+
61
+ /** 行切分(尾随换行终止最后一行——非额外空行):"a\nb\n" → ["a","b"]。
62
+ * 2026-09-08 导出(D1 落点 edit-batch.mjs applyLineEdit 复用同语义)。 */
63
+ export function splitLines(text) {
64
+ const lines = text.split("\n")
65
+ if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop()
66
+ return lines
67
+ }
68
+
69
+ /**
70
+ * 行级 LCS(整行相等判定)合并:公共行保留(LCS 序)、old 独有行删除、new 独有行按其在
71
+ * new 中相对公共行的位置插入;**零重叠 → 替换即删**(D3,2026-09-08 语义升级——
72
+ * breaking:old 行整体删除、new 取而代之,旧行不再保留——原"零重叠→插入保留旧行"
73
+ * 语义废止;新增行用 insert_after)。结果在 LF 域;区域尾随换行随 oldText(区域边界保持)。
74
+ * 返回 { ok: true, resultText } 或 { ok: false, reason }。
75
+ */
76
+ export function applyPatchLines(oldText, newText) {
77
+ if (newText === "") return { ok: false, reason: EMPTY_NEW_STRING }
78
+ const oldLines = splitLines(oldText)
79
+ const newLines = splitLines(newText)
80
+ if (oldLines.length > MAX_DIFF_LINES || newLines.length > MAX_DIFF_LINES) {
81
+ return { ok: false, reason: REGION_TOO_LARGE }
82
+ }
83
+ // 判定 1(D3——替换即删):零重叠 → old 行删、new 整体取代(旧行不再保留)
84
+ const newSet = new Set(newLines)
85
+ const merged = oldLines.some((l) => newSet.has(l))
86
+ ? lcsMerge(oldLines, newLines) // 判定 2/3——公共行保留、差异行增删
87
+ : [...newLines]
88
+ return { ok: true, resultText: merged.join("\n") + (oldText.endsWith("\n") ? "\n" : "") }
89
+ }
90
+
91
+ /** LCS 推导(DP + 回溯)——整行相等判定;结果 = 公共行 + 差异行增删的合并序列。 */
92
+ function lcsMerge(a, b) {
93
+ const n = a.length
94
+ const m = b.length
95
+ const width = m + 1
96
+ const dp = new Uint16Array((n + 1) * width)
97
+ for (let i = 1; i <= n; i++) {
98
+ for (let j = 1; j <= m; j++) {
99
+ dp[i * width + j] = a[i - 1] === b[j - 1]
100
+ ? dp[(i - 1) * width + (j - 1)] + 1
101
+ : Math.max(dp[(i - 1) * width + j], dp[i * width + (j - 1)])
102
+ }
103
+ }
104
+ const merged = []
105
+ let i = n
106
+ let j = m
107
+ while (i > 0 && j > 0) {
108
+ if (a[i - 1] === b[j - 1]) {
109
+ merged.push(a[i - 1])
110
+ i--
111
+ j--
112
+ } else if (dp[(i - 1) * width + j] >= dp[i * width + (j - 1)]) {
113
+ i-- // old 独有行——删除
114
+ } else {
115
+ merged.push(b[j - 1]) // new 独有行——按 LCS 相对位置插入
116
+ j--
117
+ }
118
+ }
119
+ while (j > 0) { merged.push(b[j - 1]); j-- }
120
+ return merged.reverse()
121
+ }
122
+
123
+ /**
124
+ * (2026-09-05 用户裁定——EDIT.md §5——顶层 path + edits 并存合法化:顶层 path = 无自带
125
+ * path 条目的默认——模型直觉形态「顶层 path + 数组条目」不再拒绝;条目自带 path 优先)。
126
+ * 互斥收窄为只对顶层 old_string/new_string(+ 2026-09-08 D1 的 line/startLine/endLine)——
127
+ * edits 下它们无批语义可解释——顶层 path 不再触发。
128
+ */
129
+ export function assertEditArgsExclusive(args) {
130
+ if (args.old_string !== undefined || args.new_string !== undefined || hasLineParams(args)) {
131
+ throw new Error(EDIT_ARGS_MUTEX)
132
+ }
133
+ }
134
+
135
+ /**
136
+ * 前置校验——内容形态:空 old / 非字符串 new;按行号改形态(D1,2026-09-08;阶段 2
137
+ * 删行——EDIT.md §5):与 old_string 互斥 / line 与 startLine|endLine 互斥 /
138
+ * startLine+endLine 须成对 / 正整数 / endLine ≥ startLine / replace_all 不适用 /
139
+ * new_string:省略(undefined)= 删行 / 显式空串 = 显式错(防误删)/ 其他类型报错。
140
+ * error 文本按调用形态(单形态 rich / 批量 label)。
141
+ * opts: { label = ""(批量前缀 "edit for <path>: "), rich = true(单形态完整文本) }
142
+ */
143
+ export function validateEditEntry(entry, opts = {}) {
144
+ const label = opts.label ?? ""
145
+ if (hasLineParams(entry)) {
146
+ if (entry.old_string !== undefined) {
147
+ throw new Error(label + "line/startLine/endLine are mutually exclusive with old_string — target by line number OR by content, not both")
148
+ }
149
+ if (entry.line !== undefined && (entry.startLine !== undefined || entry.endLine !== undefined)) {
150
+ throw new Error(label + "line is mutually exclusive with startLine/endLine — single line or a range, not both")
151
+ }
152
+ if ((entry.startLine === undefined) !== (entry.endLine === undefined)) {
153
+ throw new Error(label + "startLine and endLine must be given together (1-based, inclusive)")
154
+ }
155
+ for (const k of ["line", "startLine", "endLine"]) {
156
+ if (entry[k] !== undefined && (!Number.isInteger(entry[k]) || entry[k] < 1)) {
157
+ throw new Error(label + `${k} must be a positive integer (1-based), got ${JSON.stringify(entry[k])}`)
158
+ }
159
+ }
160
+ if (entry.startLine !== undefined && entry.endLine < entry.startLine) {
161
+ throw new Error(label + `endLine (${entry.endLine}) is before startLine (${entry.startLine})`)
162
+ }
163
+ if (entry.replace_all) {
164
+ throw new Error(label + "replace_all does not apply to line-based edits (a line range is already a single explicit target)")
165
+ }
166
+ if (entry.new_string === "") {
167
+ throw new Error(label + EMPTY_NEW_STRING_LINE + (opts.rich === false ? "" : " — nothing written"))
168
+ }
169
+ if (entry.new_string !== undefined && typeof entry.new_string !== "string") {
170
+ throw new Error(
171
+ label + `new_string must be a string (got ${typeof entry.new_string})` +
172
+ (opts.rich === false ? "" : " — nothing written")
173
+ )
174
+ }
175
+ return
176
+ }
177
+ if (!entry.old_string) {
178
+ throw new Error(
179
+ label + "old_string must not be empty" + (opts.rich === false ? "" : " (empty string matches everywhere and would corrupt the file)")
180
+ )
181
+ }
182
+ if (entry.new_string === "") {
183
+ // 空串 vs 省略矩阵(EDIT.md §5/§8.1):内容形态空串 = 显式错(无界意图防静默)——
184
+ // 先于 replace_all 分支(否则 replace_all + 空串会静默删除全部 occurrences——绕过保护)
185
+ throw new Error(label + EMPTY_NEW_STRING + (opts.rich === false ? "" : " — nothing written"))
186
+ }
187
+ if (typeof entry.new_string !== "string") {
188
+ throw new Error(
189
+ label + `new_string must be a string${entry.new_string === undefined ? " (missing)" : ` (got ${typeof entry.new_string})`}` +
190
+ (opts.rich === false ? "" : " — nothing written")
191
+ )
192
+ }
193
+ }
194
+
195
+ /**
196
+ * 分支 0 形态判定(EDIT.md §4):old/new 各为单行——无内部换行(splitLines
197
+ * 语义:尾随单个换行符是行终止而非新行——old/new 行内容均不含 \n)——且 new 非空
198
+ * ("" 经 splitLines 切分为 []——由下方 !== "" 守卫先拦截——空 new 不落本分支——落
199
+ * applyPatchLines 的空 new 显式错误——单行替换永不成删除)。多匹配与 replace_all 不落
200
+ * 本分支(occurrences 错误 / 字面全部先行)。
201
+ */
202
+ function isSingleLineReplace(entry) {
203
+ return (
204
+ entry.new_string !== "" &&
205
+ splitLines(entry.old_string).length === 1 &&
206
+ splitLines(entry.new_string).length === 1
207
+ )
208
+ }
209
+
210
+ /**
211
+ * 条目级判定+应用(单形态与批量共用——EDIT.md §6"批量条目判定+应用调用 edit-diff"):
212
+ * 匹配校验(精确存在——非 replace_all 单次)→ 按判定序应用(分支 0 单行替换 / 零重叠替换即删 /
213
+ * LCS 替换 / replace_all 字面)→
214
+ * 元数据(受影响区首行/行数差/次数——recordWrite 与结果回显用)。
215
+ * 返回 { updated, editStartLine, lineShift, occurrences, note?, deleted? };失败抛错(含路径/引导)。
216
+ * opts: { path, abortPrefix(批量 "edit aborted (atomic — no files written): ") }
217
+ */
218
+ /**
219
+ * P15.11(2026-09-05 用户裁定——edit 连续失败分析):old_string 逐字 not-found 时——
220
+ * 若文件中存在**唯一**窗口:行数与 old 相同、逐行 trim() 相等(内容零差异——差异仅前导/
221
+ * 尾随空白——EOL 已在 normalize 域消除)→ 返回 { actual }(actual = 文件窗口原文);
222
+ * 多窗口(两处 trim 同内容不同空白)→ null(歧义不猜——报错引导);old 含尾换行 → null
223
+ * (终止符语义边界——不做窗口猜测)。模型从 read 记忆拷贝 old_string 时丢/加前导空格是
224
+ * 纯机械损耗(本轮 12 次 edit 失败中 8 次空白差异)——内容零差异时自动落点 + 结果明示。
225
+ */
226
+ function findWhitespaceVariant(content, old) {
227
+ if (old.endsWith("\n")) return null
228
+ const oldLines = old.split("\n")
229
+ const fileLines = content.split("\n")
230
+ const m = oldLines.length
231
+ if (m === 0 || fileLines.length < m) return null
232
+ const trimmed = oldLines.map((l) => l.trim())
233
+ let hit = null
234
+ for (let i = 0; i + m <= fileLines.length; i++) {
235
+ let same = true
236
+ for (let j = 0; j < m; j++) {
237
+ if (fileLines[i + j].trim() !== trimmed[j]) { same = false; break }
238
+ }
239
+ if (!same) continue
240
+ const actual = fileLines.slice(i, i + m).join("\n")
241
+ if (actual === old) continue // 逐字已匹配——occurrences=0 前提下不会发生
242
+ if (hit) return null // 多窗口空白差异 → 歧义 → 不猜
243
+ hit = { actual }
244
+ }
245
+ return hit
246
+ }
247
+
248
+ /**
249
+ * P15.11 note 文案(各端成功消息追加——双端同句)
250
+ */
251
+ export const WHITESPACE_VARIANT_NOTE = "applied to the unique whitespace-only match (content identical, leading/trailing whitespace differs from your old_string)"
252
+
253
+ export function computeEditEntry(content, entry, opts = {}) {
254
+ validateEditEntry(entry, { ...opts, rich: !opts.abortPrefix })
255
+ // D1 按行号改(EDIT.md §2/§5):行号定位条目直接按行替换(applyLineEdit——落点
256
+ // edit-batch.mjs),不走 old_string 匹配/判定序;省略 new_string = 删行(EDIT.md §5);
257
+ // 互斥/越界/显式空串均在 validateEditEntry/applyLineEdit 显式报错。
258
+ if (hasLineParams(entry)) return applyLineEdit(content, entry, opts)
259
+ // P15.11:not-found 时先查唯一空白差异窗口——命中则以其原文为实际 old 继续(内容零差异
260
+ // ——自动落点 + note 明示);实质差异/歧义仍走下方 not-found 报错(不猜内容)。
261
+ let old = entry.old_string
262
+ let occurrences = content.split(old).length - 1
263
+ let note = null
264
+ if (occurrences === 0) {
265
+ const variant = findWhitespaceVariant(content, old)
266
+ if (variant) { old = variant.actual; occurrences = 1; note = WHITESPACE_VARIANT_NOTE }
267
+ }
268
+ // D2 模糊匹配(2026-09-08——P15.11 的推广):逐字/trim 等价都失败后,找唯一
269
+ // normalize 后 ≥90% 行相等的窗口——细微差异(缩进/引号/行尾空格/少量行差异)自动
270
+ // 落点 + note 明示;多窗口歧义/不足阈值仍走 not-found 报错(不猜内容)。
271
+ if (occurrences === 0) {
272
+ const fuzzy = findFuzzyWindow(content, old)
273
+ if (fuzzy) { old = fuzzy.actual; occurrences = 1; note = FUZZY_MATCH_NOTE }
274
+ }
275
+ if (occurrences === 0) {
276
+ const preview = entry.old_string.slice(0, 100).split("\n")[0]
277
+ const cands = findCandidates(content.split("\n"), entry.old_string)
278
+ let candText = ""
279
+ if (cands.length > 0) {
280
+ const header = entry.old_string.includes("\n")
281
+ ? ` similar lines (old_string line 1: "${entry.old_string.split("\n")[0].slice(0, 80)}"):`
282
+ : " similar lines:"
283
+ candText = "\n" + header + "\n" + cands.map((c) => ` L${c.line}: ${c.preview} (${Math.round(c.score * 100)}%)`).join("\n")
284
+ }
285
+ throw new Error(
286
+ `${opts.abortPrefix ?? ""}old_string not found in ${opts.path ?? "file"}\n` +
287
+ ` searched: "${preview}${entry.old_string.length > 100 ? "…" : ""}" — use grep to locate the actual content\n` +
288
+ (opts.absPath && lastWriteOf(opts.absPath)?.type === "write"
289
+ ? ` hints: this file was modified since your last read (write 全文重写后内容全变) — re-read it to refresh your copy of the content, then retry\n`
290
+ : opts.absPath && isDirty(opts.absPath)
291
+ ? ` hints: this file was modified since your last read (a prior write marked it dirty) — re-read it to refresh your copy of the content, then retry\n`
292
+ : ` hints: whitespace mismatch? file already changed? try reading the file first\n`) +
293
+ candText
294
+ )
295
+ }
296
+ if (occurrences > 1 && !entry.replace_all) {
297
+ throw new Error(
298
+ `${opts.abortPrefix ?? ""}old_string matches ${occurrences} times in ${opts.path ?? "file"}; provide more context or set replace_all`
299
+ )
300
+ }
301
+ const matchIdx = content.indexOf(old)
302
+ const editStartLine = matchIdx >= 0 ? content.slice(0, matchIdx).split("\n").length : 1
303
+ let updated
304
+ let resultForShift
305
+ if (entry.replace_all) {
306
+ // 字面替换——不做插入规则(old 多处时"插到哪处"无定义)——不落分支 0(EDIT.md §4)
307
+ updated = content.split(old).join(entry.new_string)
308
+ resultForShift = entry.new_string
309
+ } else if (isSingleLineReplace(entry)) {
310
+ // 分支 0(EDIT.md §4——单行精确替换):old 单行 && new 单行 && old 唯一匹配
311
+ // (occ==1 既有校验保证)&& new 非空(空 new 显式错误先于本分支——防删除)→ **就地
312
+ // 替换该行**——不再零重叠插入(P15.8——行尾段编辑反复踩的插入坑)。只替换行内容段:
313
+ // old/new 的尾随换行符属文件结构而非行内容——留在原位——行数不变——EOL 由调用方
314
+ // joinWithEol 恢复(normalizeEOL 后本域为 LF)。边界声明(评审 2026-09-04):new 为
315
+ // 行内容空串(如 new="\n"——置空行意图)时,常规带终止换行的行变空行(行数不变);
316
+ // 唯一例外是**无终止换行的末行**被置空——该行随文件结束自然消失(行数 −1——文件格式
317
+ // 固有边界——空行无法表达)——不落 EMPTY_NEW_STRING 错误路径(new 本身非空串)。
318
+ const oldLine = splitLines(old)[0]
319
+ const newLine = splitLines(entry.new_string)[0]
320
+ updated = content.slice(0, matchIdx) + newLine + content.slice(matchIdx + oldLine.length)
321
+ resultForShift = newLine
322
+ } else {
323
+ const r = applyPatchLines(old, entry.new_string)
324
+ if (!r.ok) throw new Error(r.reason)
325
+ updated = content.slice(0, matchIdx) + r.resultText + content.slice(matchIdx + old.length)
326
+ resultForShift = r.resultText
327
+ }
328
+ // 行数差 = 应用后区域行数 − 旧区域行数(分支 0 单行替换:0;零重叠替换即删:new−old;LCS:new−old)
329
+ const lineShift = splitLines(resultForShift).length - splitLines(old).length
330
+ return { updated, editStartLine, lineShift, occurrences: entry.replace_all ? occurrences : 1, note }
331
+ }
332
+
333
+ /**
334
+ * 单形态 edit 执行体(自 file.mjs 整段迁出——模块拆分写优先纪律:逻辑体不变)——
335
+ * file.mjs 只留工具壳与转发。
336
+ */
337
+ export async function runSingleEdit(args, ctx) {
338
+ const abs = resolveInCwd(ctx, args.path)
339
+ const raw = await readFile(abs, "utf8")
340
+ const content = normalizeEOL(raw)
341
+ const out = computeEditEntry(content, args, { path: args.path, absPath: abs })
342
+ // 写回按原文行尾(joinWithEol)——normalizeEOL 先行避免 \r\n 污染 split/join
343
+ // 单一写路径点:默认 = writeFile + 记账;端侧注入 ⇒ 编辑器径(§2.13.5)。
344
+ await writeThroughPath(abs, joinWithEol(normalizeEOL(out.updated).split("\n"), raw), {
345
+ op: "write",
346
+ record: { type: "edit", startLine: out.editStartLine, shift: out.lineShift },
347
+ })
348
+ const diff = gitDiffOne(ctx.cwd, abs)
349
+ const baseResult = out.deleted
350
+ ? `Deleted ${deleteTarget(out)} of ${args.path}${diff ? "\n" + diff : ""}${await autoSyntaxCheck(abs)}`
351
+ : `Edited ${args.path}: replaced ${out.occurrences} occurrence(s)${out.note ? ` — ${out.note}` : ""}${diff ? "\n" + diff : ""}${await autoSyntaxCheck(abs)}`
352
+ return await composeEditReceipt({
353
+ abs,
354
+ path: args.path,
355
+ writeLine: out.editStartLine,
356
+ base: baseResult,
357
+ deleted: out.deleted === true,
358
+ occurrences: out.occurrences,
359
+ note: out.note ?? null,
360
+ })
361
+ }
362
+
363
+ // ─── edit 回执形态注入缝(#69——「回执形态按端注入」,形态参 §2.13.5 注入缝)──────────
364
+ /**
365
+ * 回执组装注入位(**缺省不覆盖** = CLI 回执形态(base + 写入点上下文)——零行为变;
366
+ * 端装配层可覆盖为本端回执形态)。核内零端名分支(契约 5——本档只认 `format` 函数名)。
367
+ * 契约:`format(fields) → string | null | undefined | Promise<同>`——null / undefined =
368
+ * 未处理 ⇒ 回默认回执;其余返回值即该条回执。
369
+ */
370
+ let injectedReceipt = null
371
+ export function configureEditReceipt(impl) {
372
+ injectedReceipt = impl && typeof impl.format === "function" ? impl : null
373
+ }
374
+ /** 撤销注入(测试与端装配生命周期用——缺省态 = CLI 回执)。 */
375
+ export function resetEditReceipt() { injectedReceipt = null }
376
+
377
+ /**
378
+ * edit 回执组装单点(单形态 / 数组形态共用——#69):先组默认回执(base + 写入点上下文),
379
+ * 注入面命中 ⇒ 以其 format(fields) 返回值为准(`fields.default` = 默认回执原文,供端侧改写)。
380
+ */
381
+ export async function composeEditReceipt(fields) {
382
+ const defaultText = await appendWriteContext(fields.abs, fields.writeLine, fields.base)
383
+ if (injectedReceipt) {
384
+ const out = await injectedReceipt.format({ ...fields, default: defaultText })
385
+ if (out !== null && out !== undefined) return String(out)
386
+ }
387
+ return defaultText
388
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * tools/exec-run.mjs — 可中断执行器注入缝(CORE-UNIFICATION §2.13.4——#61 / #63 / #96 同源缺口)。
3
+ *
4
+ * 缺口(本档存在的原因):核内 linter / verify 的检查器执行直调 `execFileSync` / `spawnSync`
5
+ * (CLI 语义——同步阻塞,signal 不可达);宿主端以可中断执行器(spawn + abort / timeout 树杀,
6
+ * Stop 可停)跑同类命令。端侧接核后若不换执行面,会丢失可中断执行 ⇒ 本模块 = 那一跳:
7
+ * 核内**执行面单点** `runCommand`。
8
+ *
9
+ * 默认径 = CLI 语义(**零行为变**):`execFileSync` + utf8 + timeout + stdio ignore/pipe/pipe
10
+ * (与各调用点落地前的实参逐字等价)。端侧 `configureExecRun({ run })` 覆盖 ⇒ 可中断执行器;
11
+ * **缺省不覆盖**。核内**零端名分支**(契约 5——本档只认 `run` 函数名)。
12
+ *
13
+ * 契约(注入实现):
14
+ * · `run(cmd, args, opts) → Promise<string> | string`——成功给 stdout;
15
+ * · 失败以 Error reject——`.stdout` / `.stderr` / `.code` 与 `execFileSync` 错误同形
16
+ * (端侧现形 = 宿主 `runInterruptible`);
17
+ * · `opts = { cwd?, timeout?, signal? }`——**默认径忽略 signal**(execFileSync 不可中断,
18
+ * 与现行 CLI 行为逐字同);注入径自决(如 abort ⇒ 树杀)。
19
+ */
20
+ import { execFileSync } from "node:child_process"
21
+
22
+ let injected = null
23
+
24
+ /** 覆盖执行面(**端装配层**调用;核内不调用——缺省不覆盖)。传 null / 非对象 / 非函数 ⇒ 撤销。 */
25
+ export function configureExecRun(impl) {
26
+ injected = impl && typeof impl === "object" && typeof impl.run === "function" ? impl : null
27
+ }
28
+
29
+ /** 撤销注入(测试与端装配生命周期用——缺省态 = execFileSync 默认径)。 */
30
+ export function resetExecRun() { injected = null }
31
+
32
+ /** 默认径(CLI 语义——utf8 + timeout + stdio ignore/pipe/pipe;cwd 未给则不设)。 */
33
+ function defaultRun(cmd, args, opts) {
34
+ const options = { encoding: "utf8", timeout: opts.timeout, stdio: ["ignore", "pipe", "pipe"] }
35
+ if (opts.cwd !== undefined) options.cwd = opts.cwd
36
+ return execFileSync(cmd, args, options)
37
+ }
38
+
39
+ /** 核内执行面单点——linter / verify 的命令执行全部经此(注入 ⇒ 端侧执行器接管)。 */
40
+ export async function runCommand(cmd, args, opts = {}) {
41
+ if (injected) return await injected.run(cmd, args, opts)
42
+ return defaultRun(cmd, args, opts)
43
+ }