@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/config-watch.mjs
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* config-watch.mjs — config.json 外部写盘感知·**纯逻辑面**(R8 · 桌面功能对位批「上提」KD-T2——
|
|
3
|
-
* 自 `thincoder-vscode/src/extension/config-watch.mjs
|
|
3
|
+
* 自 `thincoder-vscode/src/extension/config-watch.mjs` 的 `startConfigWatch` 逐字随迁:去抖 300ms ∕ stat 元组
|
|
4
4
|
* (`mtimeMs:size`)比对 ∕ 自写抑制;原批 = 第 21 批 B5 · `docs/design/SETTINGS.md` §2.6)。
|
|
5
5
|
*
|
|
6
6
|
* `~/.thincoder/config.json` 为多端共享单文件:CLI(`/advisor`、`settings set`)或手工编辑写盘后,
|
|
7
7
|
* 常开面板原本零感知(只在打开面板/保存时拉快照)。机制(零语义改):
|
|
8
8
|
* 宿主 fs 事件 →(端落子)`onEvent` → 去抖 → 元组比对 → 变化才 `onChange`(事件驱动零空转)。
|
|
9
9
|
* 自写抑制:本端自身写盘同样改元组 ⇒ 裸推送会在用户编辑中被快照重建;写盘成功后核 `config-io`
|
|
10
|
-
* `saveRaw` 同步回调 `onConfigSelfWrite`(`config-io.mjs
|
|
10
|
+
* `saveRaw` 同步回调 `onConfigSelfWrite`(`config-io.mjs`)⇒ `noteSelfWrite()` 把基线刷到当前
|
|
11
11
|
* 元组 ⇒ 事件到达时元组已等于基线 ⇒ 零推送;外部写无回调 ⇒ 元组异于基线 ⇒ 推送。
|
|
12
12
|
*
|
|
13
13
|
* 平台面两注入(核内零宿主绑定):① `attach`(fs 事件源工厂——端落子:VSC = `createFileSystemWatcher`、
|
package/config.mjs
CHANGED
|
@@ -64,6 +64,13 @@ export const DEFAULTS = {
|
|
|
64
64
|
// §6.30.3 门三件③(D-TW5):timer 空闲自唤醒开关——默认开(工具描述面已承诺到点提醒);
|
|
65
65
|
// false ⇒ 端侧不武装闩(回既有回合边界投递——零调度器面)。键面随 DEFAULTS 派生进 settings 类型表。
|
|
66
66
|
timerWake: true,
|
|
67
|
+
// 注入开关(PROMPT-SYSTEM.md §6.24 · 台账 #1420):文档类 ∥ 记忆类自动注入——默认开;
|
|
68
|
+
// 判据 `!== false`(缺省 = 现行为零变)。消费点 = 核 `agent/setup.mjs` 判点(逐 run 现读)。
|
|
69
|
+
// 2026-10-15(§6.27 · 台账 #1465):① 项目指令块独立成键——`injectDocs` 收窄为只管 ② 相关文档块;
|
|
70
|
+
// 两键完全正交(关任一方另一方照常)。`injectProjectRules` 判点 = 核 `agent/setup.mjs`(`prepareRun` 项目指令段)。
|
|
71
|
+
injectDocs: true,
|
|
72
|
+
injectProjectRules: true,
|
|
73
|
+
injectMemories: true,
|
|
67
74
|
},
|
|
68
75
|
memory: {
|
|
69
76
|
dbPath: join(configDir, "memory.db"),
|
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, onManifestInit, 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,12 +301,20 @@ 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
|
|
|
314
|
+
// #1376:eng 建档路(`manifest.mjs` `resolveEngineeringManifest` init 支)成功 ⇒ 同清两缓存——
|
|
315
|
+
// 反向注册(本档已向依赖 manifest.mjs,其侧零出边保持——KD-M1-33);失败路径不广播、文件面零变。
|
|
316
|
+
onManifestInit(clearDeclarationCache)
|
|
317
|
+
|
|
174
318
|
/** Retired-carrier warning (existence check only — zero content parsing, zero fallback). */
|
|
175
319
|
function warnRetiredCarrier(root) {
|
|
176
320
|
console.warn(`[declaration] ${RETIRED_REL_PATH} is retired and no longer read — move codePaths / index.*Extensions / advisor.{docMap,standardsDoc} into PROJECT-MANIFEST.json`)
|
|
@@ -188,7 +332,7 @@ function warnRetiredCarrier(root) {
|
|
|
188
332
|
* A retired `.thincoder/conventions.json` at the manifest's project root → one warning
|
|
189
333
|
* per cache miss (content zero-parsed, zero effect on the readings — KD-M1-34).
|
|
190
334
|
* @param {string} cwd — project dir / anchor (root resolved by manifest.mjs)
|
|
191
|
-
* @returns {Readonly<{declared: boolean, codePaths: readonly string[],
|
|
335
|
+
* @returns {Readonly<{declared: boolean, codePaths: readonly string[], batchRecords: readonly (readonly string[])[],
|
|
192
336
|
* index: {codeExtensions: string[], docExtensions: string[], excludePaths: string[], publicRepos: string[]},
|
|
193
337
|
* advisor: {docMap: string, standardsDoc: string}, root: string|null}>}
|
|
194
338
|
*/
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* doc-check/anchors.mjs — 文档纪律机检 · 起步判据 ①(锚):指针抽取 + 解析序 + 节在盘判定。
|
|
3
|
+
*
|
|
4
|
+
* 判据句单源 = `docs/core/design/DOC-DISCIPLINE.md` §3.20 判据 ①(本节只实现,不重述口径):
|
|
5
|
+
* · 文档指针 = 档 token(含 `.md`,可带目录段)± 节指示(紧随 `§` + 编号 N / N.M / N.M.K);
|
|
6
|
+
* **行号坐标(`:N`)不作节指示**——行号不作引用;
|
|
7
|
+
* · 档不在盘 ⇒ `档缺`;带节指示 ∧ 节不在盘 ⇒ `节缺`。
|
|
8
|
+
*
|
|
9
|
+
* 抽取面:逐行扫;fenced 块整块跳过;可执行行(`isExecutableLine`)整行跳过(命令保字面)。
|
|
10
|
+
* 解析序:① 仓根相对 → ② 域根相对(`scanDirs` 逐根)→ ③ 引用方档所在目录相对 →
|
|
11
|
+
* ④ 域内唯一 basename 索引;同名 ≥2 ⇒ **不判**(列报告面)。
|
|
12
|
+
* 节在盘:目标档内存在标题行,其标题编号(标题文本去 `^#{1,6}\s` 后的首个 `\d+(?:\.\d+)*`)= 指针节号。
|
|
13
|
+
* 残余登记(fail-open 方向——归评审核验面):裸档名在域内多义 ⇒ 不判;未闭合围栏 ⇒ 至档尾免;
|
|
14
|
+
* 指针行的坐标尾不入判、同行的档 token 照判。
|
|
15
|
+
*/
|
|
16
|
+
import { readFileSync, statSync } from "node:fs"
|
|
17
|
+
import { basename, dirname, resolve } from "node:path"
|
|
18
|
+
import { fenceMark, isExecutableLine } from "./width.mjs"
|
|
19
|
+
|
|
20
|
+
/** 文档指针 token:`(目录段/)*名.md` + 可选行号坐标尾 + 可选节指示(`§` + 编号)。
|
|
21
|
+
* 左界守卫排除更长 token 的尾段;右界排除后续字母数字(`.md` 之后紧邻 `[\w]` ⇒ 非完整 token)。 */
|
|
22
|
+
const ANCHOR_RE = /(?<![A-Za-z0-9_.\-\\/@])((?:[A-Za-z0-9_.\-]+\/)*[A-Za-z0-9_.\-]+\.md)(?::\d+(?:-\d+)?)?(?:\s*§\s*(\d+(?:\.\d+)*))?(?![\w])/g
|
|
23
|
+
|
|
24
|
+
/** 文件存在性(目录 / 读错 ⇒ false)。 */
|
|
25
|
+
function isFile(p) { try { return statSync(p).isFile() } catch { return false } }
|
|
26
|
+
|
|
27
|
+
/** 标题编号(标题文本去 `^#{1,6}\s` 后的首个 `\d+(?:\.\d+)*`);非标题行 ⇒ null。 */
|
|
28
|
+
function headingNumber(line) {
|
|
29
|
+
const m = /^#{1,6}\s+/.exec(line)
|
|
30
|
+
if (!m) return null
|
|
31
|
+
const n = /\d+(?:\.\d+)*/.exec(line.slice(m[0].length))
|
|
32
|
+
return n ? n[0] : null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 节在盘:目标档内任一标题行的编号 = 指针节号(字符串逐字等值——两处同归一形)。 */
|
|
36
|
+
function sectionOnDisk(path, num) {
|
|
37
|
+
let text = ""
|
|
38
|
+
try { text = readFileSync(path, "utf8") } catch { return false }
|
|
39
|
+
for (const l of text.split("\n")) if (headingNumber(l) === num) return true
|
|
40
|
+
return false
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 档解析(判据 ① 的解析序 ①–④):命中 ⇒ `{ ok, path }`;同名 ≥2 ⇒ `{ ambiguous, candidates }`;零命中 ⇒ `{ ok:false }`。
|
|
44
|
+
* 行号坐标尾(`:N`)不参与档解析——token 只取 `.md` 段(抽取面已剥离坐标尾与节指示)。 */
|
|
45
|
+
function resolveDoc(token, refFile, base, scanDirs, index) {
|
|
46
|
+
const p1 = resolve(base, token)
|
|
47
|
+
if (isFile(p1)) return { ok: true, path: p1 } // ① 仓根相对
|
|
48
|
+
for (const d of scanDirs) { const p = resolve(base, d, token); if (isFile(p)) return { ok: true, path: p } } // ② 域根相对
|
|
49
|
+
const p3 = resolve(dirname(refFile), token)
|
|
50
|
+
if (isFile(p3)) return { ok: true, path: p3 } // ③ 引用方档所在目录相对
|
|
51
|
+
const cands = index.get(basename(token)) ?? [] // ④ 域内唯一 basename 索引
|
|
52
|
+
if (cands.length === 1) return { ok: true, path: cands[0] }
|
|
53
|
+
if (cands.length > 1) return { ambiguous: true, candidates: cands }
|
|
54
|
+
return { ok: false }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 锚判据主行程:域内逐档逐行抽取 + 解析 + 节在盘判定。
|
|
59
|
+
* @param {string} root 项目根(判据参数已由调用方自 `checkConfig` 读出)
|
|
60
|
+
* @param {{scanDirs?:string[], files?:string[]}} o `files` = 域内 `.md` 全集(单源 = `scanDomain`)
|
|
61
|
+
* @returns {{hits:Array, reports:Array}} hits = 命中(`档缺` / `节缺`);reports = 报告面(多义——不作判)
|
|
62
|
+
*/
|
|
63
|
+
export function checkAnchors(root, { scanDirs = [], files = [] } = {}) {
|
|
64
|
+
const base = resolve(root)
|
|
65
|
+
const dirs = Array.isArray(scanDirs) ? scanDirs : []
|
|
66
|
+
const index = new Map()
|
|
67
|
+
for (const f of files) {
|
|
68
|
+
const k = basename(f)
|
|
69
|
+
if (!index.has(k)) index.set(k, [])
|
|
70
|
+
index.get(k).push(f)
|
|
71
|
+
}
|
|
72
|
+
const hits = []
|
|
73
|
+
const reports = []
|
|
74
|
+
for (const f of files) {
|
|
75
|
+
let text = ""
|
|
76
|
+
try { text = readFileSync(f, "utf8") } catch { continue }
|
|
77
|
+
const re = new RegExp(ANCHOR_RE.source, "g") // 每档一份实例(零共享 lastIndex 状态)
|
|
78
|
+
let fence = null // 围栏标记(` ∥ ~);null = 不在块内
|
|
79
|
+
text.split("\n").forEach((l, i) => {
|
|
80
|
+
const mark = fenceMark(l)
|
|
81
|
+
if (mark !== null) { // 围栏块整块跳过:起于围栏行、止于同形围栏行(未闭合 ⇒ 至档尾免)
|
|
82
|
+
if (fence === null) fence = mark
|
|
83
|
+
else if (mark === fence) fence = null
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
if (fence !== null) return
|
|
87
|
+
if (isExecutableLine(l)) return // 可执行行整行跳过
|
|
88
|
+
let m
|
|
89
|
+
while ((m = re.exec(l)) !== null) {
|
|
90
|
+
const token = m[1]
|
|
91
|
+
const num = m[2] ?? null
|
|
92
|
+
const r = resolveDoc(token, f, base, dirs, index)
|
|
93
|
+
if (r.ambiguous) { reports.push({ file: f, line: i + 1, token, candidates: r.candidates }); continue }
|
|
94
|
+
if (!r.ok) { hits.push({ file: f, line: i + 1, token, clazz: "档缺" }); continue }
|
|
95
|
+
if (num !== null && !sectionOnDisk(r.path, num)) hits.push({ file: f, line: i + 1, token, clazz: "节缺", section: num, target: r.path })
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
return { hits, reports }
|
|
100
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* doc-check/index.mjs — 文档纪律机检 · 引擎入口(域扫描 + 声明面读取 + 报面行)。
|
|
3
|
+
*
|
|
4
|
+
* 判据两条(起步 · 闭集——口径单源 = `docs/core/design/DOC-DISCIPLINE.md` §3.20 条目 S):
|
|
5
|
+
* ① 锚(档缺 / 节缺)——实现 = `./anchors.mjs`;② 行宽——口径 = §3.18 条目 Q,实现 = `./width.mjs`。
|
|
6
|
+
* 判据参数由**被开发项目**的 `PROJECT-MANIFEST.json` `checkConfig` 声明(不硬编码任何本仓路径)。
|
|
7
|
+
* 纯函数:**零门禁 / 零退出码 / 零阻断**——报告态由工具面 `agent-tools/doc-check.mjs` 呈现;
|
|
8
|
+
* 输出不得被读作拒执行的理由(用户令 > 机检)。
|
|
9
|
+
*/
|
|
10
|
+
import { relative, resolve } from "node:path"
|
|
11
|
+
import { MANIFEST_REL, readManifest } from "../manifest.mjs"
|
|
12
|
+
import { scanDomain, checkDocWidths } from "./width.mjs"
|
|
13
|
+
import { checkAnchors } from "./anchors.mjs"
|
|
14
|
+
|
|
15
|
+
/** 相对路径显示形(正斜杠——跨平台同形)。 */
|
|
16
|
+
const rel = (root, p) => relative(root, p).replace(/\\/g, "/")
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 跑一次文档纪律机检(报告态)。
|
|
20
|
+
* @param {string} root 项目根(判据参数取该根的 `PROJECT-MANIFEST.json` `checkConfig`)
|
|
21
|
+
* @returns {{report:string, hits:{total:number, anchors:number, width:number}, anchorHits:Array,
|
|
22
|
+
* widthHits:Array, anchorReports:Array, files:string[], scanDirs:string[], lineWidth:number,
|
|
23
|
+
* zones:string[], exclude:string[]}}
|
|
24
|
+
*/
|
|
25
|
+
export function runDocCheck(root) {
|
|
26
|
+
const base = resolve(root)
|
|
27
|
+
const read = readManifest(base)
|
|
28
|
+
if (!read.ok) {
|
|
29
|
+
throw new Error(`doc_check: cannot read ${MANIFEST_REL} at ${base} — the check parameters come from that project's checkConfig (${read.reason ?? read.errors.join(";")})`)
|
|
30
|
+
}
|
|
31
|
+
const cfg = read.manifest.checkConfig ?? {}
|
|
32
|
+
const scanDirs = (Array.isArray(cfg.scanDirs) ? cfg.scanDirs : []).filter((d) => typeof d === "string" && d !== "")
|
|
33
|
+
const exclude = Array.isArray(cfg.anchors?.exclude) ? cfg.anchors.exclude : []
|
|
34
|
+
const lineWidth = cfg.lineWidth
|
|
35
|
+
const zones = (Array.isArray(cfg.widthExemptZones) ? cfg.widthExemptZones : []).filter((z) => typeof z === "string" && z !== "")
|
|
36
|
+
|
|
37
|
+
const files = scanDomain(base, { scanDirs, exclude })
|
|
38
|
+
const anchors = checkAnchors(base, { scanDirs, files })
|
|
39
|
+
const widths = checkDocWidths(base, { lineWidth, scanDirs, exclude, exemptZones: zones, files })
|
|
40
|
+
|
|
41
|
+
const total = anchors.hits.length + widths.length
|
|
42
|
+
const out = [
|
|
43
|
+
"文档纪律机检(报告态 · 非门禁)",
|
|
44
|
+
`射程:${base} · ${scanDirs.join(" + ") || "(无)"}(${files.length} 档)· 阈值 ${lineWidth} 字符 · 区带豁免:${zones.length ? zones.join(" ∥ ") : "无"} · 排除:${exclude.length ? exclude.join(" ∥ ") : "(无)"}`,
|
|
45
|
+
"声明:判据参数依据 `PROJECT-MANIFEST.json` `checkConfig`",
|
|
46
|
+
`命中 ${total} 条(锚 ${anchors.hits.length} · 行宽 ${widths.length})`,
|
|
47
|
+
]
|
|
48
|
+
// 报告面(不作判):域内同名 ≥2 ⇒ 多义不判(残余登记——fail-open 方向,归评审核验面)
|
|
49
|
+
for (const r of anchors.reports) {
|
|
50
|
+
out.push(`报告 锚·多义 ${rel(base, r.file)}:${r.line} \`${r.token}\`(同名 ${r.candidates.length}:${r.candidates.map((c) => rel(base, c)).join(" ∥ ")}——报告面,不作判)`)
|
|
51
|
+
}
|
|
52
|
+
for (const h of anchors.hits) {
|
|
53
|
+
out.push(h.clazz === "档缺"
|
|
54
|
+
? `✗ 锚·档缺 ${rel(base, h.file)}:${h.line} \`${h.token}\``
|
|
55
|
+
: `✗ 锚·节缺 ${rel(base, h.file)}:${h.line} \`${h.token}\`(目标 ${rel(base, h.target)} 无 §${h.section})`)
|
|
56
|
+
}
|
|
57
|
+
for (const w of widths) out.push(`✗ 行宽 ${rel(base, w.file)}:${w.line}(${w.len} 字符)`)
|
|
58
|
+
out.push(total ? `结论:${total} 条命中——报告态,不作拒执行的理由` : "结论:0 条命中(锚 ∥ 行宽 均清)")
|
|
59
|
+
|
|
60
|
+
return { report: out.join("\n"), hits: { total, anchors: anchors.hits.length, width: widths.length }, anchorHits: anchors.hits, widthHits: widths, anchorReports: anchors.reports, files, scanDirs, lineWidth, zones, exclude }
|
|
61
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* doc-check/width.mjs — 文档纪律机检的产品档位(行宽谓词族 + 域扫描 + 行宽判据——实现单源)。
|
|
3
|
+
*
|
|
4
|
+
* 口径单源 = `docs/core/design/DOC-DISCIPLINE.md` §3.18 条目 Q(度量 / 域 / 豁免类 / 阈值);
|
|
5
|
+
* 产品内建裁定 = 同档 §3.20 条目 S——谓词单源随迁:本仓 `scripts/doc-check-width.mjs` 改述为
|
|
6
|
+
* **再出口缝**(薄档 · 零逻辑 · 逐名再出口),盘上不留第二份谓词实现。
|
|
7
|
+
* 度量 = `[...line].length`(CJK 计 1——不引显示宽度 / 字体);阈值 / 域 / 排除全部读
|
|
8
|
+
* `PROJECT-MANIFEST.json` 的 `checkConfig`——零硬编码本仓路径,换项目开箱可用。
|
|
9
|
+
* 豁免五类 = 表格行 ∥ fenced 块 ∥ 可执行行 ∥ 连续引文 ∥ 区带:**只认结构谓词**,
|
|
10
|
+
* 无行级白名单(按行号 / 注释声明的豁免不入判)。
|
|
11
|
+
* 边界:本档零门禁 / 零退出码 / 零阻断——报告态由 `agent-tools/doc-check.mjs` 呈现。
|
|
12
|
+
*/
|
|
13
|
+
import { readdirSync, readFileSync, statSync } from "node:fs"
|
|
14
|
+
import { join, resolve } from "node:path"
|
|
15
|
+
import { MANIFEST_REL } from "../manifest.mjs"
|
|
16
|
+
|
|
17
|
+
/** 域发现跳过目录(不视作候选产品域;递归亦不进入)。 */
|
|
18
|
+
const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", "coverage", ".turbo", ".thincoder"])
|
|
19
|
+
|
|
20
|
+
/** 域发现(声明面):cwd 根(持 manifest)+ 各持自身 manifest 的一级子目录——判据 = manifest 在场,无路径硬编码。 */
|
|
21
|
+
export function discoverDomains(root) {
|
|
22
|
+
const rootAbs = resolve(root)
|
|
23
|
+
const hasManifest = (p) => { try { return statSync(join(p, MANIFEST_REL)).isFile() } catch { return false } }
|
|
24
|
+
const out = []
|
|
25
|
+
if (hasManifest(rootAbs)) out.push(rootAbs)
|
|
26
|
+
let names = []
|
|
27
|
+
try { names = readdirSync(rootAbs) } catch { /* 不可读:仅根域 */ }
|
|
28
|
+
for (const n of names) {
|
|
29
|
+
if (SKIP_DIRS.has(n) || n.startsWith("dist-")) continue
|
|
30
|
+
const p = join(rootAbs, n)
|
|
31
|
+
try { if (statSync(p).isDirectory() && hasManifest(p)) out.push(p) } catch { /* 跳过 */ }
|
|
32
|
+
}
|
|
33
|
+
return out
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 递归列 .md(跳过 SKIP_DIRS ∧ 声明面 exclude 段——exclude 逐层生效)。 */
|
|
37
|
+
function walkMd(dir, exclude, out) {
|
|
38
|
+
let names = []
|
|
39
|
+
try { names = readdirSync(dir) } catch { return out }
|
|
40
|
+
for (const n of names) {
|
|
41
|
+
if (SKIP_DIRS.has(n) || n.startsWith("dist-") || exclude.includes(n)) continue
|
|
42
|
+
const p = join(dir, n)
|
|
43
|
+
let st
|
|
44
|
+
try { st = statSync(p) } catch { continue }
|
|
45
|
+
if (st.isDirectory()) walkMd(p, exclude, out)
|
|
46
|
+
else if (n.endsWith(".md")) out.push(p)
|
|
47
|
+
}
|
|
48
|
+
return out
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 源域(单源):`scanDirs` × `exclude` 声明面下的全部 .md——锚检查与行宽检查同一文件集。 */
|
|
52
|
+
export function scanDomain(base, { scanDirs = [], exclude = [] } = {}) {
|
|
53
|
+
const ex = Array.isArray(exclude) ? exclude : []
|
|
54
|
+
const out = []
|
|
55
|
+
for (const d of Array.isArray(scanDirs) ? scanDirs : []) walkMd(resolve(base, d), ex, out)
|
|
56
|
+
return [...new Set(out)].sort()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 表格行谓词(markdown 表格行结构性不可折行——行宽豁免,与锚检查同源)。 */
|
|
60
|
+
export const isTableRow = (l) => /^\s*\|.*\|\s*$/.test(String(l))
|
|
61
|
+
|
|
62
|
+
/** 围栏标记(行首可选空白 + 三反引号 ∥ 三波浪号 ⇒ 标记字符;非围栏行 ⇒ null)——**单源**:
|
|
63
|
+
* `isFenceLine` 与两处块扫描(本节行宽 ∥ `./anchors.mjs` 锚抽取)同取此式,使「同形围栏行止」只此一解
|
|
64
|
+
* (同一模块内同符两义 = 单源可漂的结构口子)。 */
|
|
65
|
+
export function fenceMark(l) {
|
|
66
|
+
const m = /^\s*(`{3,}|~{3,})/.exec(String(l))
|
|
67
|
+
return m ? m[1][0] : null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 围栏行谓词(单源 = `fenceMark`)——围栏行自身与块内整行均豁免。 */
|
|
71
|
+
export const isFenceLine = (l) => fenceMark(l) !== null
|
|
72
|
+
|
|
73
|
+
/** 引文块行谓词(行首可选空白 + `>`)——连续 `>` 行整块豁免(引证须逐字连续、不可折,同表行理据)。 */
|
|
74
|
+
export const isQuoteLine = (l) => /^\s*>/.test(String(l))
|
|
75
|
+
|
|
76
|
+
/** 区带头谓词(§3.15 谓词面):标题文本去首部编号(`^\d+(?:\.\d+)*[.、]?\s*`)后——等于区带名 ∨ 以
|
|
77
|
+
* 「区带名 + (∕(」起(容括注后缀,如 `变更记录(续)`);闭集外一律不纳(fail-closed)。 */
|
|
78
|
+
export function isZoneHead(headingText, zones = []) {
|
|
79
|
+
const text = String(headingText).replace(/^\d+(?:\.\d+)*[.、]?\s*/, "")
|
|
80
|
+
return zones.some((z) => typeof z === "string" && z !== "" && (text === z || text.startsWith(z + "(") || text.startsWith(z + "(")))
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** 行内可执行坐标(命令 / 搜索模式串码段)——整行豁免(证据与命令保字面,与锚判据同源)。 */
|
|
84
|
+
export function isExecutableLine(line) {
|
|
85
|
+
const l = String(line)
|
|
86
|
+
if (/`[^`\n]*&&[^`\n]*`/.test(l)) return true // 命令链(cd … && …)
|
|
87
|
+
if (/`[^`\n]*\b(?:cd|node|npm|npx|grep|rg|git)\s[^`\n]*`/.test(l)) return true // 码段内命令词
|
|
88
|
+
if (/`[^`\n]*(?:\[\^|\\\.|\(\?<|\(\?:|\{\d)/.test(l)) return true // 码段内搜索模式串片段
|
|
89
|
+
return false
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** 行内位置是否在反引号码段内(引述面不判)。 */
|
|
93
|
+
export function inCodeSpan(line, idx) {
|
|
94
|
+
return (String(line).slice(0, idx).match(/`/g) ?? []).length % 2 === 1
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** 行宽判据(单判据 = `checkConfig.lineWidth`):`[...line].length` > 阈值 ∧ 非表格行 ∧ 非 fenced 块内
|
|
98
|
+
* ∧ 非可执行行 ∧ 非连续引文 ∧ 非区带内 ⇔ 命中(口径逐字 = §3.18 条目 Q)。
|
|
99
|
+
* 单遍扫描携标题栈(`^(#{1,6})\s` 行入栈——同级 ∥ 更高级出栈):行所属标题链任一为区带头 ⇒ 豁免。
|
|
100
|
+
* 围栏未闭合 ⇒ 至档尾整块免(fail-open 方向——§3.18 残余登记 ①);`exemptZones` 缺省 ∥ 空集 ⇒ 零豁免。 */
|
|
101
|
+
export function checkDocWidths(base, { lineWidth, scanDirs, exclude, exemptZones = [], files } = {}) {
|
|
102
|
+
const zones = (Array.isArray(exemptZones) ? exemptZones : []).filter((z) => typeof z === "string" && z !== "")
|
|
103
|
+
const hits = []
|
|
104
|
+
// `files` 给定时直接用(调用方已扫同一域 ⇒ 域只走一遍、文件集由值保证同一);缺省自扫(既有调用面不变)。
|
|
105
|
+
for (const f of files ?? scanDomain(base, { scanDirs, exclude })) {
|
|
106
|
+
let text = ""
|
|
107
|
+
try { text = readFileSync(f, "utf8") } catch { continue }
|
|
108
|
+
const stack = [] // 标题栈 —— { level, exempt };exempt = 自身 ∥ 祖先命中区带
|
|
109
|
+
let fence = null // 围栏标记(` ∥ ~);null = 不在块内
|
|
110
|
+
text.split("\n").forEach((l, i) => {
|
|
111
|
+
const mark = fenceMark(l)
|
|
112
|
+
if (mark !== null) {
|
|
113
|
+
if (fence === null) fence = mark
|
|
114
|
+
else if (mark === fence) fence = null
|
|
115
|
+
return // 围栏行自身免
|
|
116
|
+
}
|
|
117
|
+
if (fence !== null) return // 块内整块免(未闭合 ⇒ 至档尾免)
|
|
118
|
+
const hm = /^(#{1,6})\s+(.*)$/.exec(l)
|
|
119
|
+
if (hm) {
|
|
120
|
+
while (stack.length && stack[stack.length - 1].level >= hm[1].length) stack.pop()
|
|
121
|
+
const self = isZoneHead(hm[2].trim(), zones)
|
|
122
|
+
stack.push({ level: hm[1].length, exempt: self || (stack[stack.length - 1]?.exempt ?? false) })
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
const inZone = stack[stack.length - 1]?.exempt === true
|
|
126
|
+
const len = [...l].length
|
|
127
|
+
if (len > lineWidth && !isTableRow(l) && !isExecutableLine(l) && !isQuoteLine(l) && !inZone) hits.push({ file: f, line: i + 1, len })
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
return hits
|
|
131
|
+
}
|
package/file-refs.mjs
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
* 上提源 = VSC `thincoder-vscode/src/extension/file-refs.mjs` 自持档(逐字搬——regex ∕ 4000 截断 ∕
|
|
4
4
|
* `[File: …]` 围栏 ∕ 摘要块格式零语义改良);本档 = **单源**;两端各持薄壳(端侧 `node:fs` 探针注入):
|
|
5
5
|
* VSC `thincoder-vscode/src/extension/file-refs.mjs` ∥ 桌面 `thincoder-desktop/src/main/file-refs.mjs`。
|
|
6
|
-
* 消费面:VSC 注入 `panel-chat.mjs
|
|
7
|
-
* `panel-session-write.mjs
|
|
8
|
-
* 恢复剥离 = `session-slots.mjs` `pageHistory` ∥ 标题剥离 = 核 `generate-title.mjs
|
|
9
|
-
* 文本形逐字冻结(`docs/vsc/design/WEBVIEW.md
|
|
6
|
+
* 消费面:VSC 注入 `panel-chat.mjs` 的 `injectAtRefs` ∕ 恢复剥离 `panel-session.mjs` 的 `stripAtRefs` ∕ 标题剥离
|
|
7
|
+
* `panel-session-write.mjs` 的 `generateTitle`;桌面注入缝 = `turn-driver.mjs`(起跑前单点,`turn-face.mjs`)∥
|
|
8
|
+
* 恢复剥离 = `session-slots.mjs` `pageHistory` ∥ 标题剥离 = 核 `generate-title.mjs` 的 `ensureSessionTitle`(读源处)。
|
|
9
|
+
* 文本形逐字冻结(`docs/vsc/design/WEBVIEW.md` §4.4「落线文本逐字不变」契约):上提不携任何行为面改动。
|
|
10
10
|
* 判据 = **盘面探针注入缝**(`probe = { readFileSync, existsSync, statSync }` —— `node:fs` 同名面;
|
|
11
|
-
* 缺 ∕ 形违 ⇒ 抛 —— fail-loud:盘面闸未接线不得静默扮成「零引用」,沿 `file-links.mjs
|
|
11
|
+
* 缺 ∕ 形违 ⇒ 抛 —— fail-loud:盘面闸未接线不得静默扮成「零引用」,沿 `file-links.mjs` 的 `extractFileLinks` 先例)。
|
|
12
12
|
* `stripAtRefs` = 注入语法的逆变换:fail-closed 判据逐字依赖注入输出格式 —— 语法与其逆变换同住一处,
|
|
13
13
|
* 零第二实现(本档为唯一产者)。
|
|
14
14
|
*/
|
|
@@ -26,7 +26,7 @@ function probeOf(probe) {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
/** Scan text for @path references and replace with inline file content.
|
|
29
|
-
* 零文本(非串 ∥ 空串)先于探针校验早退(零判据——探针不检,同序沿核 `file-links.mjs
|
|
29
|
+
* 零文本(非串 ∥ 空串)先于探针校验早退(零判据——探针不检,同序沿核 `file-links.mjs` 的 `extractFileLinks`)。 */
|
|
30
30
|
export function injectAtRefs(text, cwd, probe) {
|
|
31
31
|
if (typeof text !== "string" || text === "") return text
|
|
32
32
|
const io = probeOf(probe)
|
package/generate-title.mjs
CHANGED
|
@@ -14,7 +14,7 @@ import { resolveProviderSecrets } from "./env-ref.mjs"
|
|
|
14
14
|
// (`isRealUserMsg`——纯函数零依赖,无环)。
|
|
15
15
|
import { isRealUserMsg } from "./history-window.mjs"
|
|
16
16
|
// #632(缺面族批补 · @ 文件引用对齐 · 2026-09-29):标题源剥离单源——注入形不得进标题请求(核件
|
|
17
|
-
// `file-refs.mjs`;CLI 无注入 ⇒ 零动作;VSC 同位转口 = `panel-session-write.mjs
|
|
17
|
+
// `file-refs.mjs`;CLI 无注入 ⇒ 零动作;VSC 同位转口 = `panel-session-write.mjs` 的 `generateTitle`)。
|
|
18
18
|
import { stripAtRefs } from "./file-refs.mjs"
|
|
19
19
|
// #363③(off 形族单源——MODEL-SPECS.md §16.2 / §16.4):禁思考形与「是否可禁」一律引核
|
|
20
20
|
// (`thinkOffShape` / `thinkOffPath`——本档零副本);spec 查表同单源(`specForModel`)。
|
package/hooks.mjs
CHANGED
|
@@ -98,7 +98,7 @@ async function runOneHook(event, hook, ctx) {
|
|
|
98
98
|
// Timeout guard(#135-④:`timeout` ms 到点杀掉子进程 + 放行——原由 spawn 的 `timeout`
|
|
99
99
|
// 选项承担,见上)。句柄捕获 + `unref()`:进程存活期间守卫照常触发(真 CLI 运行事件循环
|
|
100
100
|
// 另有 handle),但不再单独维持事件循环存活 ⇒ 子进程钩子用例的测试进程零尾挂;
|
|
101
|
-
// 任一出口经 `done` 收尾 clearTimeout(house 先例 = mcp/transport-stdio.mjs
|
|
101
|
+
// 任一出口经 `done` 收尾 clearTimeout(house 先例 = `mcp/transport-stdio.mjs` 的 `killTree`)。
|
|
102
102
|
guard = setTimeout(() => {
|
|
103
103
|
try { proc.kill() } catch { /* already gone */ }
|
|
104
104
|
done(0)
|
package/i18n.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* - `projectDictionary(locale)` = **VSC 字典投影**:核常量 → VSC `locales/*.json` 的扁平
|
|
8
8
|
* 字典形态(VSC 的 `i18n.mjs` `t()` 壳在 S2 接线时并入本投影,壳内零文案)。
|
|
9
9
|
*
|
|
10
|
-
* 契约(CORE-UNIFICATION §2.12.2
|
|
10
|
+
* 契约(CORE-UNIFICATION §2.12.2「输出文案类」):**机器消费面冻结**——本表键值
|
|
11
11
|
* 与现 VSC `locales/{en,zh}.json` 逐字一致(已锁定串不得改);人读文案后续变更须登记 +
|
|
12
12
|
* CHANGELOG(S2 落地物)。`${name}` = 插值占位(两端同约定)。
|
|
13
13
|
*
|
|
@@ -76,6 +76,16 @@ export const CORE_MESSAGES = Object.freeze({
|
|
|
76
76
|
"tool.done": { en: "done", zh: "完成" },
|
|
77
77
|
// ── 台账(ledger)· F-LX3 变体键库首跑提示(批 2026-10-05-ledger-variant-db-notice · 台账 #935 · LEDGER.md §12.2 逐字;消费面 = CLI TUI 启动行)
|
|
78
78
|
"ledger.variantDbNotice": { en: "Legacy variant-key ledger DB found for this project (pre-upgrade; rows may be outside the current ledger): thincoder ledger audit to inspect; thincoder ledger migrate to reconcile", zh: "检测到本项目的存量变体键台账库(升级遗留,数据可能未并入当前库):thincoder ledger audit 查看;thincoder ledger migrate 收正" },
|
|
79
|
+
// ── SSH 会话(批 ssh-session · 台账 #1436 · TOOLS.md §6.22 ⑧——凭据窗词面单源;三端经本字典投影)──
|
|
80
|
+
"ssh.credentialTitle": { en: "SSH credential", zh: "SSH 凭据" },
|
|
81
|
+
"ssh.credentialPrompt": { en: "Enter the login credential for ${host}", zh: "输入 ${host} 的登录凭据" },
|
|
82
|
+
"ssh.secretTitle": { en: "SSH secret", zh: "SSH 密文" },
|
|
83
|
+
"ssh.secretPrompt": { en: "Enter the secret for ${host}", zh: "输入 ${host} 的密文" },
|
|
84
|
+
"ssh.field.user": { en: "Username", zh: "用户名" },
|
|
85
|
+
"ssh.field.password": { en: "Password", zh: "密码" },
|
|
86
|
+
"ssh.field.secret": { en: "Secret", zh: "密文" },
|
|
87
|
+
// 无 UI 失败句(E6;三端投影单源):en 侧 = CLI 端 `SECRET_NO_UI_TEXT` 逐字(端常量收正为核键)。
|
|
88
|
+
"ssh.no_ui": { en: "ssh credential input requires an interactive UI (none in this context) - use key-based auth or ssh-agent", zh: "ssh 凭据输入需要交互界面(当前场合没有)——请改用密钥认证或 ssh-agent" },
|
|
79
89
|
})
|
|
80
90
|
// 深度冻结(机器消费面冻结——嵌套 { en, zh } 也冻结;浅冻结下值可被静默改写)。
|
|
81
91
|
for (const entry of Object.values(CORE_MESSAGES)) Object.freeze(entry)
|