@thincoder/core 0.10.6 → 0.10.7
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 +9 -0
- package/agent-tools/manifest.mjs +7 -0
- package/conventions.mjs +1 -1
- package/declaration.mjs +153 -13
- package/memory/code-search.mjs +21 -4
- package/memory/code-sync.mjs +7 -4
- package/memory/docs.mjs +23 -6
- package/memory/file-list.mjs +13 -7
- package/memory/file-walk.mjs +9 -4
- package/memory/sync-tail.mjs +11 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,15 @@
|
|
|
3
3
|
All notable changes to the core package are documented here.
|
|
4
4
|
Format: Keep a Changelog · 中文 · 号在发布时定(CalVer——见 `docs/RELEASE.md` §4)。
|
|
5
5
|
|
|
6
|
+
## [0.10.7] — 2026-10-14
|
|
7
|
+
|
|
8
|
+
> 0.10.6 → 0.10.7(月内 +1——发布时定号)
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **批次档不入文档索引(#1366)**:`docs/batches/**` 无条件默认排除(用户 19:03「不管 manifest 里配置没配置排除路径,都把批次档目录排除掉」);读面准入过滤同拍。
|
|
13
|
+
- **索引范围声明化(#1367)**:只索引 manifest 声明目录 ∪ 一级子目录(用户 19:25「只索引 manifest 指定的目录……还要考虑一级子目录下的 manifest」);结构门替换开放式排除;manifest 写 ⇒ 范围缓存失效。
|
|
14
|
+
|
|
6
15
|
## [0.10.6] — 2026-10-13
|
|
7
16
|
|
|
8
17
|
> 0.10.5 → 0.10.6(月内 +1——发布时定号;P0 热修)
|
package/agent-tools/manifest.mjs
CHANGED
|
@@ -27,11 +27,16 @@
|
|
|
27
27
|
*
|
|
28
28
|
* 报告面随动(KD-M1-40):本工具**不写** `agent.manifest`(第二值源否——工具目标可为非锚项目)——
|
|
29
29
|
* 写盘 = 盘面变化,情境行经既有 ③b mtime 门控于下回合采纳(`agent/setup-reminders.mjs`)。
|
|
30
|
+
*
|
|
31
|
+
* 缓存失效(#1367 面⑦):写档成功(init ∥ write——全部成功路径)⇒ `clearDeclarationCache()`——
|
|
32
|
+
* 「补档即回归」进程内即时(声明 ∥ 范围缓存同清);失败路径不清(盘零变 ⇒ 缓存照旧);
|
|
33
|
+
* 手工改档 ⇒ 重启生效(零文件侦听)。
|
|
30
34
|
*/
|
|
31
35
|
import { existsSync } from "node:fs"
|
|
32
36
|
import { join, resolve } from "node:path"
|
|
33
37
|
import { MANIFEST_REL, MANIFEST_SCHEMA, initManifest, projectRootView, projectView, readManifest, writeManifest } from "../manifest.mjs"
|
|
34
38
|
import { DESC } from "../tools/shared.mjs"
|
|
39
|
+
import { clearDeclarationCache } from "../conventions.mjs" // #1367 面⑦:写档成功 ⇒ 缓存失效(fix 轮接线)
|
|
35
40
|
|
|
36
41
|
/** 拒面统一前缀(设计 §2.10:`manifest <action>: refused — …`)——盘零变类拒绝一律经此抛错。 */
|
|
37
42
|
function refuse(action, reason) {
|
|
@@ -150,6 +155,7 @@ export const manifestTool = {
|
|
|
150
155
|
} catch (e) {
|
|
151
156
|
throw refuse("init", `could not write the default manifest at ${file} — ${e?.message ?? String(e)}`)
|
|
152
157
|
}
|
|
158
|
+
clearDeclarationCache() // 写成功:补档 ⇒ 范围即回归(进程内——失败路径不清)
|
|
153
159
|
return `manifest init: created default manifest (DEFAULT_MANIFEST)\npath=${file}`
|
|
154
160
|
}
|
|
155
161
|
|
|
@@ -188,6 +194,7 @@ export const manifestTool = {
|
|
|
188
194
|
} catch (e) {
|
|
189
195
|
throw refuse("write", `the edited manifest fails the pre-write validation (disk unchanged) — ${e?.message ?? String(e)}`)
|
|
190
196
|
}
|
|
197
|
+
clearDeclarationCache() // 写成功:声明 ∥ 范围逐字随动(进程内——失败路径不清)
|
|
191
198
|
return `manifest write: set ${key} = ${JSON.stringify(value)}\npath=${file}`
|
|
192
199
|
},
|
|
193
200
|
}
|
package/conventions.mjs
CHANGED
|
@@ -189,4 +189,4 @@ export function isStatePath(p, conv) {
|
|
|
189
189
|
// 迁出面(2026-10-01 拆分批 · #755 ∥ #786):声明装载面外提 `declaration.mjs`——本档经
|
|
190
190
|
// 转口保名(消费面 / 单元测试档 import 面零改;`DEFAULT_DECLARATION` 另供本档分类裁判缺省取用)。
|
|
191
191
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
192
|
-
export { loadProjectDeclaration, declarationForTarget, DEFAULT_DECLARATION, isExcludedRelPath, declaredPublicRoots, clearDeclarationCache } from "./declaration.mjs"
|
|
192
|
+
export { loadProjectDeclaration, declarationForTarget, DEFAULT_DECLARATION, isExcludedRelPath, declaredIndexScope, isInDeclaredScope, declaredPublicRoots, clearDeclarationCache } from "./declaration.mjs"
|
package/declaration.mjs
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
* name / shape / defaults = schema 面 `docs/core/design/MANIFEST.md` §2.2)。迁出块逐字;
|
|
6
6
|
* 原档 `conventions.mjs` 经 `export { … } from` 转口保名(消费面 / 单元测试档 import 面零改)。
|
|
7
7
|
*/
|
|
8
|
-
import { existsSync, statSync } from "node:fs"
|
|
8
|
+
import { existsSync, readdirSync, statSync } from "node:fs"
|
|
9
9
|
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path"
|
|
10
|
-
import { DEFAULT_MANIFEST, manifestFilePath, owningProject, readManifest } from "./manifest.mjs"
|
|
10
|
+
import { DEFAULT_MANIFEST, MANIFEST_REL, docRootPaths, manifestFilePath, owningProject, readManifest } from "./manifest.mjs"
|
|
11
|
+
import { isSkippedRelPath } from "./memory/file-walk.mjs" // 面⑦ 子目录扫描的跳过段谓词单源(零环:file-walk 不引本档)
|
|
11
12
|
import { logEvent } from "./log.mjs"
|
|
12
13
|
|
|
13
14
|
/** Retired declaration carrier (project-root relative). Its EXISTENCE is checked, its
|
|
@@ -78,6 +79,7 @@ function buildDeclaration(m, root = null) {
|
|
|
78
79
|
const docExtensions = normalizeExtensions(m?.index?.docExtensions)
|
|
79
80
|
const excludePaths = normalizePathList(m?.index?.excludePaths)
|
|
80
81
|
const publicRepos = normalizePathList(m?.index?.publicRepos)
|
|
82
|
+
const batchRecords = batchSegments(m?.docRoot?.batches) // §6.14 面⑥ 声明值投影(`declared` 旗不涉——docRoot 族现状零改)
|
|
81
83
|
const docMap = normalizeString(m?.advisor?.docMap)
|
|
82
84
|
const standardsDoc = normalizeString(m?.advisor?.standardsDoc)
|
|
83
85
|
const defaults = normalizeCodePaths(DEFAULT_MANIFEST.codePaths) ?? []
|
|
@@ -86,6 +88,7 @@ function buildDeclaration(m, root = null) {
|
|
|
86
88
|
return Object.freeze({
|
|
87
89
|
declared,
|
|
88
90
|
codePaths: Object.freeze(codePaths),
|
|
91
|
+
batchRecords: Object.freeze(batchRecords.map((s) => Object.freeze(s))),
|
|
89
92
|
index: Object.freeze({
|
|
90
93
|
codeExtensions: Object.freeze(codeExtensions),
|
|
91
94
|
docExtensions: Object.freeze(docExtensions),
|
|
@@ -101,25 +104,158 @@ function buildDeclaration(m, root = null) {
|
|
|
101
104
|
* consumer gets; built from `DEFAULT_MANIFEST` so the default values stay single-source. */
|
|
102
105
|
export const DEFAULT_DECLARATION = buildDeclaration(null)
|
|
103
106
|
|
|
104
|
-
/**
|
|
105
|
-
*
|
|
107
|
+
/** 段序列归一(面⑥ 目标集):值(串 ∥ 串数组——元素层归一同 `normalizePathList`)⇒ 实义段序列
|
|
108
|
+
* (空 ∥ `.` ∥ `..` 段剔除——空序列另剔,防恒真;去重保序)。 */
|
|
109
|
+
function batchSegments(v) {
|
|
110
|
+
const out = []
|
|
111
|
+
for (const e of normalizePathList(Array.isArray(v) ? v : v == null ? [] : [v])) {
|
|
112
|
+
const segs = e.split("/").filter((s) => s && s !== "." && s !== "..")
|
|
113
|
+
if (segs.length && !out.some((q) => q.length === segs.length && q.every((s, i) => s === segs[i]))) out.push(segs)
|
|
114
|
+
}
|
|
115
|
+
return out
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** 约定段(面⑥——`DEFAULT_MANIFEST.docRoot.batches` 单源派生;默认值改名/迁移两处同动)。 */
|
|
119
|
+
const DEFAULT_BATCH_SEGMENTS = batchSegments(DEFAULT_MANIFEST.docRoot.batches)
|
|
120
|
+
|
|
121
|
+
/** 排除谓词(单源——§6.14 面① 前缀 ∥ 面⑥ 批次档段序列;两族同一谓词全判,非第二判定面):
|
|
122
|
+
* 面⑥(2026-10-13 · 台账 #1366 · **无条件**——不看 `index.excludePaths` 是否配置):`rel` 命中
|
|
123
|
+
* 约定段 ∪ 声明值段序列(`decl.batchRecords`——`docRoot.batches` 投影)于**路径任意深度**
|
|
124
|
+
* (段边界 = 整段一致——`docs/batchesx` ∥ `doc/batches` 判假)⇒ true;`docRoot.batches: null`
|
|
125
|
+
* ⇒ 约定段恒在(无逃生口);`decl` 缺 ⇒ 约定段照判。
|
|
126
|
+
* 面①(前缀语义):`rel`(**`base` 相对**——缺省视为根相对)命中声明前缀(恰等 ∨ 后随 `/`——
|
|
127
|
+
* `openclaw` 不吞 `openclaw-fork`)⇒ true;缺省 `[]` ⇒ 恒 false(零行为变化)。
|
|
106
128
|
* `base`(调用面 cwd/origin·#700)= `rel` 的相对基:`resolve` + `relative` 换算至 `decl.root` 根面
|
|
107
|
-
*
|
|
108
|
-
* (保守不排除——沉默洞方向收窄,与 D17 aux 面「indeterminate ⇒
|
|
109
|
-
* ⇒ `rel`
|
|
129
|
+
* 再判(cwd = 项目根时与既有行为逐字等义);换算越出根面(`..` 头 ∥ 根外绝对形)⇒ 面① **不命中**
|
|
130
|
+
* (保守不排除——沉默洞方向收窄,与 D17 aux 面「indeterminate ⇒ 不匹配」同向)∥ 面⑥ 按入参
|
|
131
|
+
* **原样 `rel`** 判(段序列面不回退——面① 不命中口径逐字零改);`decl.root` 缺 ⇒ `rel` 原样
|
|
132
|
+
* (根起步调用面等义)。 */
|
|
110
133
|
export function isExcludedRelPath(rel, decl, base = null) {
|
|
111
|
-
const list = decl?.index?.excludePaths
|
|
112
|
-
if (!Array.isArray(list) || list.length === 0) return false
|
|
113
134
|
let s = String(rel ?? "").replace(/\\/g, "/")
|
|
114
135
|
const root = decl?.root
|
|
136
|
+
let inRoot = true
|
|
115
137
|
if (base != null && root) {
|
|
116
138
|
const converted = relative(root, resolve(base, s)).replace(/\\/g, "/")
|
|
117
|
-
if (converted === ".." || converted.startsWith("../") || isAbsolute(converted))
|
|
118
|
-
s = converted
|
|
139
|
+
if (converted === ".." || converted.startsWith("../") || isAbsolute(converted)) inRoot = false
|
|
140
|
+
else s = converted
|
|
119
141
|
}
|
|
142
|
+
// 面⑥ 段序列(约定段 ∪ 声明值——任意深度连续命中);越根(`inRoot=false`)⇒ `s` 即原样 rel
|
|
143
|
+
const segs = s.split("/").filter(Boolean)
|
|
144
|
+
if ([...DEFAULT_BATCH_SEGMENTS, ...(decl?.batchRecords ?? [])].some((seq) => segs.some((_, i) => seq.every((q, j) => segs[i + j] === q)))) return true
|
|
145
|
+
// 面① 前缀(既有口径逐字零改:缺省 `[]` ∥ 越根 ⇒ 不命中)
|
|
146
|
+
const list = decl?.index?.excludePaths
|
|
147
|
+
if (!Array.isArray(list) || list.length === 0 || !inRoot) return false
|
|
120
148
|
return list.some((p) => p && (s === p || s.startsWith(`${p}/`)))
|
|
121
149
|
}
|
|
122
150
|
|
|
151
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
152
|
+
// §6.14 面⑦ 索引范围声明化(结构门——2026-10-13 · 台账 #1367):范围 = 锚的**管辖 manifest**
|
|
153
|
+
// 声明目录 ∪ **一级子目录**(带档者)声明目录——「未声明 ⇒ 不进」;条目集空 ⇒ 零索引
|
|
154
|
+
// (不回落全树)。键面 = `docRoot.*`(五子键合并)∪ `codePaths`——两键各按自身既有语义
|
|
155
|
+
// (本规 = 新增消费点,键值语义零改)。写面(收录)∥ 读面(存量行准入)共用本谓词族。
|
|
156
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
/** 文档面前缀集合(条目根相对——`docRoot.*` 五子键合并展开;解析单源 = `docRootPaths`,F7):
|
|
159
|
+
* 值 = 根相对字面前缀(恰等 ∨ 后随 `/`——路径及子树;文件值如 `docs/RELEASE.md` ⇒ 恰该文件);
|
|
160
|
+
* `"."` = 该条目根自身(整根面);越根值(`../` 形)剔除——条目子树内的文件永不可命中它。
|
|
161
|
+
* 去重保序。 */
|
|
162
|
+
function entryDocPaths(docRoot, root) {
|
|
163
|
+
const out = []
|
|
164
|
+
if (docRoot === null || typeof docRoot !== "object" || Array.isArray(docRoot)) return out
|
|
165
|
+
for (const key of Object.keys(DEFAULT_MANIFEST.docRoot)) {
|
|
166
|
+
for (const abs of docRootPaths(docRoot[key], root)) {
|
|
167
|
+
const rel = relative(root, abs).replace(/\\/g, "/")
|
|
168
|
+
if (rel === "" || rel === ".") { if (!out.includes(".")) out.push("."); continue }
|
|
169
|
+
if (rel === ".." || rel.startsWith("../") || isAbsolute(rel)) continue
|
|
170
|
+
if (!out.includes(rel)) out.push(rel)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return out
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** 段序列任意深度连续命中(面⑦ 代码面——大小写不敏感 = 既有键语义口径,同 `conventions.mjs`
|
|
177
|
+
* `hasSequenceIn`);`face` = 已小写化的根相对段表。 */
|
|
178
|
+
function hasSegmentRun(face, seq) {
|
|
179
|
+
const want = String(seq).replace(/\\/g, "/").split("/").filter(Boolean).map((s) => s.toLowerCase())
|
|
180
|
+
if (want.length === 0) return false
|
|
181
|
+
for (let i = 0; i + want.length <= face.length; i++) {
|
|
182
|
+
if (want.every((w, j) => face[i + j] === w)) return true
|
|
183
|
+
}
|
|
184
|
+
return false
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** 面⑦ 一条目(`root` = 仓根绝对路径;档在场 = 调用面已判):`docPaths` = 根相对字面前缀 ∥
|
|
188
|
+
* `codePaths` = 段名数组(既有归一单源 = `loadProjectDeclaration`——`[]` = 无段面)。档**非法**
|
|
189
|
+
* (坏 JSON ∥ 形态错)⇒ 两族按默认值参与 + 既有可见告警(声明链 KD-M1-32——`loadProjectDeclaration`
|
|
190
|
+
* 的降级告警单源,本档零第二实现)——不静默成零。 */
|
|
191
|
+
function scopeEntryOf(root) {
|
|
192
|
+
const decl = loadProjectDeclaration(root)
|
|
193
|
+
const r = readManifest(root)
|
|
194
|
+
const docRoot = r.ok ? r.manifest.docRoot : DEFAULT_MANIFEST.docRoot // 档在场才补默认(无档 = 无条目,不走本函数)
|
|
195
|
+
return Object.freeze({
|
|
196
|
+
root,
|
|
197
|
+
docPaths: Object.freeze(entryDocPaths(docRoot, root)),
|
|
198
|
+
codePaths: Object.freeze([...decl.codePaths]),
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* §6.14 面⑦ 范围求并(结构门——单源):① `dir` 的**管辖 manifest**(`manifestFilePath(dir)` 同一根
|
|
204
|
+
* 解析——最近祖先带档 ∥ 发现兜底;**可高于 `dir`** ⇒ 深 cwd 等义)在场 ⇒ 锚条目(`root` = 档所在
|
|
205
|
+
* 目录);② `dir` 的**直接子目录**(非跳过段——点段 ∥ 内建名单,防 `.thincoder/tmp` 类伪档)根下带档
|
|
206
|
+
* 者 ⇒ 各一条目(`root` = 该子目录)。**一层、纯向下、绝不向上**(发现纪律在案)。
|
|
207
|
+
* 冻结条目数组(按根去重保序);条目集空 ⇒ 范围空 ⇒ **零索引**(不回落全树)。
|
|
208
|
+
* 缓存按 `dir` 归一绝对路径(`clearDeclarationCache()` 同清——测试缝单点)。
|
|
209
|
+
* @param {string} [dir] 会话锚 ∕ 同步根(项目内子目录 ⇒ 管辖 manifest 生效)
|
|
210
|
+
* @returns {ReadonlyArray<{root: string, docPaths: readonly string[], codePaths: readonly string[]}>}
|
|
211
|
+
*/
|
|
212
|
+
export function declaredIndexScope(dir) {
|
|
213
|
+
const key = resolve(dir ?? ".")
|
|
214
|
+
const hit = _scopeCache.get(key)
|
|
215
|
+
if (hit) return hit
|
|
216
|
+
const roots = []
|
|
217
|
+
const anchorFile = manifestFilePath(key) // 数据档路径单源(KD-M1-18)
|
|
218
|
+
if (existsSync(anchorFile)) roots.push(dirname(anchorFile))
|
|
219
|
+
let names = []
|
|
220
|
+
try { names = readdirSync(key, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort() } catch { /* 不可读 ⇒ 零子条目(不抛——同发现梯内核) */ }
|
|
221
|
+
for (const name of names) {
|
|
222
|
+
if (isSkippedRelPath(name)) continue
|
|
223
|
+
const child = join(key, name)
|
|
224
|
+
if (!existsSync(join(child, MANIFEST_REL)) || roots.includes(child)) continue // 发现兜底命中的子目录 = 锚条目同根 ⇒ 去重
|
|
225
|
+
roots.push(child)
|
|
226
|
+
}
|
|
227
|
+
const scope = Object.freeze(roots.map(scopeEntryOf))
|
|
228
|
+
_scopeCache.set(key, scope)
|
|
229
|
+
return scope
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* §6.14 面⑦ 范围谓词(纯谓词——单一实现,写面 ∥ 读面共用):`rel`(`base` 相对;缺省按进程 cwd
|
|
234
|
+
* 解析)经 `resolve` 绝对化 ⇒ 逐条目判归属(`abs` ∈ `root` 子树;无归属条目 = **未声明**)⇒ 两面试
|
|
235
|
+
* 任一命中即入范围(两层锚条目间 = 并集):
|
|
236
|
+
* · **文档面** = 条目根相对面**字面前缀**(恰等 ∨ 后随 `/`;`"."` = 整根);
|
|
237
|
+
* · **代码面** = 段序列**任意深度**命中(大小写不敏感)。
|
|
238
|
+
* 不在任何条目子树内(越根)⇒ 不命中。`scope` 空 ∥ 缺 ⇒ false(零索引面——未声明 ⇒ 不进)。
|
|
239
|
+
* @param {string} rel 路径(`base` 相对)
|
|
240
|
+
* @param {ReadonlyArray<object>} scope `declaredIndexScope()` 回执
|
|
241
|
+
* @param {string|null} [base] `rel` 的相对基(目录绝对路径;缺省 = 进程 cwd)
|
|
242
|
+
* @returns {boolean}
|
|
243
|
+
*/
|
|
244
|
+
export function isInDeclaredScope(rel, scope, base = null) {
|
|
245
|
+
if (!Array.isArray(scope) || scope.length === 0) return false
|
|
246
|
+
const abs = resolve(base ?? ".", String(rel ?? "").replace(/\\/g, "/"))
|
|
247
|
+
for (const entry of scope) {
|
|
248
|
+
const relFromRoot = relative(entry.root, abs).replace(/\\/g, "/")
|
|
249
|
+
if (relFromRoot === "" || relFromRoot === ".." || relFromRoot.startsWith("../") || isAbsolute(relFromRoot)) continue
|
|
250
|
+
if (entry.docPaths.some((p) => p === "." || relFromRoot === p || relFromRoot.startsWith(`${p}/`))) return true
|
|
251
|
+
if (entry.codePaths.length > 0) {
|
|
252
|
+
const face = relFromRoot.split("/").filter(Boolean).map((s) => s.toLowerCase())
|
|
253
|
+
if (entry.codePaths.some((seq) => hasSegmentRun(face, seq))) return true
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return false
|
|
257
|
+
}
|
|
258
|
+
|
|
123
259
|
/**
|
|
124
260
|
* 声明公共仓**绝对根集**(§6.15 解析层——检索同步 ∥ 读面 origin 集 ∥ 引用解析三消费面共用的单一定义,
|
|
125
261
|
* 不得二写):值形层逐项 `resolve(decl.root, p)` ⇒ 绝对根集(去重保序);存在性判归消费面(本层不判)。
|
|
@@ -165,10 +301,14 @@ export function resolveDeclaredRef(base, part) {
|
|
|
165
301
|
const isFile = (p) => { try { return statSync(p).isFile() } catch { return false } }
|
|
166
302
|
|
|
167
303
|
const _cache = new Map()
|
|
304
|
+
/** 面⑦ 范围缓存(按 `dir` 归一绝对路径——子目录档 ∥ 声明变更后经本缝同清)。 */
|
|
305
|
+
const _scopeCache = new Map()
|
|
168
306
|
|
|
169
|
-
/** Drop the per-data-file-path cache (test seam — declarations are read once per path).
|
|
307
|
+
/** Drop the per-data-file-path cache (test seam — declarations are read once per path).
|
|
308
|
+
* #1367:面⑦ 范围缓存同清(「声明 ∥ 一级子目录档」变更后同一条测试缝)。 */
|
|
170
309
|
export function clearDeclarationCache() {
|
|
171
310
|
_cache.clear()
|
|
311
|
+
_scopeCache.clear()
|
|
172
312
|
}
|
|
173
313
|
|
|
174
314
|
/** Retired-carrier warning (existence check only — zero content parsing, zero fallback). */
|
|
@@ -188,7 +328,7 @@ function warnRetiredCarrier(root) {
|
|
|
188
328
|
* A retired `.thincoder/conventions.json` at the manifest's project root → one warning
|
|
189
329
|
* per cache miss (content zero-parsed, zero effect on the readings — KD-M1-34).
|
|
190
330
|
* @param {string} cwd — project dir / anchor (root resolved by manifest.mjs)
|
|
191
|
-
* @returns {Readonly<{declared: boolean, codePaths: readonly string[],
|
|
331
|
+
* @returns {Readonly<{declared: boolean, codePaths: readonly string[], batchRecords: readonly (readonly string[])[],
|
|
192
332
|
* index: {codeExtensions: string[], docExtensions: string[], excludePaths: string[], publicRepos: string[]},
|
|
193
333
|
* advisor: {docMap: string, standardsDoc: string}, root: string|null}>}
|
|
194
334
|
*/
|
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/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/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
|
}
|