@thincoder/core 0.10.6 → 0.10.8
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 +24 -0
- package/advisor/compaction.mjs +1 -1
- package/advisor/run.mjs +1 -1
- package/agent/chat-call.mjs +40 -27
- package/agent/child-marks.mjs +1 -1
- package/agent/dispatch-gates.mjs +7 -4
- package/agent/dispatch-run.mjs +1 -0
- package/agent/family-tools.mjs +11 -6
- package/agent/helpers.mjs +6 -4
- package/agent/image-heal.mjs +65 -0
- package/agent/light-round.mjs +4 -4
- package/agent/live-beat.mjs +4 -4
- package/agent/record-results.mjs +9 -1
- package/agent/run-stages.mjs +2 -1
- package/agent/setup-reminders.mjs +14 -13
- package/agent/setup.mjs +62 -46
- package/agent/spawn-child.mjs +1 -1
- package/agent/subagent-event.mjs +1 -1
- package/agent/suspension.mjs +2 -1
- package/agent/timers.mjs +8 -8
- package/agent/turn-loop.mjs +1 -1
- package/agent-tools/advisor.mjs +1 -1
- package/agent-tools/async-discard.mjs +40 -5
- package/agent-tools/async-settle.mjs +3 -1
- package/agent-tools/audit-block.mjs +1 -1
- package/agent-tools/batch-paths.mjs +20 -20
- package/agent-tools/checkpoint.mjs +1 -1
- package/agent-tools/design-slots.mjs +44 -5
- package/agent-tools/doc-check.mjs +37 -0
- package/agent-tools/eng.mjs +5 -3
- package/agent-tools/manifest.mjs +25 -18
- package/agent-tools/parent-channel.mjs +3 -3
- package/agent-tools/review-facts.mjs +2 -2
- package/agent-tools/settings.mjs +1 -1
- package/agent-tools/spawn-gates.mjs +8 -6
- package/agent-tools/ssh-session.mjs +439 -0
- package/agent-tools/ssh-transport.mjs +311 -0
- package/agent-tools/subagent-scheduler.mjs +6 -5
- package/agent-tools/subagent.mjs +7 -5
- package/agent-tools/task.mjs +1 -1
- package/agent-tools/timer.mjs +6 -0
- package/agent-tools/zero-write-watch.mjs +2 -2
- package/agent-tools.mjs +3 -2
- package/attachments.mjs +4 -4
- package/browser/cdp.mjs +2 -2
- package/browser/launch.mjs +50 -4
- package/browser/snapshot.mjs +1 -1
- package/config-io.mjs +7 -2
- package/config-watch.mjs +2 -2
- package/config.mjs +7 -0
- package/conventions.mjs +1 -1
- package/declaration.mjs +157 -13
- package/doc-check/anchors.mjs +100 -0
- package/doc-check/index.mjs +61 -0
- package/doc-check/width.mjs +131 -0
- package/file-refs.mjs +6 -6
- package/generate-title.mjs +1 -1
- package/hooks.mjs +1 -1
- package/i18n.mjs +11 -1
- package/ledger-cmd.mjs +4 -5
- package/ledger-db.mjs +100 -21
- package/ledger-migrate.mjs +3 -3
- package/ledger-read.mjs +6 -7
- package/ledger-tools.mjs +24 -9
- package/ledger.mjs +23 -54
- package/log.mjs +17 -3
- package/manifest-discovery.mjs +21 -18
- package/manifest-schema.mjs +2 -13
- package/manifest.mjs +37 -22
- package/mcp/transport-stdio.mjs +1 -1
- package/mcp.mjs +2 -2
- package/memory/code-search.mjs +21 -4
- package/memory/code-sync.mjs +7 -4
- package/memory/core.mjs +1 -1
- package/memory/docs.mjs +23 -6
- package/memory/file-list.mjs +13 -7
- package/memory/file-walk.mjs +9 -4
- package/memory/scan.mjs +1 -1
- package/memory/sweep.mjs +1 -1
- package/memory/sync-tail.mjs +11 -7
- package/model-specs.mjs +7 -7
- package/notify-policy.mjs +6 -6
- package/package.json +2 -1
- package/permission.mjs +14 -0
- package/prompts/advisor-design.md +43 -13
- package/prompts/advisor-round1.md +25 -5
- package/prompts/advisor-round2.md +17 -4
- package/prompts/advisor-round3.md +17 -4
- package/prompts/common.md +60 -6
- package/prompts/consult-base.md +8 -2
- package/prompts/discipline-engineering.md +11 -10
- package/prompts/discipline-normal.md +22 -14
- package/prompts/persona-coder.md +1 -1
- package/prompts/persona-eng-designer.md +8 -3
- package/prompts/persona-engineering.md +21 -5
- package/prompts/persona-normal.md +4 -1
- package/provider/errors.mjs +8 -0
- package/provider/google.mjs +3 -0
- package/provider/normalize.mjs +42 -12
- package/provider/responses-request.mjs +1 -1
- package/provider/responses.mjs +2 -2
- package/provider/wait-status.mjs +1 -1
- package/provider-flows.mjs +1 -1
- package/queued.mjs +2 -2
- package/rules.mjs +4 -4
- package/session-gc.mjs +1 -1
- package/session-lifecycle.mjs +1 -1
- package/session-slot-write.mjs +2 -2
- package/session-slots-manifest.mjs +1 -1
- package/session.mjs +1 -1
- package/shell-candidates.mjs +2 -2
- package/shell-identity.mjs +1 -1
- package/team.mjs +61 -4
- package/think-off.mjs +3 -3
- package/token-ttl.mjs +1 -1
- package/tool-docs/bash.md +1 -1
- package/tool-docs/browser.md +1 -1
- package/tool-docs/doc_check.md +31 -0
- package/tool-docs/manifest.md +14 -2
- package/tool-docs/ssh.md +34 -0
- package/tool-docs/subagent.md +2 -0
- package/tool-docs/timer.md +2 -1
- package/tools/browser.mjs +1 -1
- package/tools/execute.mjs +1 -1
- package/tools/git-run.mjs +2 -2
- package/tools/git.mjs +8 -8
- package/tools/index.mjs +3 -0
- package/tools/ops.mjs +1 -1
- package/tools/process-tree.mjs +2 -2
- package/tools/ssh-askpass.mjs +35 -0
- package/tools/ssh.mjs +92 -0
- package/vision-reader.mjs +4 -4
package/manifest-schema.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* manifest-schema.mjs — **校验 ∕ 默认值族**出档(2026-09-29 structure-split-2 · 台账 #620):自 `manifest.mjs`
|
|
3
|
-
*
|
|
4
|
-
* `DEFAULT_MANIFEST`(默认八键)· `MANIFEST_SCHEMA`(判据单源)·
|
|
3
|
+
* 全量迁出——`isValidDocRootValue`(docRoot 子键值形态判据)·
|
|
4
|
+
* `DEFAULT_MANIFEST`(默认八键)· `MANIFEST_SCHEMA`(判据单源)· 元素层谓词一件 · `validateManifest` ·
|
|
5
5
|
* `fillDefaults`;**结构拆分零语义**(面不变 ∕ 判据不变,只换宿主档;切点 ∕ 缝单源 =
|
|
6
6
|
* 批档 `docs/batches/2026-09-29-structure-split-2.md` §2.2-C)。
|
|
7
7
|
*
|
|
@@ -43,7 +43,6 @@ export const DEFAULT_MANIFEST = Object.freeze({
|
|
|
43
43
|
widthExemptZones: Object.freeze([]),
|
|
44
44
|
anchors: Object.freeze({ domain: "docs", exclude: Object.freeze(["_archive", "batches"]) }),
|
|
45
45
|
exemptions: Object.freeze([]),
|
|
46
|
-
lineCounts: Object.freeze([]), // 行数面机检声明(#546):[] = 未载惰性(doc-check 行数族)
|
|
47
46
|
}),
|
|
48
47
|
// 项目声明三族(2026-09-27 声明载体唯一化批并入——键名 / 形态逐字承前;来源档已退役)。
|
|
49
48
|
codePaths: Object.freeze(["src"]),
|
|
@@ -72,12 +71,6 @@ function isNonEmptyStringArray(v) {
|
|
|
72
71
|
return Array.isArray(v) && v.every((e) => typeof e === "string" && e.trim() !== "")
|
|
73
72
|
}
|
|
74
73
|
|
|
75
|
-
/** `checkConfig.lineCounts` 元素层判据(#546):数组且各元素为非数组对象、`doc` / `section` 皆非空串(`trim` 后非空);`[]` = 未载惰性。 */
|
|
76
|
-
function isLineCountsValue(v) {
|
|
77
|
-
return Array.isArray(v) && v.every((e) => e !== null && typeof e === "object" && !Array.isArray(e)
|
|
78
|
-
&& typeof e.doc === "string" && e.doc.trim() !== "" && typeof e.section === "string" && e.section.trim() !== "")
|
|
79
|
-
}
|
|
80
|
-
|
|
81
74
|
/**
|
|
82
75
|
* 形状校验(模块设计 §2.2)——枚举 / version 数值恒做;**纯函数、零 fs**(原 `{ cwd }`
|
|
83
76
|
* 指针分支已随字段整链裁撤——KD-M1-5 墓志);不落盘。键存在性入 missingKeys(非拒):
|
|
@@ -116,10 +109,6 @@ export function validateManifest(obj) {
|
|
|
116
109
|
if (nested === "docRoot" && !isValidDocRootValue(value[sub])) {
|
|
117
110
|
errors.push(`docRoot.${sub} 值形态非法:${JSON.stringify(value[sub])}(须为非空字符串 / 非空字符串数组 / null(本面无)——KD-M1-6 / KD-M1-7 / KD-M1-37)`)
|
|
118
111
|
}
|
|
119
|
-
// checkConfig.lineCounts 元素层形态(#546):数组,元素 = { doc, section } 非空串;非法 → 拒(不静默跳过)。
|
|
120
|
-
if (nested === "checkConfig" && sub === "lineCounts" && !isLineCountsValue(value[sub])) {
|
|
121
|
-
errors.push(`checkConfig.lineCounts 值形态非法:${JSON.stringify(value[sub])}(须为数组,元素 = { doc: 非空串, section: 非空串 }——#546)`)
|
|
122
|
-
}
|
|
123
112
|
}
|
|
124
113
|
}
|
|
125
114
|
// 三族声明键形态(KD-M1-32——fail-closed:非法即 errors,不静默跳过;缺键仍在 missingKeys)。
|
package/manifest.mjs
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* 未知键(#802 / KD-M1-36)→ 产 unknownKeys + 一行可见告警(console.warn + logEvent;
|
|
11
11
|
* 同档同键集去重、读到零未知键清该档 memo)——非拒定性零变。
|
|
12
12
|
* - validateManifest(obj) → { ok, errors, missingKeys, unknownKeys }:枚举 / version 数值 /
|
|
13
|
-
* docRoot 子键值形态(串 | 数组 | null(本面无——KD-M1-37),F7
|
|
13
|
+
* docRoot 子键值形态(串 | 数组 | null(本面无——KD-M1-37),F7)——**纯函数、零 fs**;不落盘。
|
|
14
14
|
* - 三族声明键(`codePaths` / `index.{codeExtensions,docExtensions,publicRepos}` / `advisor.{docMap,standardsDoc}`——
|
|
15
15
|
* KD-M1-31 / M1-32):缺键补默认;形态错 = 档非法(fail-closed,与 docRoot 子键同款);
|
|
16
16
|
* 读向 = `declaration.mjs`(2026-10-01 自 `conventions.mjs` 迁出——经其转口可达)经 `readManifest` /
|
|
@@ -25,10 +25,10 @@
|
|
|
25
25
|
* - projectView(target) → { state, root, path, manifest?, candidates?, errors?, matched }:**按用点解析**
|
|
26
26
|
* 的读侧单点(归属 ∨ 发现兜底 / 五态 / 非抛错 / **零写**——KD-M1-24)。
|
|
27
27
|
* - resolveProjectRoot(cwd):项目根(归属 ∨ 发现——KD-M1-30)= `projectRootView(cwd).root`(薄委托);
|
|
28
|
-
* projectRootView(cwd) → { state: ok|
|
|
28
|
+
* projectRootView(cwd) → { state: ok|workspace|none, root, candidates }:项目根直读形(判定单点——§2.2)。
|
|
29
29
|
* - resolveEngineeringManifest(cwd, { writer, init }) → 入口决策树(**非抛错**——KD-M1-20):
|
|
30
30
|
* 两端入口钩子与翻转面(拒翻)共用同一张树(判据单源;§2.8 F1)——失败码五枚
|
|
31
|
-
* `missing` / `invalid` / `no-project` / `
|
|
31
|
+
* `missing` / `invalid` / `no-project` / `workspace` / `init-failed`(KD-M1-28)。名中 `Engineering` = 历史命名(沿用不改名);消费面 = 两端入口钩子 + 翻转面——**非**「模式拥有本模块」。
|
|
32
32
|
* - manifestFilePath(cwd) → 数据档绝对路径(档路径单源 KD-M1-18——读 / 写 / mtime 门控三处同源)。
|
|
33
33
|
* - initManifest(cwd, { writer = 'subagent' } = {}) → 经写门写 DEFAULT_MANIFEST,缺省拒。
|
|
34
34
|
* - writeManifest(cwd, manifest, { writer = 'subagent' } = {}) → 落盘前先校验(ok:false 拒
|
|
@@ -53,14 +53,14 @@ export { DEFAULT_MANIFEST, MANIFEST_SCHEMA, isValidDocRootValue, validateManifes
|
|
|
53
53
|
* · **归属(第一段——§⑥ 归属形)**:`owningProject(target)` 沿祖先链取最近带档目录
|
|
54
54
|
* (子优于根 / 不跨兄弟 / 无全局优先级)⇒ 读该档 ⇒ `ok` / `invalid`。
|
|
55
55
|
* · **发现兜底(第二段——§⑥ 发现规则,纯向下)**:祖先链无档 ⇒ `discoverProjects(target)` ⇒ 命中
|
|
56
|
-
* (`self` / `unique`)⇒ 读其档(带档 ⇒ `ok` / `invalid`;缺档 ⇒ `missing`);`
|
|
57
|
-
* `
|
|
56
|
+
* (`self` / `unique`)⇒ 读其档(带档 ⇒ `ok` / `invalid`;缺档 ⇒ `missing`);`workspace` ⇒
|
|
57
|
+
* `workspace`(+ `candidates`);`none` ⇒ `no-project`。
|
|
58
58
|
* 写侧(建档)**不在**此函数(归入口决策树——KD-M1-25 / M1-29)⇒ 注入器 / 只读消费面不会变写点。
|
|
59
59
|
* `matched` 契约:归属段命中 ⇒ `'manifest'`;发现段 ⇒ `discoverProjects.matched` 逐字;
|
|
60
|
-
* `no-project`(/ 覆盖位短路)⇒ `null
|
|
60
|
+
* `no-project`(/ 覆盖位短路)⇒ `null`——报明行工作区变体按它分野(§2.6 条 1b)。
|
|
61
61
|
* 读错(非 ENOENT——权限 / 目录等)收为 `invalid`(非抛错契约;`readManifest` 语义零改)。
|
|
62
62
|
* @param {string} [target] 目标路径(目录 / 文件——动作作用于哪个项目的路径)
|
|
63
|
-
* @returns {{state:'ok'|'missing'|'no-project'|'
|
|
63
|
+
* @returns {{state:'ok'|'missing'|'no-project'|'workspace'|'invalid', root:string|null, path:string|null,
|
|
64
64
|
* manifest?:object, candidates?:string[], errors?:string[], matched:'manifest'|'git'|null}}
|
|
65
65
|
*/
|
|
66
66
|
export function projectView(target) {
|
|
@@ -68,7 +68,7 @@ export function projectView(target) {
|
|
|
68
68
|
const owning = owningProject(anchor)
|
|
69
69
|
if (owning) return viewAtRoot(owning, "manifest")
|
|
70
70
|
const d = discoverProjects(anchor)
|
|
71
|
-
if (d.kind === "
|
|
71
|
+
if (d.kind === "workspace") return { state: "workspace", root: null, path: null, candidates: d.candidates, matched: d.matched }
|
|
72
72
|
if (d.kind === "none") return { state: "no-project", root: null, path: null, matched: null }
|
|
73
73
|
return viewAtRoot(d.root, d.matched)
|
|
74
74
|
}
|
|
@@ -187,6 +187,16 @@ export function requireManifest(cwd) {
|
|
|
187
187
|
return readManifest(cwd)
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
/** 建档(init 支)成功后的同步通知位(#1376——**反向注册**:`declaration.mjs` 已向依赖本模块,
|
|
191
|
+
* 订阅方由对侧注册 ⇒ 本模块保持零出边(MANIFEST.md KD-M1-33);形同 `config-io.mjs` `onConfigSelfWrite`
|
|
192
|
+
* ——返回退订函数;订阅方失败不影响建档)。 */
|
|
193
|
+
const manifestInitFns = new Set()
|
|
194
|
+
export function onManifestInit(fn) {
|
|
195
|
+
if (typeof fn !== "function") return () => {}
|
|
196
|
+
manifestInitFns.add(fn)
|
|
197
|
+
return () => { manifestInitFns.delete(fn) }
|
|
198
|
+
}
|
|
199
|
+
|
|
190
200
|
/**
|
|
191
201
|
* 入口决策树(**非抛错**形态——KD-M1-20;docs/core/design/MANIFEST.md §2.8 F1):三面共用——
|
|
192
202
|
* ① CLI 装配 / 重估薄包装 ② VSC `hydrateRun` 钩子块 ③ 翻转面(`eng` 工具 / `/eng` /
|
|
@@ -196,22 +206,22 @@ export function requireManifest(cwd) {
|
|
|
196
206
|
* 分支(两分支六出口——§2.8 F1 树):
|
|
197
207
|
* 档合法 → { ok:true, manifest, created:false }(manifest = 补默认值后的档内容)
|
|
198
208
|
* 缺档 + init → 梯②④⑤(锚 = 裸仓 / 裸仓命中 / 无项目)⇒ `initManifest(cwd, { writer })`
|
|
199
|
-
* (抛错 → `init-failed
|
|
200
|
-
* message, candidates }`(**不建 / 不猜**)
|
|
209
|
+
* (抛错 → `init-failed`);工作区(锚自身无档 ∧ 下辖 ≥2 项目)⇒
|
|
210
|
+
* `{ ok:false, code:'workspace', message, candidates }`(**不建 / 不猜**)
|
|
201
211
|
* 缺档 + !init → 梯⑤(无项目)⇒ `{ ok:false, code:'no-project' }`(KD-M1-28;`init:false` 面);
|
|
202
|
-
* 梯②④(裸仓可解析、档缺)⇒ `{ ok:false, code:'missing' }
|
|
212
|
+
* 梯②④(裸仓可解析、档缺)⇒ `{ ok:false, code:'missing' }`;工作区 ⇒ 同上
|
|
203
213
|
* 档非法 → { ok:false, code:'invalid', message, errors }
|
|
204
214
|
*
|
|
205
215
|
* `writer` 由调用点**显式**传(生产调用点全传 `'main'`):缺省 `'subagent'` 是写门 fail-closed
|
|
206
216
|
* 缺省(KD-M1-3),误用缺省 ⇒「缺档 + 梯②④⑤」退化为 `init-failed`(拒翻——与 §2.8 F2 /
|
|
207
217
|
* AC-20② 语义相反;T38 反证格)。
|
|
208
|
-
*
|
|
209
|
-
* 「项目不可解析」(稳定锚句
|
|
218
|
+
* 失败码文案分野(KD-M1-28):无项目 ⇒ 可在锚处落地 / 工作区 ⇒ 报明事实 + 点名项目根;文案族 =
|
|
219
|
+
* 「项目不可解析」(稳定锚句 `/项目不可解析/`)——**仅「档非法」格**(§2.8 F2 拒翻面唯一拒格)。
|
|
210
220
|
* 边界:`readManifest` 的非 ENOENT 读错(权限 / 目录等)由 `projectView` 收为 `invalid`(非抛错
|
|
211
221
|
* 契约——`readManifest` 返回语义零改);本树只承诺上列六出口。
|
|
212
222
|
* @param {string} [cwd] 会话锚(缺省 → 进程 cwd)
|
|
213
223
|
* @returns {{ok:true, manifest:object, created:boolean}
|
|
214
|
-
* | {ok:false, code:'missing'|'invalid'|'no-project'|'
|
|
224
|
+
* | {ok:false, code:'missing'|'invalid'|'no-project'|'workspace'|'init-failed',
|
|
215
225
|
* message?:string, errors?:string[], candidates?:string[]}}
|
|
216
226
|
*/
|
|
217
227
|
export function resolveEngineeringManifest(cwd, { writer = "subagent", init = true } = {}) {
|
|
@@ -223,24 +233,29 @@ export function resolveEngineeringManifest(cwd, { writer = "subagent", init = tr
|
|
|
223
233
|
message: `项目不可解析:${MANIFEST_REL} 非法(fail-closed):${(view.errors ?? []).join(";")}`,
|
|
224
234
|
}
|
|
225
235
|
}
|
|
226
|
-
if (view.state === "
|
|
227
|
-
return { ok: false, code: "
|
|
236
|
+
if (view.state === "workspace") {
|
|
237
|
+
return { ok: false, code: "workspace", candidates: view.candidates, message: workspaceProjectMessage(cwd, view.candidates) }
|
|
228
238
|
}
|
|
229
239
|
// 缺档(梯②④⑤):init ⇒ 就地建档(内容 = DEFAULT_MANIFEST,经写门 writer:'main');!init ⇒ 归码。
|
|
230
240
|
if (!init) return { ok: false, code: view.state === "no-project" ? "no-project" : "missing" }
|
|
231
241
|
try {
|
|
232
|
-
|
|
242
|
+
const manifest = initManifest(cwd, { writer })
|
|
243
|
+
// #1376:建档支成功 ⇒ 同步广播(进程内声明 ∥ 范围缓存即刻随动——如 eng 翻转面结束后
|
|
244
|
+
// 同进程的声明复读;订阅方失败不阻断建档;失败路径不广播)。
|
|
245
|
+
for (const fn of manifestInitFns) { try { fn() } catch (e) { console.warn(`[manifest] init subscriber failed: ${e?.message ?? e}`) } }
|
|
246
|
+
return { ok: true, manifest, created: true }
|
|
233
247
|
} catch (e) {
|
|
234
248
|
return { ok: false, code: "init-failed", message: e?.message ?? String(e) }
|
|
235
249
|
}
|
|
236
250
|
}
|
|
237
251
|
|
|
238
|
-
/**
|
|
239
|
-
*
|
|
240
|
-
|
|
252
|
+
/** 工作区消息(**事实行**——§2.5「单项目解析律」条 / §2.8 F2 工作区行):念明下辖项目**全列**
|
|
253
|
+
* (绝对路径、按名排序)+ 可执行下一步(点名项目根 / 目标路径)——报明事实,非错误框定
|
|
254
|
+
* (不书「项目不可解析」/ 候选二义)。 */
|
|
255
|
+
function workspaceProjectMessage(cwd, candidates) {
|
|
241
256
|
const list = (candidates ?? []).map((c) => `- ${c}`).join("\n")
|
|
242
|
-
return
|
|
243
|
-
`每个项目一份 ${MANIFEST_REL}
|
|
257
|
+
return `工作区:会话锚 ${cwd} 自身无 ${MANIFEST_REL},下辖 ${(candidates ?? []).length} 个项目(下列全部)——` +
|
|
258
|
+
`每个项目一份 ${MANIFEST_REL};请显式给出目标项目根或目标路径(动作按用点路径解析):\n${list}`
|
|
244
259
|
}
|
|
245
260
|
|
|
246
261
|
/**
|
package/mcp/transport-stdio.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { spawn, spawnSync } from "node:child_process"
|
|
|
5
5
|
import { rpcId, CALL_TIMEOUT_MS, withTimeout, quoteArg } from "./helpers.mjs"
|
|
6
6
|
|
|
7
7
|
/** 退出相位标志(D-MC19):模块加载期注册的 exit 钩子置位——退出相位无事件循环,
|
|
8
|
-
* 只同步操作可落地(注册序恒早于宿主 cleanup——`thincoder-cli/src/tui/index.mjs
|
|
8
|
+
* 只同步操作可落地(注册序恒早于宿主 cleanup——`thincoder-cli/src/tui/index.mjs` 的 `createExitCleanup`)。 */
|
|
9
9
|
let exiting = false
|
|
10
10
|
process.on("exit", () => { exiting = true })
|
|
11
11
|
|
package/mcp.mjs
CHANGED
|
@@ -93,14 +93,14 @@ async function createConnectedTransport(rawConfig, serverName) {
|
|
|
93
93
|
* 零副作用:不进 _sessions、不动 agent.tools、无 onDead 挂钩;finally close
|
|
94
94
|
* (closed=true 使 onDead 重连不会触发)。initialize 与 tools/list 同受
|
|
95
95
|
* INIT_TIMEOUT_MS 约束(见 doInitialize)。不复用 connectMcpServer(避免污染
|
|
96
|
-
* session
|
|
96
|
+
* session 幂等表)。读数 = `{ ok, toolCount, tools, latencyMs }`(`tools` = 工具名数组——控制台具名读数,2026-10-14 走查轮增补)。 */
|
|
97
97
|
export async function probeMcpServer(config) {
|
|
98
98
|
const start = Date.now()
|
|
99
99
|
let transport
|
|
100
100
|
let mcpTools
|
|
101
101
|
try {
|
|
102
102
|
;({ transport, mcpTools } = await createConnectedTransport(config, config.name ?? config.command ?? config.url ?? config.wsUrl))
|
|
103
|
-
|
|
103
|
+
return { ok: true, toolCount: mcpTools.length, tools: mcpTools.map((t) => (typeof t?.name === "string" ? t.name : "")).filter((n) => n !== ""), latencyMs: Date.now() - start }
|
|
104
104
|
} catch (error) {
|
|
105
105
|
return { ok: false, error: error?.message ?? String(error) }
|
|
106
106
|
} finally {
|
package/memory/code-search.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { embed, embedTolerant, cosine, toBlob, fromBlob } from "../embedding.mjs"
|
|
9
9
|
import { scanVectors, createTopK } from "./scan.mjs"
|
|
10
10
|
import { normalizeOrigin } from "./origin.mjs"
|
|
11
|
-
import { declaredPublicRoots } from "../conventions.mjs"
|
|
11
|
+
import { declaredPublicRoots, loadProjectDeclaration, isExcludedRelPath, declaredIndexScope, isInDeclaredScope } from "../conventions.mjs"
|
|
12
12
|
import { buildFtsQuery, ensureEmbeddings, EMBED_TEXT_MAX_LEN } from "./core.mjs"
|
|
13
13
|
import { safeSliceUTF16 } from "../text-budget.mjs"
|
|
14
14
|
import { DESC } from "../tools/shared.mjs" // #15 描述外置:文本单点 = tool-docs/code_search.md
|
|
@@ -35,13 +35,28 @@ export async function codeSearch(memory, query, { limit = 5 } = {}) {
|
|
|
35
35
|
const origins = searchOrigins(memory)
|
|
36
36
|
const ftsOriginFilter = origins.length === 1 ? `AND c.origin = ?` : origins.length > 1 ? `AND c.origin IN (${origins.map(() => "?").join(", ")})` : ""
|
|
37
37
|
const originParams = origins
|
|
38
|
+
// §6.14 读面准入过滤(FTS 列表建成后 ∥ 向量 top-K push 前;两判据并列互不吞):排除谓词(面①∪面⑥,
|
|
39
|
+
// 行 origin 为 `base`)∪ 声明范围门(面⑦——`isInDeclaredScope(path, declaredIndexScope(origin), origin)`,
|
|
40
|
+
// 逐 origin 预载一次)⇒「批次档 ∥ 范围外路径从未入索引」等价检索;存量行不删(收尾保护位);窗口欠额接受。
|
|
41
|
+
const declCache = new Map()
|
|
42
|
+
const scopeCache = new Map()
|
|
43
|
+
const rowHidden = (origin, path) => {
|
|
44
|
+
const d = declCache.get(origin) ?? loadProjectDeclaration(origin)
|
|
45
|
+
declCache.set(origin, d)
|
|
46
|
+
if (isExcludedRelPath(path, d, origin)) return true
|
|
47
|
+
let scope = scopeCache.get(origin)
|
|
48
|
+
if (scope === undefined) { scope = declaredIndexScope(origin); scopeCache.set(origin, scope) }
|
|
49
|
+
return !isInDeclaredScope(path, scope, origin)
|
|
50
|
+
}
|
|
51
|
+
const ftsNeedOrigin = origins.length !== 1 // 单 origin 形 SQL 零改;多 origin ∥ 无过滤形补 `origin` 列(行 origin 随行可得)
|
|
38
52
|
|
|
39
53
|
const ftsList = ftsQuery ? memory.db.prepare(`
|
|
40
|
-
SELECT c.rowid, c.path, c.language, c.symbol_name, c.content, c.line_start, c.line_end, bm25(code_chunks_fts) AS rank
|
|
54
|
+
SELECT c.rowid, c.path, c.language, c.symbol_name, c.content, c.line_start, c.line_end, bm25(code_chunks_fts) AS rank${ftsNeedOrigin ? ", c.origin" : ""}
|
|
41
55
|
FROM code_chunks_fts JOIN code_chunks c ON c.rowid = code_chunks_fts.rowid
|
|
42
56
|
WHERE code_chunks_fts MATCH ? ${ftsOriginFilter}
|
|
43
57
|
ORDER BY rank LIMIT ?
|
|
44
|
-
`).all(ftsQuery, ...originParams, Math.max(limit * 4, 20))
|
|
58
|
+
`).all(ftsQuery, ...originParams, Math.max(limit * 4, 20))
|
|
59
|
+
.filter((r) => !rowHidden(origins.length === 1 ? origins[0] : r.origin, r.path)) : []
|
|
45
60
|
|
|
46
61
|
if (!memory.embedder) return ftsList.slice(0, limit)
|
|
47
62
|
|
|
@@ -70,7 +85,9 @@ export async function codeSearch(memory, query, { limit = 5 } = {}) {
|
|
|
70
85
|
for (const origin of origins.length ? origins : [null]) {
|
|
71
86
|
await scanVectors(memory.db, vecSql, origin ? [origin] : [], {
|
|
72
87
|
cursorKey,
|
|
73
|
-
onRow: (r) =>
|
|
88
|
+
onRow: (r) => { // §6.14 面⑥∪面⑦ 准入(top-K push 前;无过滤形行 origin = 游标键 `origin` 列)
|
|
89
|
+
if (!rowHidden(origin ?? r.origin, r.path)) top.push({ id: r.rowid, rowid: r.rowid, score: cosine(qvec, fromBlob(r.embedding)) })
|
|
90
|
+
},
|
|
74
91
|
})
|
|
75
92
|
}
|
|
76
93
|
const vecList = top.list().map((c) => ({ rowid: c.id, score: c.score }))
|
package/memory/code-sync.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import { ensureEmbeddings, invalidateStaleEmbeddings } from "./core.mjs"
|
|
|
11
11
|
import { detectLanguage, _upsertCodeFile, _upsertDocFile, yieldTick } from "./code-index.mjs"
|
|
12
12
|
import { isSkippedRelPath, extensionOf } from "./file-walk.mjs"
|
|
13
13
|
import { createRowBudget, rowCountOfPath, sweepStaleRows } from "./sync-tail.mjs"
|
|
14
|
-
import { loadProjectDeclaration, isExcludedRelPath, declaredPublicRoots } from "../conventions.mjs"
|
|
14
|
+
import { loadProjectDeclaration, isExcludedRelPath, declaredPublicRoots, declaredIndexScope, isInDeclaredScope } from "../conventions.mjs"
|
|
15
15
|
import { logEvent } from "../log.mjs"
|
|
16
16
|
import { indexExtensions, listProjectFiles } from "./file-list.mjs"
|
|
17
17
|
|
|
@@ -53,6 +53,7 @@ export async function gitSync(memory, dir, opts = {}) {
|
|
|
53
53
|
async function gitSyncDir(memory, dir, { onProgress } = {}) {
|
|
54
54
|
const origin = normalizeOrigin(dir) // §6.11 写缝归一(git / 文件 I/O 用原样 dir;库面 origin 一律归一值)
|
|
55
55
|
const decl = loadProjectDeclaration(dir)
|
|
56
|
+
const scope = declaredIndexScope(dir) // §6.14 面⑦ 范围门(diff 缝逐行判——见下)
|
|
56
57
|
const { execFile: _execFile } = await import("node:child_process")
|
|
57
58
|
const { code: codeExts, doc: docExts } = indexExtensions(dir)
|
|
58
59
|
const gitRun = (args) => new Promise((resolve, reject) => {
|
|
@@ -95,7 +96,8 @@ async function gitSyncDir(memory, dir, { onProgress } = {}) {
|
|
|
95
96
|
const abs = join(dir, rel)
|
|
96
97
|
const ext = extensionOf(rel)
|
|
97
98
|
|
|
98
|
-
|
|
99
|
+
// §6.14 准入三族同判(面④ 跳过段 ∥ 面①∪面⑥ 排除谓词 ∥ 面⑦ 声明范围):范围外 = 不收录
|
|
100
|
+
if (isSkippedRelPath(rel) || isExcludedRelPath(rel, decl, dir) || !isInDeclaredScope(rel, scope, dir)) continue
|
|
99
101
|
|
|
100
102
|
if (!codeExts.has(ext) && !docExts.has(ext)) { skipped++; continue }
|
|
101
103
|
|
|
@@ -210,8 +212,8 @@ async function codeSyncDir(memory, dir, { onProgress, yieldFn = yieldTick, nowFn
|
|
|
210
212
|
}
|
|
211
213
|
}
|
|
212
214
|
|
|
213
|
-
// §6.14 B6 ∕ M3:收尾 stale 循环(同款让出 +
|
|
214
|
-
removed = await sweepStaleRows(memory, { table: "code_chunks", origin, indexed, seen, decl, base: dir, yieldFn, nowFn, yieldMs })
|
|
215
|
+
// §6.14 B6 ∕ M3:收尾 stale 循环(同款让出 + 收尾保护位:面①∪面⑥ 排除 ∪ 面⑦ 范围)——机制单源 = `sync-tail.mjs`
|
|
216
|
+
removed = await sweepStaleRows(memory, { table: "code_chunks", origin, indexed, seen, decl, base: dir, scope: declaredIndexScope(dir), yieldFn, nowFn, yieldMs })
|
|
215
217
|
|
|
216
218
|
onProgress?.({ phase: "done", total: files.length, updated, removed, skipped, failed, overSizeSkipped, budgetSkipped: budgetSkipped.length })
|
|
217
219
|
// §6.14 B7 维护口:模型键失配的失效**执行**(读面只判定降级——执行住同步尾;覆盖 /reindex ∕ 桌面 ∕ VSC 构建)
|
|
@@ -248,6 +250,7 @@ export async function reindexFile(memory, cwd, absPath) {
|
|
|
248
250
|
if (rel === ".." || rel.startsWith("../")) return
|
|
249
251
|
if (isSkippedRelPath(rel)) return
|
|
250
252
|
if (isExcludedRelPath(rel, loadProjectDeclaration(cwd), cwd)) return // §6.14 面① 第三起效点(单文件缝零写)
|
|
253
|
+
if (!isInDeclaredScope(rel, declaredIndexScope(cwd), cwd)) return // §6.14 面⑦ 同址第三起效点(范围外单文件零写)
|
|
251
254
|
// Declared extensions count for the single-file path too (union with built-ins).
|
|
252
255
|
const { code: codeExts, doc: docExts } = indexExtensions(cwd)
|
|
253
256
|
|
package/memory/core.mjs
CHANGED
|
@@ -132,7 +132,7 @@ export function fetchEntry(memory, uid) {
|
|
|
132
132
|
const lastColon = uid.lastIndexOf(":")
|
|
133
133
|
const origin = lastColon > layer.length ? uid.slice(layer.length + 1, lastColon) : ""
|
|
134
134
|
const path = lastColon > layer.length ? uid.slice(lastColon + 1) : uid.slice(layer.length + 1)
|
|
135
|
-
const projectOrigin = normalizeOrigin(memory.projectOrigin) // §6.11 读缝归一(uid
|
|
135
|
+
const projectOrigin = normalizeOrigin(memory.projectOrigin) // §6.11 读缝归一(uid 解析面零改——`fetchEntry` 余量兜底仍在)
|
|
136
136
|
if (layer === "project" && projectOrigin) {
|
|
137
137
|
const r = memory.db.prepare(`SELECT type, title, content, tags, author FROM files WHERE layer = ? AND origin = ? AND path = ?`).get(layer, origin || projectOrigin, path)
|
|
138
138
|
if (r) return { ...r, layer, id: uid }
|
package/memory/docs.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import { _upsertDocFile, yieldTick } from "./code-index.mjs"
|
|
|
12
12
|
import { markIndexedCommit, listProjectFiles, indexExtensions, declaredRootEntries } from "./code-sync.mjs"
|
|
13
13
|
import { searchOrigins } from "./code-search.mjs"
|
|
14
14
|
import { createRowBudget, rowCountOfPath, sweepStaleRows } from "./sync-tail.mjs"
|
|
15
|
-
import { loadProjectDeclaration } from "../conventions.mjs" //
|
|
15
|
+
import { loadProjectDeclaration, isExcludedRelPath, declaredIndexScope, isInDeclaredScope } from "../conventions.mjs" // 面① 谓词经 `decl` 传 `sweepStaleRows`;面⑥∪面⑦ 读面准入本档直调
|
|
16
16
|
import { logEvent } from "../log.mjs"
|
|
17
17
|
import { safeSliceUTF16 } from "../text-budget.mjs"
|
|
18
18
|
import { DESC } from "../tools/shared.mjs" // #15 描述外置:文本单点 = tool-docs/*.md(DESC 单解析面,缺档抛错语义不变)
|
|
@@ -88,8 +88,8 @@ export async function syncDocRoot(memory, dir, { onProgress, yieldFn = yieldTick
|
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
// §6.14 B6 ∕ M3:收尾 stale 循环(同款让出 +
|
|
92
|
-
removed = await sweepStaleRows(memory, { table: "doc_chunks", origin, indexed, seen, decl, base: dir, yieldFn, nowFn, yieldMs })
|
|
91
|
+
// §6.14 B6 ∕ M3:收尾 stale 循环(同款让出 + 收尾保护位:面①∪面⑥ 排除 ∪ 面⑦ 范围)——机制单源 = `sync-tail.mjs`
|
|
92
|
+
removed = await sweepStaleRows(memory, { table: "doc_chunks", origin, indexed, seen, decl, base: dir, scope: declaredIndexScope(dir), yieldFn, nowFn, yieldMs })
|
|
93
93
|
|
|
94
94
|
onProgress?.({ phase: "done", total: files.length, updated, removed, skipped, failed, overSizeSkipped, budgetSkipped: budgetSkipped.length })
|
|
95
95
|
// §6.14 B7 维护口:模型键失配的失效**执行**(读面只判定降级——执行住同步尾)
|
|
@@ -117,13 +117,28 @@ export async function docSearch(memory, query, { limit = 5, origins: originsOver
|
|
|
117
117
|
const origins = originsOverride ?? searchOrigins(memory)
|
|
118
118
|
const ftsOriginFilter = origins.length === 1 ? `AND d.origin = ?` : origins.length > 1 ? `AND d.origin IN (${origins.map(() => "?").join(", ")})` : ""
|
|
119
119
|
const originParams = origins
|
|
120
|
+
// §6.14 读面准入过滤(FTS 列表建成后 ∥ 向量 top-K push 前;两判据并列互不吞):排除谓词(面①∪面⑥,
|
|
121
|
+
// 行 origin 为 `base`)∪ 声明范围门(面⑦——`isInDeclaredScope(path, declaredIndexScope(origin), origin)`,
|
|
122
|
+
// 逐 origin 预载一次)⇒「批次档 ∥ 范围外路径从未入索引」等价检索;存量行不删(收尾保护位);窗口欠额接受。
|
|
123
|
+
const declCache = new Map()
|
|
124
|
+
const scopeCache = new Map()
|
|
125
|
+
const rowHidden = (origin, path) => {
|
|
126
|
+
const d = declCache.get(origin) ?? loadProjectDeclaration(origin)
|
|
127
|
+
declCache.set(origin, d)
|
|
128
|
+
if (isExcludedRelPath(path, d, origin)) return true
|
|
129
|
+
let scope = scopeCache.get(origin)
|
|
130
|
+
if (scope === undefined) { scope = declaredIndexScope(origin); scopeCache.set(origin, scope) }
|
|
131
|
+
return !isInDeclaredScope(path, scope, origin)
|
|
132
|
+
}
|
|
133
|
+
const ftsNeedOrigin = origins.length !== 1 // 单 origin 形 SQL 零改;多 origin ∥ 无过滤形补 `origin` 列(行 origin 随行可得)
|
|
120
134
|
|
|
121
135
|
const ftsList = ftsQuery ? memory.db.prepare(`
|
|
122
|
-
SELECT d.rowid, d.path, d.language, d.heading, d.content, d.line_start, d.line_end, bm25(doc_chunks_fts) AS rank
|
|
136
|
+
SELECT d.rowid, d.path, d.language, d.heading, d.content, d.line_start, d.line_end, bm25(doc_chunks_fts) AS rank${ftsNeedOrigin ? ", d.origin" : ""}
|
|
123
137
|
FROM doc_chunks_fts JOIN doc_chunks d ON d.rowid = doc_chunks_fts.rowid
|
|
124
138
|
WHERE doc_chunks_fts MATCH ? ${ftsOriginFilter}
|
|
125
139
|
ORDER BY rank LIMIT ?
|
|
126
|
-
`).all(ftsQuery, ...originParams, Math.max(limit * 4, 20))
|
|
140
|
+
`).all(ftsQuery, ...originParams, Math.max(limit * 4, 20))
|
|
141
|
+
.filter((r) => !rowHidden(origins.length === 1 ? origins[0] : r.origin, r.path)) : []
|
|
127
142
|
|
|
128
143
|
if (!memory.embedder) return ftsList.slice(0, limit)
|
|
129
144
|
|
|
@@ -152,7 +167,9 @@ export async function docSearch(memory, query, { limit = 5, origins: originsOver
|
|
|
152
167
|
for (const origin of origins.length ? origins : [null]) {
|
|
153
168
|
await scanVectors(memory.db, vecSql, origin ? [origin] : [], {
|
|
154
169
|
cursorKey,
|
|
155
|
-
onRow: (r) =>
|
|
170
|
+
onRow: (r) => { // §6.14 面⑥∪面⑦ 准入(top-K push 前;无过滤形行 origin = 游标键 `origin` 列)
|
|
171
|
+
if (!rowHidden(origin ?? r.origin, r.path)) top.push({ id: r.rowid, rowid: r.rowid, score: cosine(qvec, fromBlob(r.embedding)) })
|
|
172
|
+
},
|
|
156
173
|
})
|
|
157
174
|
}
|
|
158
175
|
const vecList = top.list().map((c) => ({ rowid: c.id, score: c.score }))
|
package/memory/file-list.mjs
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* memory/file-list.mjs — 项目文件清单面(自 `memory/code-sync.mjs` 迁出 · 2026-10-01 core 拆分批 #755 ∥ #786)。
|
|
3
3
|
* 内容 = `indexExtensions`(内置扩展名表 ∪ 项目声明并集)∥ `listProjectFiles`(git 列面 + 非 git walk
|
|
4
|
-
* 回退 + unlisted
|
|
4
|
+
* 回退 + unlisted 计数;准入 = 排除(§6.14 面①∪面⑥——git 列面筛行 ∥ walk 剪枝)+ 声明范围门
|
|
5
|
+
* (§6.14 面⑦——双径逐文件判 ∥ walk 不剪枝))——迁出块逐字。
|
|
5
6
|
* 原档 `memory/code-sync.mjs` 经 `export { … } from` 转口保名(消费面 / 单元测试档 import 面零改)。
|
|
6
7
|
*/
|
|
7
8
|
import { CODE_EXTS, DOC_EXTS } from "./schema.mjs"
|
|
8
9
|
import { walkProjectFiles, isSkippedRelPath, extensionOf, createUnlistedTally, MAX_WALK_FILES } from "./file-walk.mjs"
|
|
9
|
-
import { loadProjectDeclaration, isExcludedRelPath } from "../conventions.mjs"
|
|
10
|
+
import { loadProjectDeclaration, isExcludedRelPath, declaredIndexScope, isInDeclaredScope } from "../conventions.mjs"
|
|
10
11
|
import { logEvent } from "../log.mjs"
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -33,10 +34,13 @@ export function indexExtensions(dir) {
|
|
|
33
34
|
* Returns { entries, unlisted }: `entries` = { abs, rel } pairs (rel relative to
|
|
34
35
|
* dir); `unlisted` = files whose extension is in NO index list (count + sample),
|
|
35
36
|
* the visible signal behind "my .xyz files are not searchable" (PORTABILITY PO-9).
|
|
36
|
-
*
|
|
37
|
-
*
|
|
37
|
+
* Both admission facets filter BOTH paths: the exclusion predicate (§6.14 面① declared
|
|
38
|
+
* `index.excludePaths` ∪ 面⑥ batch-record segments) — git listing filters the line set,
|
|
39
|
+
* the walk prunes excluded subtrees during traversal (no expansion, no budget); and the
|
|
40
|
+
* declared-scope gate (§6.14 面⑦ — un-declared ⇒ not indexed) — per-file on both paths,
|
|
41
|
+
* no pruning on the walk (a segment-sequence face may match at any depth).
|
|
38
42
|
* `truncated` = the non-git walk hit its file cap (capped trees are logged, never
|
|
39
|
-
* silently indexed-partial). opts.maxFiles = the walk cap (test seam).
|
|
43
|
+
* silently indexed-partial; out-of-scope files spend no budget). opts.maxFiles = the walk cap (test seam).
|
|
40
44
|
*/
|
|
41
45
|
export async function listProjectFiles(dir, exts, { maxFiles } = {}) {
|
|
42
46
|
const { execFile: _execFile } = await import("node:child_process")
|
|
@@ -44,6 +48,8 @@ export async function listProjectFiles(dir, exts, { maxFiles } = {}) {
|
|
|
44
48
|
const { code, doc } = indexExtensions(dir)
|
|
45
49
|
const decl = loadProjectDeclaration(dir)
|
|
46
50
|
const excluded = (rel) => isExcludedRelPath(rel, decl, dir)
|
|
51
|
+
const scope = declaredIndexScope(dir) // §6.14 面⑦ 范围门(单源谓词;本根一份——条目集空 ⇒ 零索引)
|
|
52
|
+
const inScope = (rel) => isInDeclaredScope(rel, scope, dir)
|
|
47
53
|
const tally = createUnlistedTally(new Set([...code, ...doc]))
|
|
48
54
|
|
|
49
55
|
const files = []
|
|
@@ -58,7 +64,7 @@ export async function listProjectFiles(dir, exts, { maxFiles } = {}) {
|
|
|
58
64
|
})).replace(/\\/g, "/")
|
|
59
65
|
} catch {
|
|
60
66
|
// Not a git repo → filesystem walk (FR15: the index must still be usable).
|
|
61
|
-
const walked = await walkProjectFiles(dir, exts, { knownExts: new Set([...code, ...doc]), isExcluded: excluded, ...(maxFiles !== undefined ? { maxFiles } : {}) })
|
|
67
|
+
const walked = await walkProjectFiles(dir, exts, { knownExts: new Set([...code, ...doc]), isExcluded: excluded, isInScope: inScope, ...(maxFiles !== undefined ? { maxFiles } : {}) })
|
|
62
68
|
files.push(...walked.files)
|
|
63
69
|
// The walk's cap must stay visible end-to-end (file-walk.mjs: "never silently
|
|
64
70
|
// dropped") — a capped listing says so in the log, not only in a discarded flag.
|
|
@@ -78,7 +84,7 @@ export async function listProjectFiles(dir, exts, { maxFiles } = {}) {
|
|
|
78
84
|
const p = line.trim()
|
|
79
85
|
if (!p) continue
|
|
80
86
|
const rel = p.replace(/\\/g, "/")
|
|
81
|
-
if (isSkippedRelPath(rel) || excluded(rel)) continue
|
|
87
|
+
if (isSkippedRelPath(rel) || excluded(rel) || !inScope(rel)) continue
|
|
82
88
|
const ext = extensionOf(rel)
|
|
83
89
|
if (!ext || !exts.has(ext)) { tally.note(rel); continue }
|
|
84
90
|
files.push({ abs: joinPath(dir, rel), rel })
|
package/memory/file-walk.mjs
CHANGED
|
@@ -80,14 +80,18 @@ export function createUnlistedTally(knownExts) {
|
|
|
80
80
|
* silently dropped.
|
|
81
81
|
* @param {string} dir — project root
|
|
82
82
|
* @param {Set<string>} exts — extensions to keep (lower-case, leading dot)
|
|
83
|
-
* @param {{maxFiles?: number, knownExts?: Set<string>|null, isExcluded?: ((rel: string) => boolean)|null
|
|
83
|
+
* @param {{maxFiles?: number, knownExts?: Set<string>|null, isExcluded?: ((rel: string) => boolean)|null,
|
|
84
|
+
* isInScope?: ((rel: string) => boolean)|null}} [opts]
|
|
84
85
|
* knownExts → also tally files whose extension is in NO index list
|
|
85
|
-
* isExcluded → caller-injected exclusion predicate (§6.14 面① 剪枝形): a hit prunes the
|
|
86
|
+
* isExcluded → caller-injected exclusion predicate (§6.14 面① ∪ 面⑥ 剪枝形): a hit prunes the
|
|
86
87
|
* subtree (never expanded — zero readdir, no MAX_WALK_FILES budget spent) or drops the file
|
|
88
|
+
* isInScope → caller-injected declared-scope predicate (§6.14 面⑦): a hit drops the FILE only —
|
|
89
|
+
* never prunes (the code face matches a segment sequence at any depth, so an upper directory
|
|
90
|
+
* carries no pruning judgement); an out-of-scope file spends no MAX_WALK_FILES budget
|
|
87
91
|
* @returns {Promise<{files: {abs: string, rel: string}[], truncated: boolean,
|
|
88
92
|
* unlisted: {count: number, exts: {ext: string, count: number}[]}}>}
|
|
89
93
|
*/
|
|
90
|
-
export async function walkProjectFiles(dir, exts, { maxFiles = MAX_WALK_FILES, knownExts = null, isExcluded = null } = {}) {
|
|
94
|
+
export async function walkProjectFiles(dir, exts, { maxFiles = MAX_WALK_FILES, knownExts = null, isExcluded = null, isInScope = null } = {}) {
|
|
91
95
|
const files = []
|
|
92
96
|
const tally = knownExts ? createUnlistedTally(knownExts) : null
|
|
93
97
|
let truncated = false
|
|
@@ -106,7 +110,8 @@ export async function walkProjectFiles(dir, exts, { maxFiles = MAX_WALK_FILES, k
|
|
|
106
110
|
if (!isSkippedRelPath(rel) && !isExcluded?.(rel)) stack.push({ abs: join(cur.abs, ent.name), rel })
|
|
107
111
|
continue
|
|
108
112
|
}
|
|
109
|
-
|
|
113
|
+
// 面⑦ 范围门:只丢文件、不剪枝(段序列可命中任意深度 ⇒ 上层目录无剪枝判据)
|
|
114
|
+
if (!ent.isFile() || isSkippedRelPath(rel) || isExcluded?.(rel) || (isInScope !== null && !isInScope(rel))) continue
|
|
110
115
|
const ext = extensionOf(ent.name)
|
|
111
116
|
if (!ext || !exts.has(ext)) {
|
|
112
117
|
tally?.note(rel)
|
package/memory/scan.mjs
CHANGED
|
@@ -90,7 +90,7 @@ export function createTopK(k) {
|
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
/** 末行键值元组:任一键列缺失(`undefined` / `null`)⇒ `null`(防呆①:等价现
|
|
93
|
+
/** 末行键值元组:任一键列缺失(`undefined` / `null`)⇒ `null`(防呆①:等价现 `createTopK` 的 `list()`)。 */
|
|
94
94
|
function cursorValues(keys, row) {
|
|
95
95
|
if (!row) return null
|
|
96
96
|
const out = []
|
package/memory/sweep.mjs
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* 折叠映射 + 备份路径 + 写后回读判据)。
|
|
16
16
|
* **折叠规则单源 = §6.11 步 2**(同归一键内 `(path, line_start)`(`files` = `(layer, path)`)恰一行——
|
|
17
17
|
* 保留 `mtime_ms` 最大者,并列取 `rowid` 最小者;FTS 由既有触发器随行同步)。
|
|
18
|
-
* 探针 fail-safe:**仅 ENOENT 判亡**,其余错误(权限 / 非法路径)⇒ 保留(先例 `session-stale.mjs
|
|
18
|
+
* 探针 fail-safe:**仅 ENOENT 判亡**,其余错误(权限 / 非法路径)⇒ 保留(先例 = `session-stale.mjs` 的 `pathExists`)。
|
|
19
19
|
* 执行分工:真实库写 = 父侧 ops;本档只在传入的库句柄上作业,写闸 = `confirm` 单一入参。
|
|
20
20
|
*/
|
|
21
21
|
import { existsSync, statSync } from "node:fs"
|
package/memory/sync-tail.mjs
CHANGED
|
@@ -8,14 +8,15 @@
|
|
|
8
8
|
* 一行;真实行数——F-S14 ①)+ `logEvent`;过 `INDEX_ORIGIN_ROW_CAP` 由调用方跳过列序后缀
|
|
9
9
|
* (**只停新增**——存量行不失效;跳过 = 不落行 ∕ 不记 mtime ⇒ 下趟重试)。
|
|
10
10
|
* · **stale 收尾循环**(`sweepStaleRows`):同款时间片让出(§6.10 A2 口径——每 `SCAN_YIELD_CHECK_ROWS`
|
|
11
|
-
* 行读钟,累计 ≥ `yieldMs` 即让出)+
|
|
12
|
-
*
|
|
11
|
+
* 行读钟,累计 ≥ `yieldMs` 即让出)+ **收尾保护位**(两族并列:面①∪面⑥ `isExcludedRelPath` ∪
|
|
12
|
+
* 面⑦ `isInDeclaredScope` 命中路径短路——被排除 ∥ 范围外而文件仍在盘的路径,存量行不因声明
|
|
13
|
+
* 删除;收尾只删「不在盘 ∥ 不再列」的行,收敛仅经 `sweep --path`)。
|
|
13
14
|
*
|
|
14
15
|
* 零环:只引 `log.mjs` ∕ `conventions.mjs`(谓词)∕ `schema.mjs`(常量)∕ `scan.mjs`(预算常量)∕
|
|
15
16
|
* `code-index.mjs`(`yieldTick`)——不引 `code-sync.mjs` ∕ `docs.mjs`。作业皆经传入句柄,档内零状态。
|
|
16
17
|
*/
|
|
17
18
|
import { logEvent } from "../log.mjs"
|
|
18
|
-
import { isExcludedRelPath } from "../conventions.mjs"
|
|
19
|
+
import { isExcludedRelPath, isInDeclaredScope } from "../conventions.mjs"
|
|
19
20
|
import { INDEX_ORIGIN_ROW_WARN, INDEX_ORIGIN_ROW_CAP } from "./schema.mjs"
|
|
20
21
|
import { SCAN_YIELD_CHECK_ROWS, SCAN_YIELD_MS } from "./scan.mjs"
|
|
21
22
|
import { yieldTick } from "./code-index.mjs"
|
|
@@ -65,16 +66,19 @@ export function createRowBudget(memory, origin, kind, dir, { baseOffset } = {})
|
|
|
65
66
|
* §6.14 B6 ∕ M3:收尾 stale 删除循环(同款让出 + 收尾保护位)。返回删除行数(文件级)。
|
|
66
67
|
* @param {object} memory 句柄
|
|
67
68
|
* @param {{table: string, origin: string, indexed: Map<string, number>, seen: Set<string>, decl: object,
|
|
68
|
-
* base?: string|null, yieldFn?: Function, nowFn?: Function, yieldMs?: number}} opts
|
|
69
|
+
* base?: string|null, scope?: readonly object[]|null, yieldFn?: Function, nowFn?: Function, yieldMs?: number}} opts
|
|
69
70
|
* `indexed` = 库内 path→mtime(开趟读);`seen` = 本趟列文件;`decl` = `loadProjectDeclaration()` 回执;
|
|
70
|
-
* `base` = `indexed`/`seen` 里 `rel` 的相对基(调用面 dir——#700 等价换算;缺省 =
|
|
71
|
+
* `base` = `indexed`/`seen` 里 `rel` 的相对基(调用面 dir——#700 等价换算;缺省 = 根相对);
|
|
72
|
+
* `scope` = `declaredIndexScope(base)` 回执(面⑦ 保护位——缺省 `null` ⇒ **范围面不判**(仅面①∪面⑥ 保护,
|
|
73
|
+
* 既有调用方语义零改);在场(含空数组 = 零条目)⇒ 范围外短路(存量行不删))
|
|
71
74
|
*/
|
|
72
|
-
export async function sweepStaleRows(memory, { table, origin, indexed, seen, decl, base = null, yieldFn = yieldTick, nowFn = Date.now, yieldMs = SCAN_YIELD_MS }) {
|
|
75
|
+
export async function sweepStaleRows(memory, { table, origin, indexed, seen, decl, base = null, scope = null, yieldFn = yieldTick, nowFn = Date.now, yieldMs = SCAN_YIELD_MS }) {
|
|
73
76
|
let removed = 0
|
|
74
77
|
let checked = 0
|
|
75
78
|
let lastYieldAt = nowFn()
|
|
76
79
|
for (const stale of indexed.keys()) {
|
|
77
|
-
|
|
80
|
+
// 面⑦ 保护位:`scope` 在场 ⇒「声明面内才删」(范围外短路);缺省 ⇒ 范围面不判(既有调用方语义零改)
|
|
81
|
+
if (!seen.has(stale) && !isExcludedRelPath(stale, decl, base) && (scope === null || isInDeclaredScope(stale, scope, base))) {
|
|
78
82
|
memory.db.prepare(`DELETE FROM ${table} WHERE origin = ? AND path = ?`).run(origin, stale)
|
|
79
83
|
removed++
|
|
80
84
|
}
|
package/model-specs.mjs
CHANGED
|
@@ -33,8 +33,8 @@ const MODEL_SPECS = [
|
|
|
33
33
|
["deepseek-flash", { context: 1_000_000, maxOutput: 384_000, thinking: true, prefixMode: true, thinkApi: "type", reasoningEcho: "required", reasoningEffortEnum: ["low", "high", "max"], tempRange: [0, 2], multimodal: true }],
|
|
34
34
|
// deepseek-v4.1-flash = qwen-plan channel name for DeepSeek V4.1-Flash (token-plan GET /models verified 2026-09-15) — same fields as the row above; ".1" ≠ "-" so plain prefix lookup misses the existing rows (D-PR25)
|
|
35
35
|
["deepseek-v4.1-flash", { context: 1_000_000, maxOutput: 384_000, thinking: true, prefixMode: true, thinkApi: "type", reasoningEcho: "required", reasoningEffortEnum: ["low", "high", "max"], tempRange: [0, 2], multimodal: true }],
|
|
36
|
-
// deepseek-v4-pro (V4-Pro-0813): fields unchanged — 视觉面 **未探**(不声明 multimodal
|
|
37
|
-
//
|
|
36
|
+
// deepseek-v4-pro (V4-Pro-0813): fields unchanged — 视觉面 **未探**(不声明 multimodal——保守).
|
|
37
|
+
// From 2026-09-14 12:00 Beijing all requests route to V4.1-Flash.
|
|
38
38
|
["deepseek-v4-pro", { context: 1_000_000, maxOutput: 384_000, thinking: true, prefixMode: true, thinkApi: "type", reasoningEcho: "required", reasoningEffortEnum: ["low", "high", "max"], tempRange: [0, 2] }],
|
|
39
39
|
// deepseek-v4-flash: RETIRED name — still accepted, served by V4.1-Flash today → aligned row
|
|
40
40
|
["deepseek-v4-flash", { context: 1_000_000, maxOutput: 384_000, thinking: true, prefixMode: true, thinkApi: "type", reasoningEcho: "required", reasoningEffortEnum: ["low", "high", "max"], tempRange: [0, 2], multimodal: true }],
|
|
@@ -63,7 +63,7 @@ const MODEL_SPECS = [
|
|
|
63
63
|
// GLM series
|
|
64
64
|
// GLM-5.3: thinking always-on (no "disabled"); effort converges to low/high/max — NOT the
|
|
65
65
|
// 7-level glm-5.2 enum (verified vs docs.bigmodel.cn GLM-5.3 page, 2026-08).
|
|
66
|
-
// 视觉面 **未探**(不声明 multimodal
|
|
66
|
+
// 视觉面 **未探**(不声明 multimodal——保守)。
|
|
67
67
|
// 该事实 = 机制位 `thinkAlwaysOn`(单源 = `docs/core/design/MODEL-SPECS.md` §16.3)——**官方口径**
|
|
68
68
|
// (智谱 GLM-5.3 文档:thinking always-on、无 `disabled`)。
|
|
69
69
|
["glm-5.3", { context: 1_000_000, maxOutput: 128_000, thinking: true, thinkAlwaysOn: true, thinkApi: "type", reasoningEcho: "optional", reasoningEffortEnum: ["low", "high", "max"], tempRange: [0, 1], noUsageStream: true }],
|
|
@@ -199,9 +199,9 @@ const MODEL_SPECS = [
|
|
|
199
199
|
// (多源网络转述级,本机无该渠道——行值 + 行注即标级);thinking「always-on adaptive」= 弱源读数 ⇒ 零口径不落字段(§14.3)。
|
|
200
200
|
["claude-fable-5.1", { context: 1_000_000, maxOutput: 128_000 }],
|
|
201
201
|
// Gemini series (Google)
|
|
202
|
-
["gemini-3-pro", { context: 1_000_000, maxOutput: 64_000, thinking:
|
|
203
|
-
["gemini-2.5-pro", { context: 2_000_000, maxOutput: 64_000, thinking:
|
|
204
|
-
["gemini-2.5-flash", { context: 1_000_000, maxOutput: 64_000, thinking:
|
|
202
|
+
["gemini-3-pro", { context: 1_000_000, maxOutput: 64_000, thinking: true, multimodal: true, format: "google", noUsageStream: true }],
|
|
203
|
+
["gemini-2.5-pro", { context: 2_000_000, maxOutput: 64_000, thinking: true, multimodal: true, format: "google", noUsageStream: true }],
|
|
204
|
+
["gemini-2.5-flash", { context: 1_000_000, maxOutput: 64_000, thinking: true, multimodal: true, format: "google", noUsageStream: true }],
|
|
205
205
|
// gemini-3.1-pro(2026-09-25 批 §14.2 #2-6 · §1.7.1 父侧交付):**尺寸行**——context 1_048_576 / maxOutput 65_536(多源
|
|
206
206
|
// 网络转述级,本机无该渠道——行值 + 行注即标级);thinking「有·计入输出预算」= 弱源读数 ⇒ 零口径不落字段(§14.3)。
|
|
207
207
|
["gemini-3.1-pro", { context: 1_048_576, maxOutput: 65_536 }],
|
|
@@ -350,7 +350,7 @@ export function providerSpec(provider) {
|
|
|
350
350
|
*
|
|
351
351
|
* Callers (as-of 2026-10-10): thincoder-core/agent/turn-loop.mjs (main loop) · thincoder-core/advisor/loop.mjs
|
|
352
352
|
* (review mirror) · bench/lib/client.mjs (bench client) — each passes its own spec face; VSC 转口
|
|
353
|
-
* (thincoder-vscode/src/specs.mjs
|
|
353
|
+
* (`thincoder-vscode/src/specs.mjs` 的 `providerSpec` ∕ `assistantToolCallMessage` 再导出) 零调用点(父侧收口机械笔 · 可 revert)。
|
|
354
354
|
*/
|
|
355
355
|
export function assistantToolCallMessage(response, spec) {
|
|
356
356
|
const msg = {
|