@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/CHANGELOG.md +17 -0
- package/agent-tools/manifest.mjs +7 -0
- package/browser/actions.mjs +163 -0
- package/browser/cdp.mjs +161 -0
- package/browser/clipboard.mjs +142 -0
- package/browser/input-actions.mjs +197 -0
- package/browser/input.mjs +256 -0
- package/browser/launch.mjs +143 -0
- package/browser/queue.mjs +171 -0
- package/browser/session.mjs +494 -0
- package/browser/snapshot.mjs +270 -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 +2 -1
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
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thincoder/core",
|
|
3
|
-
"version": "0.10.
|
|
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/",
|