@thincoder/core 0.10.12 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -0
- package/advisor/messages.mjs +4 -2
- package/advisor/notice.mjs +1 -1
- package/advisor/project-context.mjs +121 -11
- package/agent/completion.mjs +13 -1
- package/agent/digest-account.mjs +73 -46
- package/agent/dispatch-run.mjs +1 -1
- package/agent/dispatch.mjs +29 -21
- package/agent/family-tools.mjs +9 -5
- package/agent/helpers.mjs +1 -94
- package/agent/offload.mjs +88 -0
- package/agent/record-results.mjs +1 -6
- package/agent/run-stages.mjs +74 -49
- package/agent/run-start.mjs +8 -8
- package/agent/setup.mjs +6 -11
- package/agent/suspension.mjs +1 -1
- package/agent/turn-loop.mjs +35 -3
- package/agent/write-face.mjs +99 -0
- package/agent/write-gate.mjs +354 -5
- package/agent-tools/advisor.mjs +127 -11
- package/agent-tools/async-settle.mjs +2 -2
- package/agent-tools/bash-async.mjs +8 -2
- package/agent-tools/batch-closeout.mjs +206 -0
- package/agent-tools/batch-gate.mjs +287 -73
- package/agent-tools/batch-ledger-bind.mjs +183 -0
- package/agent-tools/batch-ledger-link.mjs +73 -0
- package/agent-tools/batch-lifecycle.mjs +113 -354
- package/agent-tools/batch-paths.mjs +45 -40
- package/agent-tools/batch-read.mjs +1 -1
- package/agent-tools/batch-reopen.mjs +142 -0
- package/agent-tools/batch-skeleton.mjs +283 -80
- package/agent-tools/batch-unit-test-gate.mjs +107 -0
- package/agent-tools/batch.mjs +243 -50
- package/agent-tools/context.mjs +6 -3
- package/agent-tools/design-slots.mjs +24 -24
- package/agent-tools/design-token.mjs +96 -15
- package/agent-tools/doc-check.mjs +18 -2
- package/agent-tools/eng.mjs +1 -1
- package/agent-tools/escalate-async.mjs +3 -3
- package/agent-tools/manifest.mjs +8 -5
- package/agent-tools/read-history.mjs +5 -5
- package/agent-tools/review-facts.mjs +31 -13
- package/agent-tools/spawn-gates.mjs +10 -14
- package/agent-tools/ssh-session.mjs +120 -111
- package/agent-tools/ssh-transfer.mjs +210 -0
- package/agent-tools/ssh-transport.mjs +277 -46
- package/agent-tools/subagent-actions-query.mjs +1 -1
- package/agent-tools/subagent-actions.mjs +1 -1
- package/agent-tools/subagent-async.mjs +3 -2
- package/agent-tools/subagent-scheduler.mjs +43 -9
- package/agent-tools/subagent-spawn.mjs +68 -9
- package/agent-tools/subagent.mjs +16 -14
- package/agent.mjs +1 -0
- package/client-identity.mjs +70 -0
- package/config-presets.mjs +5 -3
- package/config.mjs +19 -13
- package/context.mjs +39 -8
- package/declaration.mjs +2 -2
- package/doc-check/anchors.mjs +128 -28
- package/doc-check/index.mjs +93 -27
- package/doc-check/line-refs.mjs +112 -0
- package/doc-check/paragraph.mjs +24 -8
- package/embedding.mjs +2 -0
- package/explore-distill.mjs +1 -1
- package/generate-title.mjs +3 -2
- package/i18n.mjs +1 -1
- package/index-discover.mjs +4 -1
- package/ledger-close-proof.mjs +62 -0
- package/ledger-cmd.mjs +79 -194
- package/ledger-db.mjs +29 -28
- package/ledger-migrate.mjs +4 -4
- package/ledger-read.mjs +10 -8
- package/ledger-surface.mjs +1 -1
- package/ledger-tools.mjs +10 -10
- package/ledger-variant-notice.mjs +1 -1
- package/ledger-write-ops.mjs +373 -0
- package/ledger.mjs +2 -2
- package/manifest-discovery.mjs +2 -2
- package/manifest-schema.mjs +4 -0
- package/memory/memory-tool.mjs +5 -2
- package/memory/schema.mjs +5 -26
- package/model-specs.mjs +6 -5
- package/package.json +1 -1
- package/path-resolve.mjs +217 -0
- package/permission.mjs +4 -0
- package/prompt-overlays.mjs +5 -0
- package/prompts/advisor-design.md +9 -6
- package/prompts/advisor-round1.md +4 -3
- package/prompts/advisor-round2.md +6 -5
- package/prompts/advisor-round3.md +6 -5
- package/prompts/common.md +23 -10
- package/prompts/consult-base.md +2 -1
- package/prompts/discipline-engineering.md +29 -48
- package/prompts/discipline-normal.md +10 -5
- package/prompts/persona-eng-coder.md +8 -0
- package/prompts/persona-eng-designer.md +22 -12
- package/prompts/persona-engineering.md +81 -66
- package/prompts/persona-plan.md +2 -1
- package/prompts/persona-tester.md +55 -0
- package/provider/anthropic.mjs +12 -7
- package/provider/core.mjs +21 -5
- package/provider/errors.mjs +41 -0
- package/provider/google.mjs +25 -9
- package/provider/list-models.mjs +4 -3
- package/provider/rate.mjs +11 -3
- package/provider/responses.mjs +17 -7
- package/provider/sse.mjs +15 -4
- package/session-identity.mjs +21 -0
- package/session-lifecycle.mjs +12 -4
- package/session-slots.mjs +9 -14
- package/session.mjs +5 -0
- package/skip-dirs.mjs +48 -0
- package/team-ledger.mjs +4 -4
- package/text-budget.mjs +4 -0
- package/token-window.mjs +107 -10
- package/tool-docs/advisor.md +2 -2
- package/tool-docs/apply_patch.md +2 -0
- package/tool-docs/bash.md +4 -0
- package/tool-docs/batch.md +85 -1
- package/tool-docs/browser.md +3 -1
- package/tool-docs/code_search.md +3 -1
- package/tool-docs/consult_start.md +6 -2
- package/tool-docs/consult_stop.md +3 -1
- package/tool-docs/delete.md +3 -0
- package/tool-docs/doc_check.md +43 -8
- package/tool-docs/doc_search.md +5 -1
- package/tool-docs/edit.md +22 -1
- package/tool-docs/execute.md +7 -1
- package/tool-docs/fetch.md +3 -1
- package/tool-docs/file_ops.md +3 -1
- package/tool-docs/git.md +9 -1
- package/tool-docs/goal.md +2 -0
- package/tool-docs/grep.md +1 -1
- package/tool-docs/hashline_edit.md +2 -0
- package/tool-docs/insert_after.md +3 -1
- package/tool-docs/ledger.md +5 -5
- package/tool-docs/lint.md +1 -0
- package/tool-docs/manifest-read.md +2 -1
- package/tool-docs/manifest.md +9 -2
- package/tool-docs/memory.md +11 -1
- package/tool-docs/notify_parent.md +1 -1
- package/tool-docs/plan.md +2 -0
- package/tool-docs/question.md +1 -1
- package/tool-docs/read_history.md +15 -3
- package/tool-docs/read_image.md +3 -1
- package/tool-docs/recent_changes.md +3 -1
- package/tool-docs/repo_outline.md +3 -1
- package/tool-docs/settings.md +10 -2
- package/tool-docs/skill.md +2 -0
- package/tool-docs/ssh.md +30 -17
- package/tool-docs/subagent.md +50 -13
- package/tool-docs/task.md +2 -0
- package/tool-docs/team_ledger.md +1 -1
- package/tool-docs/timer.md +7 -0
- package/tool-docs/verify.md +13 -1
- package/tool-docs/websearch.md +3 -1
- package/tool-docs/write.md +2 -0
- package/tools/bash.mjs +12 -2
- package/tools/edit-batch.mjs +40 -33
- package/tools/edit-diff.mjs +69 -1
- package/tools/execute.mjs +10 -4
- package/tools/file.mjs +17 -27
- package/tools/git.mjs +27 -1
- package/tools/ops.mjs +2 -1
- package/tools/patch.mjs +12 -3
- package/tools/process-doc-mark.mjs +139 -0
- package/tools/search.mjs +15 -6
- package/tools/shared.mjs +24 -1
- package/tools/ssh.mjs +48 -7
- package/tools/tree.mjs +4 -1
- package/tools/write-path.mjs +64 -3
- package/traces/trace-store.mjs +4 -3
- package/agent/light-round.mjs +0 -63
- package/agent/unbooked-pen.mjs +0 -62
package/tools/edit-batch.mjs
CHANGED
|
@@ -3,23 +3,24 @@
|
|
|
3
3
|
* (2026-08-31 工具顺手度 §9 ②;2026-09-01 缺陷修复"同文件多条串行累积";
|
|
4
4
|
* 2026-09-04 EDIT.md §6——条目级判定+应用迁至 edit-diff.mjs——批量调共用。)
|
|
5
5
|
*
|
|
6
|
-
* 语义:同一 path
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* 语义:同一 path 的多条编辑**分两类应用**(帧判据单源 = EDIT-HELPERS §7.3 R1–R4)——
|
|
7
|
+
* 行号条目先全部应用(按 startLine 降序,范围重叠则整批拒),内容条目随后按调用序
|
|
8
|
+
* 串行累积;跨 path 条目互不影响(并行原子语义);任一条失败 → 全不写(原子性保留)。
|
|
9
|
+
* 每条目独立按判定序(EDIT.md §4——分支 0 单行替换 / 零重叠→替换即删 /
|
|
9
10
|
* LCS / 空 new 显式报错;EDIT.md §5(约束——删行形态):省略 new_string = 删行。顶层 path
|
|
10
11
|
* (args.path)为无自带 path 条目的默认(2026-09-05 用户裁定——条目自带 path 优先——见
|
|
11
|
-
* EDIT.md §5
|
|
12
|
+
* EDIT.md §5 修订注)。排顺序与累积走 edit-diff 的两个零 IO 纯函数——本档只留 IO(读 / 写 / 回执)。
|
|
12
13
|
*/
|
|
13
14
|
import { readFile } from "node:fs/promises"
|
|
14
15
|
import { resolveInCwd, normalizeEOL, joinWithEol, gitDiffOne, autoSyntaxCheck } from "./shared.mjs"
|
|
15
16
|
// edit-batch ↔ edit-diff 循环引用:两侧导入的都是函数声明(提升初始化),
|
|
16
17
|
// 仅在调用期使用——ESM 循环下安全(无模块求值期取值)。
|
|
17
18
|
// 写盘经单一写路径点(§2.13.5)——本档不再直调 fs;回执组装共用 edit-diff 的 composeEditReceipt。
|
|
18
|
-
import { writeThroughPath } from "./write-path.mjs"
|
|
19
|
+
import { writeThroughPath, lineAnchorVerdict, lineAnchorRefusal } from "./write-path.mjs"
|
|
19
20
|
// EDIT.md §6:批量条目判定+应用共用 edit-diff(EDIT.md §4 分支 0 单行替换 + 行级 LCS——零重叠→替换即删);
|
|
20
21
|
// EDIT.md §5:edits 互斥错误文本随前置校验分支迁出至 edit-diff.mjs。
|
|
21
22
|
// 回执组装共用 edit-diff 的 composeEditReceipt(单点——缺省回执 = base + 写入点上下文)。
|
|
22
|
-
import { assertEditArgsExclusive, assertEditsContainer, assertEditEntries, validateEditEntry,
|
|
23
|
+
import { assertEditArgsExclusive, assertEditsContainer, assertEditEntries, validateEditEntry, hasLineParams, planEditEntries, applyPlannedEntries, splitLines, EMPTY_NEW_STRING_LINE, EDIT_ENTRY_NO_PATH, editEntryLabel, deleteTarget, composeEditReceipt } from "./edit-diff.mjs"
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* Apply the `edits` array form: multi-file atomic replacement. Throws on any
|
|
@@ -31,12 +32,13 @@ export async function applyEditBatch(args, ctx) {
|
|
|
31
32
|
assertEditArgsExclusive(args)
|
|
32
33
|
assertEditEntries(args.edits) // #325 条目预扫(非对象条目 ⇒ 含下标成形错误;不读盘)
|
|
33
34
|
// 原子:先全量检查(所有文件的替换都可执行)——任一失败全不写。
|
|
34
|
-
// 2026-09-01 缺陷修复(TOOLS.md §9 ②"同文件多条规则"):同一 path
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
35
|
+
// 2026-09-01 缺陷修复(TOOLS.md §9 ②"同文件多条规则"):同一 path 的多条编辑基于同一份
|
|
36
|
+
// 累积内容应用,写盘前不得让后者覆盖前者(原实现每条都基于盘上原始内容计算、写盘循环
|
|
37
|
+
// 后置 → 除最后一条外全部静默丢失);跨 path 条目互不影响(并行原子语义不变)。
|
|
38
|
+
// 累积顺序自 2026-11-02 起 = 帧(行号条目降序先应用、内容条目随后)——见下方判定点。
|
|
38
39
|
const groups = new Map() // abs → 每文件一条流水线
|
|
39
|
-
for (
|
|
40
|
+
for (let i = 0; i < args.edits.length; i++) {
|
|
41
|
+
const e = args.edits[i]
|
|
40
42
|
// 2026-09-05 用户裁定:顶层 path + edits 并存合法化——`e.path || args.path`(条目优先;
|
|
41
43
|
// 缺省入口仅在此兜底);两者皆无 → 原错误文本(措辞补顶层选项)。
|
|
42
44
|
const p = e.path || args.path
|
|
@@ -47,39 +49,42 @@ export async function applyEditBatch(args, ctx) {
|
|
|
47
49
|
let g = groups.get(abs)
|
|
48
50
|
if (!g) {
|
|
49
51
|
const raw = await readFile(abs, "utf8")
|
|
50
|
-
g = { abs, path: p, raw, content: normalizeEOL(raw), edits: [] }
|
|
52
|
+
g = { abs, path: p, raw, content: normalizeEOL(raw), edits: [], origIdx: [] }
|
|
51
53
|
groups.set(abs, g)
|
|
52
54
|
}
|
|
53
55
|
g.edits.push(e)
|
|
56
|
+
g.origIdx.push(i) // 条目在 args.edits 里的原始下标——回显按它回位(EDIT-HELPERS §7.3)
|
|
54
57
|
}
|
|
55
|
-
|
|
58
|
+
// 判定点(§7.3 写死):组循环**之前**用批前快照判一次——命中即抛、零写(下方第二个循环才写盘)。
|
|
59
|
+
// 组内不再逐条判:组内漂移由 R2(降序应用)自己算得出来,逐条再判会用同一份快照判出与 R2 相反的结果。
|
|
56
60
|
for (const g of groups.values()) {
|
|
57
|
-
g.netShift = 0 // 组内行数差累积(合并快照的 shift = 全组净漂移)
|
|
58
61
|
for (const e of g.edits) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
absPath: g.abs,
|
|
64
|
-
abortPrefix: EDIT_ABORT_PREFIX,
|
|
65
|
-
})
|
|
66
|
-
prepared.push({
|
|
67
|
-
g,
|
|
68
|
-
editStartLine: out.editStartLine, // 基于累积内容计算——已天然计入前面条目的行偏移,不再累加
|
|
69
|
-
lineShift: out.lineShift,
|
|
70
|
-
occurrences: out.occurrences,
|
|
71
|
-
note: out.note ?? null, // P15.11——空白差异自动落点标记(成功消息追加)
|
|
72
|
-
deleted: out.deleted ?? false, // 删行形态(EDIT.md §5)——结果文本用 Deleted 前缀
|
|
73
|
-
})
|
|
74
|
-
g.content = out.updated // 串行累积:下一条基于本条应用后的内容
|
|
75
|
-
g.netShift += out.lineShift
|
|
62
|
+
if (!hasLineParams(e)) continue
|
|
63
|
+
const startLine = e.line ?? e.startLine
|
|
64
|
+
const endLine = e.line ?? e.endLine
|
|
65
|
+
if (lineAnchorVerdict(g.abs, startLine, endLine)) throw new Error(lineAnchorRefusal(g.abs, startLine, endLine))
|
|
76
66
|
}
|
|
77
67
|
}
|
|
78
|
-
//
|
|
68
|
+
// 回显按调用序(EDIT-HELPERS §7.3):按条目在 `edits` 数组里的**原始下标**回填——跨文件
|
|
69
|
+
// 交错的批同样如此;按文件分组去排会排成 A、A、B,而模型是按位置对号的,顺序一变它会把
|
|
70
|
+
// 结果贴到错的那一笔上。
|
|
71
|
+
const prepared = new Array(args.edits.length)
|
|
72
|
+
for (const g of groups.values()) {
|
|
73
|
+
// 帧单源(§7.3 R1–R4)= edit-diff 的两个零 IO 纯函数:排顺序 + 按计划序累积
|
|
74
|
+
const plan = planEditEntries(g.edits, g.path)
|
|
75
|
+
if (plan.rejection) throw new Error(plan.rejection)
|
|
76
|
+
const applied = applyPlannedEntries(g.content, g.edits, plan.order, { path: g.path })
|
|
77
|
+
g.content = applied.content
|
|
78
|
+
// 合并快照的 shift = 全组净漂移(公式不变;重排后的取值由 AC11 钉住)
|
|
79
|
+
g.netShift = applied.results.reduce((n, r) => n + r.lineShift, 0)
|
|
80
|
+
// results 按组内调用序;g.origIdx[k] = 该结果在 args.edits 里的原始下标
|
|
81
|
+
applied.results.forEach((r, k) => { prepared[g.origIdx[k]] = { g, ...r } })
|
|
82
|
+
}
|
|
83
|
+
// 全部检查通过——每文件一次写盘(同文件多条:写入按帧排好后累积出的最终内容);
|
|
79
84
|
// recordWrite 每组一条合并快照:startLine = 组内**所有**编辑受影响行的最小值
|
|
80
85
|
// (#2,2026-09-01 交付评审尾巴——原实现取首条 = 调用序第一条,逆序条目时
|
|
81
86
|
// 护栏下界过高、受影响区内的 insert_after 被放行),shift = 全组行数差累积
|
|
82
|
-
//
|
|
87
|
+
// ——受影响区护栏覆盖组内所有编辑(公式不变;重排后的取值由 AC11 钉住)
|
|
83
88
|
for (const g of groups.values()) {
|
|
84
89
|
const startLine = Math.min(...prepared.filter((p) => p.g === g).map((p) => p.editStartLine))
|
|
85
90
|
// 单一写路径点:默认 = writeFile + 记账;端侧注入 ⇒ 编辑器径(§2.13.5)。
|
|
@@ -116,6 +121,8 @@ export async function applyEditBatch(args, ctx) {
|
|
|
116
121
|
// ---------------------------------------------------------------------------
|
|
117
122
|
// 2026-09-08 edit 语义升级(EDIT.md §2/§3——D1 按行号改 / D2 模糊匹配,落点定死本模块;
|
|
118
123
|
// 条目判定接线在 edit-diff.mjs computeEditEntry——单形态/批量/ACP 桥三通道自动继承;
|
|
124
|
+
// 2026-11-02:排顺序(帧 R1–R4)与累积也上提到 edit-diff(planEditEntries /
|
|
125
|
+
// applyPlannedEntries)——两通道共用同一份排法。
|
|
119
126
|
// 阶段 2 删行形态见 EDIT.md §5(约束)。以下均为纯函数(无 IO)——edit-diff.mjs 调用期导入
|
|
120
127
|
// (ESM 循环引用安全:两侧仅函数声明,提升初始化——同 file.mjs ↔ edit-diff.mjs 先例)。
|
|
121
128
|
|
package/tools/edit-diff.mjs
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* edit 语义(权威 = EDIT.md §2-§5——2026-09-08 语义升级 D1/D2/D3 已并入):两种定位形态(互斥)——
|
|
5
5
|
* ① 按行号改(D1/F1):line(单行)/ startLine+endLine(1-based 闭区间)→ 直接替换该
|
|
6
6
|
* 行/行范围为 new_string(无需 old_string——实现落点 edit-batch.mjs applyLineEdit);
|
|
7
|
+
* **先过行号锚护栏**(判据与拒文单源 = write-path.mjs 的 lineAnchorVerdict /
|
|
8
|
+
* lineAnchorRefusal——上一笔写之后目标区落在受影响区内即拒);
|
|
7
9
|
* 阶段 2 删行形态(EDIT.md §5——约束):省略 new_string = 删除该行/范围(有界意图);
|
|
8
10
|
* ② 内容定位:old_string = 变化区**当前内容**(单次匹配——匹配档位:逐字 → P15.11 唯一
|
|
9
11
|
* 空白差异窗口 → D2/F2 模糊匹配(行级 normalize 后 ≥90% 行相等——edit-batch.mjs
|
|
@@ -35,7 +37,7 @@ import {
|
|
|
35
37
|
// 仅在调用期使用——ESM 循环下安全(无模块求值期取值)。
|
|
36
38
|
// 写盘经单一写路径点(§2.13.5)——本档不再直调 fs。
|
|
37
39
|
import { appendWriteContext } from "./file.mjs"
|
|
38
|
-
import { writeThroughPath, lastWriteOf, isDirty } from "./write-path.mjs"
|
|
40
|
+
import { writeThroughPath, lastWriteOf, isDirty, lineAnchorVerdict, lineAnchorRefusal } from "./write-path.mjs"
|
|
39
41
|
// D1/D2 落点(EDIT.md §6——edit-batch.mjs):按行号改 + 模糊匹配纯函数。
|
|
40
42
|
import { applyLineEdit, findFuzzyWindow, FUZZY_MATCH_NOTE } from "./edit-batch.mjs"
|
|
41
43
|
|
|
@@ -357,6 +359,63 @@ export function computeEditEntry(content, entry, opts = {}) {
|
|
|
357
359
|
return { updated, editStartLine, lineShift, occurrences: entry.replace_all ? occurrences : 1, note }
|
|
358
360
|
}
|
|
359
361
|
|
|
362
|
+
/**
|
|
363
|
+
* §7.3 帧(R1–R4)——同档多条 `edits` 的应用顺序:行号条目先全部应用、按 `startLine` 降序
|
|
364
|
+
* (从下往上改,上方行号不被同批自己顶偏);内容条目随后按数组序(位置无关寻址无帧问题)。
|
|
365
|
+
*
|
|
366
|
+
* **零 IO**(只看条目自身的行号):两个通道(本地 `edit-batch` ∥ ACP 桥)调同一份排法,
|
|
367
|
+
* 各自只提供自己的 IO——抄一份组循环就是本机制要根治的形状。
|
|
368
|
+
*
|
|
369
|
+
* @returns {{ order: number[]|null, rejection: string|null }} `order` = 数组下标的应用序
|
|
370
|
+
* (回显仍按调用序取原位);`rejection` 非空 = R3 重叠:整批拒、零写。
|
|
371
|
+
* `path`(可选)= 拒文里点名的目标档(条目自带 path 优先——顶层 path 不在条目上)。
|
|
372
|
+
*/
|
|
373
|
+
export function planEditEntries(entries, path = null) {
|
|
374
|
+
const rangeOf = (e) => [e.line ?? e.startLine, e.line ?? e.endLine]
|
|
375
|
+
const lineIdx = []
|
|
376
|
+
const contentIdx = []
|
|
377
|
+
entries.forEach((e, i) => { (hasLineParams(e) ? lineIdx : contentIdx).push(i) })
|
|
378
|
+
// R3:两条行号条目的范围在快照坐标上重叠 ⇒ 先改哪一条决定结果,模型意图在此矛盾
|
|
379
|
+
const byStart = [...lineIdx].sort((a, b) => rangeOf(entries[a])[0] - rangeOf(entries[b])[0])
|
|
380
|
+
for (let i = 1; i < byStart.length; i++) {
|
|
381
|
+
const prev = rangeOf(entries[byStart[i - 1]])
|
|
382
|
+
const cur = rangeOf(entries[byStart[i]])
|
|
383
|
+
if (cur[0] <= prev[1]) {
|
|
384
|
+
const label = entries[byStart[i]].path ?? entries[byStart[i - 1]].path ?? path ?? "file"
|
|
385
|
+
return {
|
|
386
|
+
order: null,
|
|
387
|
+
rejection:
|
|
388
|
+
`${EDIT_ABORT_PREFIX}line-number entries L${prev[0]}-L${prev[1]} and L${cur[0]}-L${cur[1]} (${label}) overlap — ` +
|
|
389
|
+
`two changes to the same region cannot both be anchored on the numbers you read; split them into separate calls`,
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
// R2:降序(同档起始行不会相等——相等即重叠,已在 R3 拒掉)
|
|
394
|
+
const desc = [...lineIdx].sort((a, b) => rangeOf(entries[b])[0] - rangeOf(entries[a])[0])
|
|
395
|
+
return { order: [...desc, ...contentIdx], rejection: null }
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* 按 `planEditEntries` 排出的应用序逐条 `computeEditEntry` 累积——**零 IO**。
|
|
400
|
+
* `results` 按调用序回到原位(回显顺序不变),元数据形同 `computeEditEntry` 返回。
|
|
401
|
+
*/
|
|
402
|
+
export function applyPlannedEntries(content, entries, order, opts = {}) {
|
|
403
|
+
let cur = content
|
|
404
|
+
const results = new Array(entries.length)
|
|
405
|
+
for (const i of order) {
|
|
406
|
+
const out = computeEditEntry(cur, entries[i], { ...opts, abortPrefix: opts.abortPrefix ?? EDIT_ABORT_PREFIX })
|
|
407
|
+
results[i] = {
|
|
408
|
+
editStartLine: out.editStartLine,
|
|
409
|
+
lineShift: out.lineShift,
|
|
410
|
+
occurrences: out.occurrences,
|
|
411
|
+
note: out.note ?? null,
|
|
412
|
+
deleted: out.deleted ?? false,
|
|
413
|
+
}
|
|
414
|
+
cur = out.updated
|
|
415
|
+
}
|
|
416
|
+
return { content: cur, results }
|
|
417
|
+
}
|
|
418
|
+
|
|
360
419
|
/**
|
|
361
420
|
* 单形态 edit 执行体(自 file.mjs 整段迁出——模块拆分写优先纪律:逻辑体不变)——
|
|
362
421
|
* file.mjs 只留工具壳与转发。
|
|
@@ -365,6 +424,15 @@ export async function runSingleEdit(args, ctx) {
|
|
|
365
424
|
const abs = resolveInCwd(ctx, args.path)
|
|
366
425
|
const raw = await readFile(abs, "utf8")
|
|
367
426
|
const content = normalizeEOL(raw)
|
|
427
|
+
// 行号锚护栏(判定点写死:读盘之后、computeEditEntry 之前):行号形才需判——内容形态是
|
|
428
|
+
// 位置无关寻址,没有可漂移的锚。判据与拒文单源 = write-path.mjs。
|
|
429
|
+
// 形态校验先于护栏:缺 endLine / 与 old_string 互斥这类错要报自己的句,不该被拒文顶掉。
|
|
430
|
+
if (hasLineParams(args)) {
|
|
431
|
+
validateEditEntry(args, { rich: true })
|
|
432
|
+
const startLine = args.line ?? args.startLine
|
|
433
|
+
const endLine = args.line ?? args.endLine
|
|
434
|
+
if (lineAnchorVerdict(abs, startLine, endLine)) throw new Error(lineAnchorRefusal(abs, startLine, endLine))
|
|
435
|
+
}
|
|
368
436
|
const out = computeEditEntry(content, args, { path: args.path, absPath: abs })
|
|
369
437
|
// 写回按原文行尾(joinWithEol)——normalizeEOL 先行避免 \r\n 污染 split/join
|
|
370
438
|
// 单一写路径点:默认 = writeFile + 记账;端侧注入 ⇒ 编辑器径(§2.13.5)。
|
package/tools/execute.mjs
CHANGED
|
@@ -26,9 +26,11 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import { spawn } from "node:child_process"
|
|
28
28
|
import { resolve } from "node:path"
|
|
29
|
-
import { DESC } from "./shared.mjs"
|
|
29
|
+
import { DESC, LEDGER_PARAM_DESC, PROJECT_PARAM_DESC } from "./shared.mjs"
|
|
30
30
|
import { killProcessTree } from "./process-tree.mjs"
|
|
31
31
|
import { nodeChildEnv } from "./node-child.mjs"
|
|
32
|
+
// 过程档标记(TOOLS.md §6.28):失败径同标(错误文本也是行内容),解析基准 = 子进程 cwd `baseDir`。
|
|
33
|
+
import { markProcessDocLines } from "./process-doc-mark.mjs"
|
|
32
34
|
|
|
33
35
|
const MAX_SCRIPT = 50_000
|
|
34
36
|
const MAX_OUTPUT = 50_000
|
|
@@ -197,6 +199,9 @@ export const executeTool = {
|
|
|
197
199
|
maximum: 600000,
|
|
198
200
|
description: `Timeout in milliseconds (default ${DEFAULT_TIMEOUT}, max 600000)`,
|
|
199
201
|
},
|
|
202
|
+
ledger: { type: "string", description: LEDGER_PARAM_DESC },
|
|
203
|
+
// 项目声明面(决策 40):`workdir` 缺位时才被读(定位参数优先);纯声明,不改执行目录。
|
|
204
|
+
project: { type: "string", description: PROJECT_PARAM_DESC },
|
|
200
205
|
},
|
|
201
206
|
required: [],
|
|
202
207
|
},
|
|
@@ -231,8 +236,9 @@ export const executeTool = {
|
|
|
231
236
|
}
|
|
232
237
|
|
|
233
238
|
const { text, ok } = await runNode(childArgs, baseDir, timeoutMs, ctx.signal)
|
|
234
|
-
//
|
|
235
|
-
|
|
236
|
-
|
|
239
|
+
// §6.28 单一出口:`filter` 只滤成功输出(原口径),打标接在它的后面;解析基准 = `baseDir`
|
|
240
|
+
// (= 子进程 cwd),不是 `ctx.cwd`——`workdir` 在场时只有按子进程 cwd 才解得对上。
|
|
241
|
+
const body = ok && args.filter ? applyFilter(text, args.filter) : text
|
|
242
|
+
return markProcessDocLines(body, baseDir)
|
|
237
243
|
},
|
|
238
244
|
}
|
package/tools/file.mjs
CHANGED
|
@@ -11,12 +11,13 @@ import {
|
|
|
11
11
|
joinWithEol,
|
|
12
12
|
majorityEol,
|
|
13
13
|
FFFD_WARNING,
|
|
14
|
+
LEDGER_PARAM_DESC,
|
|
14
15
|
} from "./shared.mjs";
|
|
15
16
|
import { applyEditBatch } from "./edit-batch.mjs";
|
|
16
17
|
import { runSingleEdit } from "./edit-diff.mjs";
|
|
17
18
|
// 单一写路径点(编辑器编辑径注入缝——CORE-UNIFICATION §2.13.5):本档三个写点全走它,
|
|
18
19
|
// 记账面同源住该档(下面只做再导出——保既有 import 面稳定)。
|
|
19
|
-
import { writeThroughPath, clearDirty, clearLastWrite,
|
|
20
|
+
import { writeThroughPath, clearDirty, clearLastWrite, lineAnchorVerdict, lineAnchorRefusal } from "./write-path.mjs";
|
|
20
21
|
import { specForModel } from "../config.mjs";
|
|
21
22
|
import { createHash } from "node:crypto";
|
|
22
23
|
import { mkdir, readFile, stat } from "node:fs/promises";
|
|
@@ -192,6 +193,7 @@ export const writeTool = {
|
|
|
192
193
|
properties: {
|
|
193
194
|
path: { type: "string", description: "File path (relative to cwd or absolute)" },
|
|
194
195
|
content: { type: "string", description: "Full content to write" },
|
|
196
|
+
ledger: { type: "string", description: LEDGER_PARAM_DESC },
|
|
195
197
|
},
|
|
196
198
|
required: ["path", "content"],
|
|
197
199
|
},
|
|
@@ -231,7 +233,7 @@ export const editTool = {
|
|
|
231
233
|
replace_all: { type: "boolean", description: "Replace all occurrences (default false)" },
|
|
232
234
|
edits: {
|
|
233
235
|
type: "array",
|
|
234
|
-
description: "Batch form — multiple edits in ONE call, atomic (any failure writes nothing;
|
|
236
|
+
description: "Batch form — multiple edits in ONE call, atomic (any failure writes nothing; line-number entries apply first, in descending startLine order, and their ranges must not overlap — overlap refuses the whole batch; content entries then apply in array order). Use it for multiple changes to the same file AND for independent changes across multiple files — prefer one batched call over N single edits. A top-level path is allowed — it defaults entries without their own path (entry paths win). Mutually exclusive with top-level old_string/new_string/line/startLine/endLine — provide each change's targeting (old_string, or line / startLine+endLine) inside its edits entry; new_string replaces when given, and line-targeted entries may omit it to DELETE the line/range.",
|
|
235
237
|
items: {
|
|
236
238
|
type: "object",
|
|
237
239
|
properties: {
|
|
@@ -245,6 +247,7 @@ export const editTool = {
|
|
|
245
247
|
},
|
|
246
248
|
},
|
|
247
249
|
},
|
|
250
|
+
ledger: { type: "string", description: LEDGER_PARAM_DESC },
|
|
248
251
|
},
|
|
249
252
|
required: [],
|
|
250
253
|
},
|
|
@@ -286,6 +289,7 @@ export const insertAfterTool = {
|
|
|
286
289
|
after_line: { type: "number", description: "Line number to insert after (1-based). Takes priority over after_regex." },
|
|
287
290
|
after_regex: { type: "string", description: "JavaScript regex to find the line to insert after (must match exactly one line)" },
|
|
288
291
|
content: { type: "string", description: "Text to insert (with leading newline if you need a blank line)" },
|
|
292
|
+
ledger: { type: "string", description: LEDGER_PARAM_DESC },
|
|
289
293
|
},
|
|
290
294
|
required: ["path", "content"],
|
|
291
295
|
},
|
|
@@ -296,31 +300,16 @@ export const insertAfterTool = {
|
|
|
296
300
|
// Read-before-insert guard: after_line anchors are line numbers, and any
|
|
297
301
|
// write since the last read made them stale. Refuse instead of silently
|
|
298
302
|
// inserting at a drifted position (the failure mode that corrupted test
|
|
299
|
-
// structure repeatedly).
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
//
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
)
|
|
310
|
-
}
|
|
311
|
-
if (args.after_line > lw.startLine) {
|
|
312
|
-
throw new Error(
|
|
313
|
-
`${args.path} 的 after_line ${args.after_line} 在上次写入(L${lw.startLine})之后——` +
|
|
314
|
-
`行号已漂移 ${lw.shift >= 0 ? "+" : ""}${lw.shift},请用新行号或先 read。`
|
|
315
|
-
)
|
|
316
|
-
}
|
|
317
|
-
// after_line <= startLine → 行号未漂移 → 允许
|
|
318
|
-
} else if (isDirty(abs)) {
|
|
319
|
-
throw new Error(
|
|
320
|
-
`${args.path} was modified since your last read — line numbers may be stale.\n` +
|
|
321
|
-
`Read the file again (read tool) to refresh line numbers, then retry insert_after.`
|
|
322
|
-
)
|
|
323
|
-
}
|
|
303
|
+
// structure repeatedly).
|
|
304
|
+
// 2026-11-02:判据与拒文单源 = tools/write-path.mjs 的 lineAnchorVerdict /
|
|
305
|
+
// lineAnchorRefusal(与 edit 行号形同一句、同一六行表——INSERT-AFTER.md §3)。
|
|
306
|
+
// 本工具的边界 = `after_line <= startLine`(插入不读写目标行,比 edit 松一格——
|
|
307
|
+
// 边界按记账类型分,已在判据内);after_regex 没有模型给的行号(定位在工具内按当前
|
|
308
|
+
// 盘面做)⇒ 只判到记账态那一层(dirty 面无快照 ⇒ 第 6 行),与改前同判。
|
|
309
|
+
const afterLine = Number.isInteger(args.after_line) ? args.after_line : null
|
|
310
|
+
const hit = lineAnchorVerdict(abs, afterLine, afterLine)
|
|
311
|
+
if (hit) throw new Error(lineAnchorRefusal(abs, afterLine, afterLine))
|
|
312
|
+
|
|
324
313
|
const raw = await readFile(abs, "utf8") // original bytes — EOL detection needs the file's real line endings
|
|
325
314
|
const text = normalizeEOL(raw)
|
|
326
315
|
const lines = text.split("\n")
|
|
@@ -385,6 +374,7 @@ export const hashlineEditTool = {
|
|
|
385
374
|
path: { type: "string", description: "File path" },
|
|
386
375
|
old_hashes: { type: "array", items: { type: "string" }, description: "SHA256 hashes (12 chars) of the lines to replace. Read the file with hashes=true first to obtain these hashes. Single line: pass 1 hash; multiple lines: pass the exact sequence of hashes." },
|
|
387
376
|
new_content: { type: "string", description: "Replacement text (can span multiple lines)" },
|
|
377
|
+
ledger: { type: "string", description: LEDGER_PARAM_DESC },
|
|
388
378
|
},
|
|
389
379
|
required: ["path", "old_hashes", "new_content"],
|
|
390
380
|
},
|
package/tools/git.mjs
CHANGED
|
@@ -2,7 +2,9 @@ import {
|
|
|
2
2
|
DESC,
|
|
3
3
|
truncate,
|
|
4
4
|
runGit,
|
|
5
|
-
gitFailureMessage
|
|
5
|
+
gitFailureMessage,
|
|
6
|
+
LEDGER_PARAM_DESC_GIT,
|
|
7
|
+
PROJECT_PARAM_DESC,
|
|
6
8
|
} from "./shared.mjs";
|
|
7
9
|
import { spawnGit } from "./git-run.mjs";
|
|
8
10
|
import { resolve } from "node:path";
|
|
@@ -86,10 +88,34 @@ export const gitTool = {
|
|
|
86
88
|
create: { type: "boolean", description: "(switch) create the branch then switch (-c)" },
|
|
87
89
|
dest: { type: "string", description: "(mv) destination path (file or directory)" },
|
|
88
90
|
worktreeAction: { type: "string", enum: ["list", "add", "remove"], description: "(worktree) list / add (path, ref) / remove (path)" },
|
|
91
|
+
ledger: { type: "string", description: LEDGER_PARAM_DESC_GIT },
|
|
92
|
+
// 项目声明面(决策 40):`path` ∥ `workdir` 缺位时才被读(定位参数优先);纯声明,不改执行目录。
|
|
93
|
+
project: { type: "string", description: PROJECT_PARAM_DESC },
|
|
89
94
|
},
|
|
90
95
|
required: ["action"],
|
|
91
96
|
},
|
|
92
97
|
readonly: false,
|
|
98
|
+
/**
|
|
99
|
+
* 动作级只读分类(决策 37 · 2026-11-03 笔-账绑定批——**本钩子是单源**)。
|
|
100
|
+
* 消费面两处:`thincoder-core/agent/dispatch-gates.mjs` 的 `readonlyActionOf(tool, "git", args)`
|
|
101
|
+
* (planMode 门 ∥ 权限短路 ∥ 写门的 `git` 写型/只读判定)——三面(CLI ∥ desktop ∥ vscode)同答案。
|
|
102
|
+
* 名单与 `thincoder-vscode/src/tools/index.mjs` 原端装饰体逐条同形(该覆写随本钩子同笔删除):
|
|
103
|
+
* 六项平铺 + 六条子动作分支。`checkpoint` 的 `list` ∥ `cat` 判只读——`checkpoint list` 有 F6 清理副作用
|
|
104
|
+
* (`lazyClearIfCommitted` 可能删除整个 checkpoint 目录),但清的是 commit 后已失去意义的过期快照 ⇒
|
|
105
|
+
* 保留只读分类免审批(`docs/core/design/CHECKPOINT.md` F6/D3 设计意图)。
|
|
106
|
+
* 缺此钩子的面回落到核名面谓词(无 `git` 分支 ⇒ false ⇒ 写型 ⇒ 要号),fail-closed,不出洞。
|
|
107
|
+
*/
|
|
108
|
+
isReadonlyAction(args) {
|
|
109
|
+
if (!args || typeof args.action !== "string") return false
|
|
110
|
+
if (["diff", "status", "log", "show", "ls-remote", "blame"].includes(args.action)) return true
|
|
111
|
+
if (args.action === "checkpoint") return ["list", "cat"].includes(args.checkpointAction)
|
|
112
|
+
if (args.action === "tag") return args.tagAction === "list"
|
|
113
|
+
if (args.action === "branch") return args.branchAction === "list"
|
|
114
|
+
if (args.action === "stash") return args.stashAction === "list"
|
|
115
|
+
if (args.action === "remote") return args.remoteAction === "list"
|
|
116
|
+
if (args.action === "worktree") return args.worktreeAction === "list"
|
|
117
|
+
return false // add/rm/commit/push/fetch/pull/reset/revert/merge/cherry-pick/checkout/restore + tag/branch/stash 写操作
|
|
118
|
+
},
|
|
93
119
|
async execute(args, ctx) {
|
|
94
120
|
// workdir: run git in a subdirectory (monorepo / multi-repo). Shadow ctx.cwd so
|
|
95
121
|
// every action + snapshotBefore + checkpoint resolves against the workdir.
|
package/tools/ops.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* wait_for 条件族 +`bash id:N done`。list ∥ 既有条件面零变。批 browser-async-fix(#1045):kill id
|
|
8
8
|
* 路由先 bg 后 browser(同一 id 命名空间)∥ wait_for 条件族 +`browser id:N done`。
|
|
9
9
|
*/
|
|
10
|
-
import { DESC, resolveInCwd, truncate } from "./shared.mjs"
|
|
10
|
+
import { DESC, resolveInCwd, truncate, LEDGER_PARAM_DESC } from "./shared.mjs"
|
|
11
11
|
// 单一写路径点(编辑器编辑径注入缝——CORE-UNIFICATION §2.13.5):路径操作也经它落盘
|
|
12
12
|
// (默认径 = 既有 cp / rename / EXDEV 回退语义,零行为变)。
|
|
13
13
|
import { writeThroughPath } from "./write-path.mjs"
|
|
@@ -26,6 +26,7 @@ export const fileOpsTool = {
|
|
|
26
26
|
action: { type: "string", enum: ["move", "copy", "rename"], description: "move | copy | rename" },
|
|
27
27
|
source: { type: "string", description: "Source path, relative to cwd or absolute" },
|
|
28
28
|
dest: { type: "string", description: "Destination path" },
|
|
29
|
+
ledger: { type: "string", description: LEDGER_PARAM_DESC },
|
|
29
30
|
},
|
|
30
31
|
required: ["action", "source", "dest"],
|
|
31
32
|
},
|
package/tools/patch.mjs
CHANGED
|
@@ -4,9 +4,10 @@ import {
|
|
|
4
4
|
resolveInCwd,
|
|
5
5
|
normalizeEOL,
|
|
6
6
|
detectFileEol,
|
|
7
|
-
majorityEol
|
|
7
|
+
majorityEol,
|
|
8
|
+
LEDGER_PARAM_DESC
|
|
8
9
|
} from "./shared.mjs";
|
|
9
|
-
import { writeThroughPath, TMP_SUFFIX, markDirty } from "./write-path.mjs";
|
|
10
|
+
import { writeThroughPath, TMP_SUFFIX, markDirty, clearLastWrite } from "./write-path.mjs";
|
|
10
11
|
import { spawnGit } from "./git-run.mjs";
|
|
11
12
|
import { mkdir, readFile, lstat, unlink } from "node:fs/promises";
|
|
12
13
|
import { existsSync } from "node:fs";
|
|
@@ -175,6 +176,7 @@ export const applyPatchTool = {
|
|
|
175
176
|
type: "object",
|
|
176
177
|
properties: {
|
|
177
178
|
patch: { type: "string", description: "Unified diff. May span multiple files (multiple --- / +++ header pairs — including creating MULTIPLE new files via --- /dev/null); --- / +++ headers per file, @@ -old,count +new,count @@ hunks (a bare @@ header is also accepted — coordinate-less hunks are located by their anchor lines: context lines plus the removed (-) lines, matched as a contiguous sequence — a unique match applies; a zero/one-context hunk is accepted only when it removes (-) at least one line and that anchor sequence is unique, while anchor-free pure-+ (insert) hunks need at least 2 context lines). The +++ b/<path> pair may be omitted for existing files — a lone --- a/<path> (or --- b/<path>) header followed directly by hunks applies to that path (new files still need --- /dev/null + +++ b/<path>)." },
|
|
179
|
+
ledger: { type: "string", description: LEDGER_PARAM_DESC },
|
|
178
180
|
},
|
|
179
181
|
required: ["patch"],
|
|
180
182
|
},
|
|
@@ -234,7 +236,13 @@ export const applyPatchTool = {
|
|
|
234
236
|
const summary = planned.map((p) => ` ${p.isNew ? "created " : "modified"} ${p.path}`).join("\n")
|
|
235
237
|
// Mark every touched file dirty — insert_after must not run on stale line numbers.
|
|
236
238
|
// 整批记账(dirty)= apply_patch 的批级语义(两段式全成功后一次落账——与改前逐字同)。
|
|
237
|
-
|
|
239
|
+
// 2026-11-02:落 dirty 同时清写入快照——apply_patch 不记快照,留着旧 `startLine` 就是
|
|
240
|
+
// 陈旧的锚:行号锚护栏第 2 行(dirty ∧ 快照在场)会拒,而第 4 行会拿旧值判「目标区在之前
|
|
241
|
+
// ⇒ 放行」——两种结果都不该由一笔没更新快照的写来决定(EDIT-HELPERS §7.1)。
|
|
242
|
+
for (const p of planned) {
|
|
243
|
+
markDirty(p.abs)
|
|
244
|
+
clearLastWrite(p.abs)
|
|
245
|
+
}
|
|
238
246
|
const syntaxChecks = await Promise.all(planned.map(async (p) => {
|
|
239
247
|
const r = await autoSyntaxCheck(p.abs)
|
|
240
248
|
return r ? `${p.path}:${r.replace("Syntax: ", "")}` : ""
|
|
@@ -256,6 +264,7 @@ export const deleteTool = {
|
|
|
256
264
|
properties: {
|
|
257
265
|
path: { type: "string", description: "File or empty-directory path (relative to cwd or absolute)" },
|
|
258
266
|
force: { type: "boolean", description: "Allow deleting git-tracked files (default false)" },
|
|
267
|
+
ledger: { type: "string", description: LEDGER_PARAM_DESC },
|
|
259
268
|
},
|
|
260
269
|
required: ["path"],
|
|
261
270
|
},
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* process-doc-mark.mjs — 工具输出里的过程档标记(单源小档;机制 = `docs/core/design/TOOLS.md` §6.28)。
|
|
3
|
+
*
|
|
4
|
+
* 依赖面说清楚:本档**不**是零依赖叶档——谓词都来自判据单源 `../declaration.mjs`,而它的静态闭包
|
|
5
|
+
* 经 `memory/file-walk.mjs` 达 `memory/schema.mjs`(`node:sqlite`)。因此端壳里静态引本档 = 把那条链拉进静态闭包;
|
|
6
|
+
* 当前端装配面本已静态可达同一链(`setup-tooltable.mjs` → 核 `agent-tools/verify.mjs` → `conventions.mjs`),
|
|
7
|
+
* 零新增风险,但别把它当零依赖档随手引。
|
|
8
|
+
*
|
|
9
|
+
* 一句话:输出里**判定得出**来源档落在索引排除集(batches 段 ∥ `_archive` 前缀)的行,
|
|
10
|
+
* 行首加 `[process doc: not a design basis]`(原行整体后随);其余行一个字节不动。**不是门**——
|
|
11
|
+
* 不拦、不筛、不改命中集,只加可读信息。
|
|
12
|
+
*
|
|
13
|
+
* 两套判据、一份文案、一个谓词、一个 helper:
|
|
14
|
+
* - `grep` 面按命中行的**档**判(消费面直调 `isProcessDocPath`——grep 自己已有 `h.file`,本档不参与拼装);
|
|
15
|
+
* - 命令面(`bash` 同步 ∥ `bash` 后台摘要 ∥ `execute` ∥ VSC 端 `bash`)的输出是任意文本,只能按
|
|
16
|
+
* **行首路径 token 真解析到盘上**判(`markProcessDocLines`)——解析不到 = 判不了 = 不动输出。
|
|
17
|
+
*
|
|
18
|
+
* 判据单源 = `../declaration.mjs` 的 `isExcludedRelPath`(面① 前缀 ∪ 面⑥ 段序列):本档零新排除名单、
|
|
19
|
+
* 零新谓词。第三参必须传 `decl.root`——`isExcludedRelPath` 内 `resolve(base, s)` 对绝对参恒等,
|
|
20
|
+
* 再由 `relative(root, …)` 换成根相对面,面① 前缀才判得住(只被面① 命中的 `_archive` 靠这一点)。
|
|
21
|
+
*
|
|
22
|
+
* 零 console 噪声:本档不调用 `console`。判据只走 `declarationForTarget` → `loadProjectDeclaration`,
|
|
23
|
+
* 后者按档缓存,只在声明档不可用 / 读不出时 warn(既有降级面,非本档新增)。
|
|
24
|
+
*/
|
|
25
|
+
import { existsSync } from "node:fs"
|
|
26
|
+
import { isAbsolute, resolve } from "node:path"
|
|
27
|
+
import { declarationForTarget, isExcludedRelPath } from "../declaration.mjs"
|
|
28
|
+
|
|
29
|
+
/** 逐字文案(33 字符 · 全 ASCII · 方括号闭合):工具输出面全英文,同一行混中英破坏「一词一物」。 */
|
|
30
|
+
export const PROCESS_DOC_MARK = "[process doc: not a design basis]"
|
|
31
|
+
|
|
32
|
+
/** 末尾汇总行(仅命令面):N = 本次输出被判为过程档的行数;N = 0 不加(`grep` 面不加此形)。 */
|
|
33
|
+
export const processDocSummary = (n) =>
|
|
34
|
+
`[process doc: ${n} of this output's lines come from process docs — not a design basis]`
|
|
35
|
+
|
|
36
|
+
/** 行首 token 的字符帽(步骤 ①):超过即成不了路径,该行跳过不解析。 */
|
|
37
|
+
const MAX_TOKEN_CHARS = 260
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 档判据(一次调用内按档记忆:`Map<absoluteFile, boolean>`)。
|
|
41
|
+
* @param {string} absFile 档的绝对路径(grep 面 = 命中行的 `h.file`;命令面 = 盘上解析出的绝对路径)
|
|
42
|
+
* @param {Map<string, boolean>} [cache] 记忆表(缺省自建——不传就没有跳调用的记忆)
|
|
43
|
+
* @returns {boolean} 所属档在索引排除集内 ⇒ true;出辖(祖先链无 manifest)⇒ false(无档类可判)
|
|
44
|
+
*/
|
|
45
|
+
export function isProcessDocPath(absFile, cache) {
|
|
46
|
+
const key = String(absFile ?? "")
|
|
47
|
+
if (!key) return false
|
|
48
|
+
const memo = cache instanceof Map ? cache : new Map() // 缺省自建:一次调用内按档记忆
|
|
49
|
+
if (memo.has(key)) return memo.get(key)
|
|
50
|
+
const decl = declarationForTarget(key) // null = 出辖(场景:会话锚在无 manifest 的目录里跑检索)
|
|
51
|
+
const marked = decl ? isExcludedRelPath(key, decl, decl.root) : false
|
|
52
|
+
memo.set(key, marked)
|
|
53
|
+
return marked
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** 剥两侧成对引号(步骤 ①)——行首字段被引起来的输出很常见。 */
|
|
57
|
+
function stripPairedQuotes(s) {
|
|
58
|
+
if (s.length < 2) return s
|
|
59
|
+
const first = s[0]
|
|
60
|
+
const last = s[s.length - 1]
|
|
61
|
+
if ((first === '"' && last === '"') || (first === "'" && last === "'")) return s.slice(1, -1)
|
|
62
|
+
return s
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 行首路径 token(步骤 ①–③)。判不了 ⇒ null(调用方原样保留该行)。
|
|
67
|
+
* @param {string} line 输出行
|
|
68
|
+
* @returns {string|null}
|
|
69
|
+
*/
|
|
70
|
+
function headPathToken(line) {
|
|
71
|
+
const head = /^\S+/.exec(String(line ?? "").trimStart())
|
|
72
|
+
if (!head) return null // 空行 / 纯空白行
|
|
73
|
+
const raw = stripPairedQuotes(head[0])
|
|
74
|
+
if (!raw || raw.length > MAX_TOKEN_CHARS) return null
|
|
75
|
+
// ② 剥尾:内层 `grep` ∥ `rg` ∥ `findstr` 的 `path:12: text` 形与尾随冒号一并剥掉
|
|
76
|
+
const token = raw.replace(/[:\-]\d+[:\-]?$/, "").replace(/:+$/, "")
|
|
77
|
+
if (!token) return null
|
|
78
|
+
// ③ 预筛:不含 `/` `\` `.` 的字段不做盘上解析(`[stdout]:` ∥ `(exit code 0)` ∥ 纯数字过不了这一关)
|
|
79
|
+
if (!/[/\\.]/.test(token)) return null
|
|
80
|
+
return token
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 盘上解析(步骤 ④):驱动绝对形 → MSYS 形 → 相对基准 `base`,取**第一个在盘上存在**者。
|
|
85
|
+
* 三支都不存在 ⇒ null(= 判不了)。UNC(`\\server\share\…`)走驱动绝对形一支。
|
|
86
|
+
* @param {string} token 行进字段(已剥尾)
|
|
87
|
+
* @param {string} [base] 解析基准:`bash` 同步 = `ctx.cwd` ∥ 后台径 = entry 记下的 `cwd` ∥ `execute` = `baseDir`
|
|
88
|
+
* @returns {string|null}
|
|
89
|
+
*/
|
|
90
|
+
function resolveOnDisk(token, base) {
|
|
91
|
+
const candidates = []
|
|
92
|
+
if (/^[A-Za-z]:[\\/]/.test(token) || token.startsWith("\\\\")) {
|
|
93
|
+
candidates.push(token) // 驱动绝对形 ∥ UNC
|
|
94
|
+
} else {
|
|
95
|
+
// MSYS 形(`/c/teamcode/…`)在 win32 上 `isAbsolute` 也为真,会落成当前盘的 `/c/…`
|
|
96
|
+
// ⇒ 必须显式把首段 `^/([A-Za-z])/` 映射为 `X:/`,否则这一支恒不命中。
|
|
97
|
+
const msys = /^\/([A-Za-z])(\/.*)?$/.exec(token)
|
|
98
|
+
if (msys) candidates.push(`${msys[1].toUpperCase()}:${msys[2] ?? "/"}`)
|
|
99
|
+
else if (isAbsolute(token)) candidates.push(token) // POSIX 绝对形(`resolve` 对绝对参恒等,同一支)
|
|
100
|
+
}
|
|
101
|
+
if (base) candidates.push(resolve(String(base), token))
|
|
102
|
+
for (const c of candidates) if (existsSync(c)) return c
|
|
103
|
+
return null
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 命令面打标(§6.28「命令输出面」六步,`bash` ∥ `execute` ∥ VSC 端 `bash` 共用):
|
|
108
|
+
* 逐行取行首 token → 真解析到盘上 → 判归属 → 命中者行首加前缀(行其余部分原样)。
|
|
109
|
+
* N > 0 ⇒ 正文之后追加一行汇总;N = 0 ⇒ **原样返回入参**(一个字节不动,无空汇总行)。
|
|
110
|
+
*
|
|
111
|
+
* 纪律:不许按「看起来像路径」打标——解析不到盘上就不标(误标比不标更坏)。
|
|
112
|
+
*
|
|
113
|
+
* @param {string} text 输出文本
|
|
114
|
+
* @param {string} [base] 解析基准(见 `resolveOnDisk`)
|
|
115
|
+
* @returns {string}
|
|
116
|
+
*/
|
|
117
|
+
export function markProcessDocLines(text, base) {
|
|
118
|
+
const src = String(text ?? "")
|
|
119
|
+
if (!src) return src
|
|
120
|
+
const fileCache = new Map() // 档 → boolean
|
|
121
|
+
const tokenCache = new Map() // token → boolean(一次调用内按 token 记忆;不设解析上限)
|
|
122
|
+
let n = 0
|
|
123
|
+
const out = src.split("\n").map((line) => {
|
|
124
|
+
const token = headPathToken(line)
|
|
125
|
+
if (!token) return line
|
|
126
|
+
let marked = tokenCache.get(token)
|
|
127
|
+
if (marked === undefined) {
|
|
128
|
+
const abs = resolveOnDisk(token, base)
|
|
129
|
+
marked = abs ? isProcessDocPath(abs, fileCache) : false
|
|
130
|
+
tokenCache.set(token, marked)
|
|
131
|
+
}
|
|
132
|
+
if (!marked) return line
|
|
133
|
+
n += 1
|
|
134
|
+
return `${PROCESS_DOC_MARK} ${line}`
|
|
135
|
+
})
|
|
136
|
+
if (n === 0) return src
|
|
137
|
+
const body = out.join("\n")
|
|
138
|
+
return `${body}${body.endsWith("\n") ? "" : "\n"}${processDocSummary(n)}`
|
|
139
|
+
}
|