@thincoder/core 0.10.5 → 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/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" // 排除谓词本档不直调(经 `decl` 传 `sweepStaleRows`)
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 循环(同款让出 + 面① 收尾保护位)——机制单源 = `sync-tail.mjs`
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) => top.push({ id: r.rowid, rowid: r.rowid, score: cosine(qvec, fromBlob(r.embedding)) }),
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 }))
@@ -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 计数;`index.excludePaths` §6.14 面① 双径过滤:git 列面筛行 ∥ walk 剪枝)——迁出块逐字。
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
- * Declared `index.excludePaths` (§6.14 面①) filters BOTH paths — git listing filters the
37
- * line set; the walk prunes excluded subtrees during traversal (no expansion, no budget).
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 })
@@ -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}} [opts]
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
- if (!ent.isFile() || isSkippedRelPath(rel) || isExcluded?.(rel)) continue
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)
@@ -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` 即让出)+ 面① **收尾保护位**(`isExcludedRelPath` 命中路径短路——
12
- * 被排除但存在的路径存量行不因声明删除,收敛仅经 `sweep --path`;缺省 `[]` ⇒ 恒不命中)。
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
- if (!seen.has(stale) && !isExcludedRelPath(stale, decl, base)) {
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thincoder/core",
3
- "version": "0.10.5",
3
+ "version": "0.10.7",
4
4
  "description": "ThinCoder shared core — shared mechanism modules + shared prompt content (CLI ↔ VS Code extension).",
5
5
  "keywords": [
6
6
  "ai",
@@ -40,6 +40,7 @@
40
40
  "advisor/",
41
41
  "agent/",
42
42
  "agent-tools/",
43
+ "browser/",
43
44
  "git/",
44
45
  "mcp/",
45
46
  "memory/",