@thincoder/core 0.10.7 → 0.10.9
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 +21 -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 +13 -8
- package/agent/helpers.mjs +12 -10
- package/agent/image-heal.mjs +65 -0
- package/agent/light-round.mjs +4 -4
- package/agent/live-beat.mjs +4 -4
- package/agent/post-turn.mjs +34 -0
- package/agent/record-results.mjs +9 -1
- package/agent/run-stages.mjs +45 -10
- package/agent/setup-reminders.mjs +14 -13
- package/agent/setup.mjs +72 -48
- 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 +7 -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 +5 -3
- package/agent-tools/batch-gate.mjs +198 -0
- package/agent-tools/batch-lifecycle.mjs +216 -36
- package/agent-tools/batch-paths.mjs +20 -20
- package/agent-tools/batch-skeleton.mjs +87 -0
- package/agent-tools/batch.mjs +11 -3
- package/agent-tools/checkpoint.mjs +1 -1
- package/agent-tools/context.mjs +28 -5
- package/agent-tools/design-slots.mjs +20 -7
- package/agent-tools/doc-check.mjs +75 -0
- package/agent-tools/eng.mjs +5 -3
- package/agent-tools/manifest.mjs +18 -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 +108 -47
- package/agent-tools/ssh-session.mjs +486 -0
- package/agent-tools/ssh-transport.mjs +324 -0
- package/agent-tools/ssh-vault.mjs +291 -0
- package/agent-tools/subagent-scheduler.mjs +6 -5
- package/agent-tools/subagent-spawn.mjs +18 -11
- package/agent-tools/subagent.mjs +17 -9
- package/agent-tools/task.mjs +1 -1
- package/agent-tools/timer.mjs +8 -2
- 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-defaults.mjs +92 -0
- package/config-io.mjs +7 -2
- package/config-normalize.mjs +109 -0
- package/config-watch.mjs +2 -2
- package/config.mjs +88 -190
- package/declaration.mjs +5 -1
- package/doc-check/anchors.mjs +142 -0
- package/doc-check/index.mjs +210 -0
- package/doc-check/version.mjs +84 -0
- package/doc-check/width.mjs +132 -0
- package/file-refs.mjs +6 -6
- package/generate-title.mjs +1 -1
- package/hooks.mjs +1 -1
- package/i18n.mjs +13 -1
- package/ledger-cmd.mjs +314 -62
- package/ledger-db.mjs +122 -31
- package/ledger-migrate.mjs +18 -12
- package/ledger-read.mjs +6 -7
- package/ledger-tools.mjs +138 -77
- package/ledger.mjs +34 -62
- package/log.mjs +17 -3
- package/manifest-discovery.mjs +21 -18
- package/manifest-schema.mjs +17 -14
- package/manifest.mjs +37 -22
- package/mcp/transport-stdio.mjs +1 -1
- package/mcp.mjs +2 -2
- package/memory/core.mjs +1 -1
- package/memory/scan.mjs +1 -1
- package/memory/sweep.mjs +1 -1
- package/model-specs.mjs +7 -7
- package/notify-policy.mjs +6 -6
- package/package.json +2 -1
- package/permission.mjs +21 -1
- package/prompts/advisor-design.md +53 -23
- package/prompts/advisor-round1.md +34 -14
- package/prompts/advisor-round2.md +32 -19
- package/prompts/advisor-round3.md +34 -21
- package/prompts/common.md +148 -79
- package/prompts/consult-base.md +14 -8
- package/prompts/discipline-engineering.md +70 -66
- package/prompts/discipline-normal.md +86 -77
- package/prompts/persona-coder.md +6 -6
- package/prompts/persona-eng-coder.md +19 -18
- package/prompts/persona-eng-designer.md +40 -31
- package/prompts/persona-engineering.md +124 -92
- package/prompts/persona-explore.md +2 -2
- package/prompts/persona-normal.md +20 -17
- package/prompts/persona-plan.md +7 -7
- package/prompts/subagent-base.md +4 -4
- package/provider/anthropic.mjs +4 -2
- package/provider/core.mjs +4 -2
- package/provider/errors.mjs +8 -0
- package/provider/google.mjs +8 -1
- package/provider/normalize.mjs +42 -12
- package/provider/responses-request.mjs +4 -3
- 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 +7 -7
- 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 +63 -5
- package/think-off.mjs +3 -3
- package/token-ttl.mjs +1 -1
- package/token-window.mjs +1 -1
- package/tool-docs/bash.md +1 -1
- package/tool-docs/batch.md +1 -1
- package/tool-docs/browser.md +1 -1
- package/tool-docs/doc_check.md +54 -0
- package/tool-docs/fetch.md +3 -2
- package/tool-docs/ledger.md +7 -5
- package/tool-docs/manifest.md +14 -2
- package/tool-docs/ssh.md +51 -0
- package/tool-docs/subagent.md +6 -9
- package/tool-docs/timer.md +2 -1
- package/tool-docs/websearch.md +3 -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 +105 -0
- package/tools/web-deepseek.mjs +172 -0
- package/tools/web.mjs +29 -4
- 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
|
@@ -11,8 +11,7 @@
|
|
|
11
11
|
* API key comes from providers[].apiKey only — environment variables are not a key source.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import {
|
|
15
|
-
import { dirname, join } from "node:path"
|
|
14
|
+
import { existsSync, readFileSync } from "node:fs"
|
|
16
15
|
import { parseModelRef, resolveChannelModel, resolveProviderPlan, resolveRuntimeProvider, defaultModelReason } from "./model-ref.mjs"
|
|
17
16
|
// 老形态迁移核(M7 v2——纯函数零依赖——本文件超 500 行硬限拆分,VSC 同构文件)
|
|
18
17
|
import { migrateLegacyModelFields } from "./config-migrate.mjs"
|
|
@@ -30,72 +29,10 @@ export { writeConfigAtomic }
|
|
|
30
29
|
|
|
31
30
|
function cfgPath() { return _configPath() }
|
|
32
31
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
subagentTurns: 100,
|
|
38
|
-
subagentModel: null, // default subagent model: "provider:model" | model name (parent provider); null = inherit parent provider
|
|
39
|
-
subagentModels: {}, // per-type override: { explore, plan, coder, "eng-coder" } — priority: subagent tool model arg > this[role] > subagentModel > parent provider
|
|
40
|
-
goalTurns: 200,
|
|
41
|
-
compactThreshold: 100000,
|
|
42
|
-
verifyGuard: false, // push model back to verify when files were mutated but verify not run (opt-in)
|
|
43
|
-
// Multi-model consultation ("会诊") + escalate ("飞刀") — CLI parity with the VS Code plugin.
|
|
44
|
-
// consultModels: candidate pool for BOTH consult and escalate ({ provider, model, effort? }, up to 5).
|
|
45
|
-
consultModels: [],
|
|
46
|
-
consultTurns: 40, // per-consultant tool-turn budget (diagnosis tasks)
|
|
47
|
-
consultTimeoutMs: 600000, // wall-clock ceiling per consultant (10min)
|
|
48
|
-
streamRules: [], // time-traveling stream rules: [{ pattern: "regex", message: "reminder", action: "abort"|"warn", repeat: "always"|"once" }]
|
|
49
|
-
advisor: { guard: false }, // code review is always available; guard: true pushes completion back until reviewed (opt-in). Also accepts provider/model/thinking/reasoningEffort/timeoutMs overrides. Deprecated: enabled (2026-08-21)
|
|
50
|
-
autoThink: false, // auto-classify task difficulty and set reasoning effort per-turn
|
|
51
|
-
engineering: false, // initial default only — the runtime value is the session authority (MANIFEST.md §2.2 / ENGINEERING-MODE-V2.md §2.3 E5.1). Strict methodology enforcement — design-before-code (design review + user approval before code)
|
|
52
|
-
// Async pool limits (AGENT-LOOP-ASYNC-POOL.md §6.10 D-24a/R14 + R13 — POOL-CONFIG-
|
|
53
|
-
// UNIFIED 2026-09-09): { engCoder, other, advisor } — eng-coder pool / other-role
|
|
54
|
-
// pool / advisor-review pool, defaults 4/4/4 (user ruling "eng-coder 四路,其他
|
|
55
|
-
// 4 路" + advisor 评审池并入同一可配体系——三池统一默认 4)。engCoder/other
|
|
56
|
-
// runtime-validated at every subagent pool admission (subagent-async
|
|
57
|
-
// resolvePoolLimits); advisor runtime-validated at every advisor launch
|
|
58
|
-
// (advisor-async advisorPoolLimitFor——F-2——两域读取器独立不共享)——非法/缺省
|
|
59
|
-
// 回退 4——settings tool 与 /config 并发池菜单写此键——变更下个 spawn/launch 生效。
|
|
60
|
-
// ⚠ 与 subagent-async.mjs ASYNC_POOL_LIMITS(两键)/advisor-async.mjs
|
|
61
|
-
// ADVISOR_POOL_LIMIT 逐键同值(运行时回退常量)——耦合锚 T-24a4 断言锁住——
|
|
62
|
-
// 勿单侧改默认。
|
|
63
|
-
poolLimits: { engCoder: 4, other: 4, advisor: 4 },
|
|
64
|
-
// §6.30.3 门三件③(D-TW5):timer 空闲自唤醒开关——默认开(工具描述面已承诺到点提醒);
|
|
65
|
-
// false ⇒ 端侧不武装闩(回既有回合边界投递——零调度器面)。键面随 DEFAULTS 派生进 settings 类型表。
|
|
66
|
-
timerWake: true,
|
|
67
|
-
},
|
|
68
|
-
memory: {
|
|
69
|
-
dbPath: join(configDir, "memory.db"),
|
|
70
|
-
projectDir: ".thincoder/memory",
|
|
71
|
-
team: null,
|
|
72
|
-
// 索引自维护(§6.14 面⑤ · 台账 #1096):维护型备份保留枚数(轮转保最新 N——引擎钳 ≥ 1)
|
|
73
|
-
maintain: { backupKeep: 2 },
|
|
74
|
-
},
|
|
75
|
-
shell: null, // bash tool shell executable (e.g. "C:\\Program Files\\Git\\bin\\bash.exe" or "pwsh"); null = system default (cmd on Windows, /bin/sh elsewhere)
|
|
76
|
-
embedding: {
|
|
77
|
-
baseURL: "https://api.siliconflow.cn/v1",
|
|
78
|
-
model: "BAAI/bge-m3",
|
|
79
|
-
},
|
|
80
|
-
mcp: {
|
|
81
|
-
servers: [],
|
|
82
|
-
},
|
|
83
|
-
websearch: {
|
|
84
|
-
// Structured search via Tavily when a key is set — empty apiKey → Bing RSS/HTML fallback (zero-config).
|
|
85
|
-
apiKey: "", // Tavily key (tvly-...) — optional
|
|
86
|
-
},
|
|
87
|
-
// 浏览器工具(BROWSER-TOOL.md §3.2):非空 ⇒ navigate 目标 host ∥ click/evaluate/type 的当前页 host 须命中
|
|
88
|
-
//(全等(大小写不敏感)∥ `*.` 前缀 = 子域通配)——收束面非沙箱;缺省空 = 不设限。
|
|
89
|
-
browser: { allowDomains: [] },
|
|
90
|
-
traces: {
|
|
91
|
-
enabled: false, // §18.6 D-TR6 修订(2026-09-05 用户裁定——发布隐私:"不希望用户那边也采集"):轨迹存档默认 OFF——新用户零采集;本地调试分析可显式开(~/.thincoder/config.json traces.enabled:true)
|
|
92
|
-
retentionHours: 24, // D-TR10:轨迹文件保留小时数——CLI 启动时删除超过该时长的文件(默认 24h)
|
|
93
|
-
},
|
|
94
|
-
// 诊断开关(用户面 ① 配置键——CONFIG.md §6.2 名录):`heapWatch` 默认开(设 false 关闭)——自愈分支
|
|
95
|
-
// 遥测;`heapSnapshot` 默认关——采集 opt-in(堆转储敏感度 ≥ traces ⇒ 对齐发布隐私先例 D-TR6;
|
|
96
|
-
// 2026-09-30 采集收网批;桌面端运行期热读 ∥ CLI 本进程下次启动)
|
|
97
|
-
diagnostics: { heapWatch: true, heapSnapshot: false },
|
|
98
|
-
}
|
|
32
|
+
// 配置默认值表本体已迁 `config-defaults.mjs`(CONTEXT-COMPACTION.md §6.16.9 —— C1:默认值数据 /
|
|
33
|
+
// 加载期归一逻辑 / 请求预算算式三件分家);本档 re-export 保持既有导入面(34 档零改)。
|
|
34
|
+
import { DEFAULTS } from "./config-defaults.mjs"
|
|
35
|
+
export { DEFAULTS }
|
|
99
36
|
|
|
100
37
|
// Model capability table + spec lookup live in model-specs.mjs (2026-08-31
|
|
101
38
|
// extract — config.mjs had grown past the 300-line advisory). Re-exported here
|
|
@@ -104,20 +41,24 @@ import { specForModel, providerSpec, specMatch, assistantToolCallMessage } from
|
|
|
104
41
|
export { specForModel, providerSpec, specMatch, assistantToolCallMessage }
|
|
105
42
|
|
|
106
43
|
|
|
107
|
-
// Window utilization
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
44
|
+
// Window utilization ratio (CONTEXT-COMPACTION.md §6.2 / D-CC30 / §6.17 / D-CC32): auto threshold =
|
|
45
|
+
// min(usableWindow(window, reserve), ⌊window × COMPACT_RATIO⌋) — reserve is the output budget
|
|
46
|
+
// ACTUALLY sent as max_tokens (uncapped; §6.17 requestBudget is the single source). Rows with
|
|
47
|
+
// reserve ≤ 20% of the window land on the ratio term; larger reservations pull the line down to
|
|
48
|
+
// window − reserve — overflow-free outranks the 75–80% band (mathematically exclusive there, §6.2).
|
|
49
|
+
// Headroom above the line absorbs injected context (directory tree, git context, outline, project
|
|
50
|
+
// instructions, memory/doc search results — 30–50K tokens per turn).
|
|
51
|
+
const COMPACT_RATIO = 0.8
|
|
52
|
+
|
|
53
|
+
/** Output reservation counted against the window: the value ACTUALLY sent as max_tokens —
|
|
54
|
+
* explicit `provider.maxTokens` when set (the transport faces send it verbatim), else the spec
|
|
55
|
+
* `maxOutput`, else 0. NOT capped: the old 25% cap reopened a residual band between the counted
|
|
56
|
+
* reservation and the real output budget — messages could cross the true window while still below
|
|
57
|
+
* the compaction line (2026-10-18 overflow 400: budget counted 250_000, 393_216 actually sent).
|
|
58
|
+
* Single source = §6.17 requestBudget; this is its reserve face (advisor budget reuses it). */
|
|
117
59
|
export function outputReserve(provider, spec) {
|
|
118
60
|
const explicit = provider?.maxTokens
|
|
119
|
-
|
|
120
|
-
return spec?.maxOutput ?? 0
|
|
61
|
+
return Number.isFinite(explicit) && explicit > 0 ? explicit : spec?.maxOutput ?? 0
|
|
121
62
|
}
|
|
122
63
|
|
|
123
64
|
/** Usable input window = window − output reservation. Degenerate guard: a reservation
|
|
@@ -132,17 +73,50 @@ export function usableWindow(context, reserve) {
|
|
|
132
73
|
return usable > 0 ? usable : Math.floor(ctx / 2)
|
|
133
74
|
}
|
|
134
75
|
|
|
135
|
-
/**
|
|
136
|
-
*
|
|
137
|
-
*
|
|
76
|
+
/** Request budget — single source for window ∥ actually-sent max_tokens ∥ compaction threshold ∥
|
|
77
|
+
* overflow guard (CONTEXT-COMPACTION.md §6.17 / D-CC32). One formula, four consumer faces:
|
|
78
|
+
* threshold (resolveCompactThreshold / per-run setup), the four transport faces (read ONLY
|
|
79
|
+
* maxTokens), the stats budget line (agent-tools/context.mjs), the pre-send guard
|
|
80
|
+
* (agent/run-stages.mjs). transport derives from provider.format ("anthropic" | "google" |
|
|
81
|
+
* "responses" | default = OpenAI-compatible). Fields:
|
|
82
|
+
* - window / windowSource: spec.context (providers[].context K override already merged by
|
|
83
|
+
* providerSpec); source = "provider-override" when the override is present, else "spec".
|
|
84
|
+
* - maxTokens / maxTokensSource: the value THIS transport writes into the request body —
|
|
85
|
+
* anthropic always sends it (explicit ?? spec.maxOutput); the other three faces send only an
|
|
86
|
+
* explicit provider.maxTokens, absent ⇒ undefined / "absent". Never a guard criterion
|
|
87
|
+
* (undefined ⇒ NaN — §6.17 guard); the guard reads limit instead.
|
|
88
|
+
* - reserve: maxTokens ?? spec.maxOutput — uncapped, always a number.
|
|
89
|
+
* - usable: usableWindow(window, reserve) (degenerate half-window guard).
|
|
90
|
+
* - threshold: min(usable, ⌊window × COMPACT_RATIO⌋) — the compaction trigger line.
|
|
91
|
+
* - limit: window − reserve — hard prompt ceiling; the guard's ONLY numeric criterion field
|
|
92
|
+
* (always a number; negative = any prompt overflows). */
|
|
93
|
+
export function requestBudget(provider, spec = providerSpec(provider)) {
|
|
94
|
+
const window = spec?.context ?? 0
|
|
95
|
+
const k = Number(provider?.context)
|
|
96
|
+
const windowSource = Number.isInteger(k) && k > 0 ? "provider-override" : "spec"
|
|
97
|
+
const explicitRaw = Number(provider?.maxTokens)
|
|
98
|
+
const explicit = Number.isFinite(explicitRaw) && explicitRaw > 0 ? explicitRaw : undefined
|
|
99
|
+
const anthropicAlwaysSends = provider?.format === "anthropic"
|
|
100
|
+
const maxTokens = explicit ?? (anthropicAlwaysSends ? spec?.maxOutput ?? 0 : undefined)
|
|
101
|
+
const maxTokensSource = explicit != null ? "explicit" : anthropicAlwaysSends ? "spec" : "absent"
|
|
102
|
+
const reserve = maxTokens ?? spec?.maxOutput ?? 0
|
|
103
|
+
const usable = usableWindow(window, reserve)
|
|
104
|
+
return {
|
|
105
|
+
window, windowSource, maxTokens, maxTokensSource, reserve, usable,
|
|
106
|
+
threshold: Math.min(usable, Math.floor(window * COMPACT_RATIO)),
|
|
107
|
+
limit: window - reserve,
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Derive compaction threshold; explicit is the value explicitly set in config file (takes priority),
|
|
112
|
+
* otherwise auto = requestBudget().threshold (§6.17 single source — min(usable, ⌊window × 0.8⌋),
|
|
113
|
+
* reserve uncapped). Second param accepts EITHER a model name string (pure spec lookup — compat shape,
|
|
114
|
+
* no caller passes a string today) OR a provider object (providerSpec — the providers[].context
|
|
138
115
|
* override in K units is honored, PROVIDER.md §15 T-C2). */
|
|
139
116
|
export function resolveCompactThreshold(explicit, modelOrProvider) {
|
|
140
117
|
if (explicit != null) return { value: explicit, auto: false }
|
|
141
118
|
const provider = typeof modelOrProvider === "string" ? { model: modelOrProvider } : (modelOrProvider ?? {})
|
|
142
|
-
|
|
143
|
-
const usable = usableWindow(spec.context, outputReserve(provider, spec))
|
|
144
|
-
const value = Math.floor(usable * COMPACT_RATIO)
|
|
145
|
-
return { value, auto: true }
|
|
119
|
+
return { value: requestBudget(provider).threshold, auto: true }
|
|
146
120
|
}
|
|
147
121
|
|
|
148
122
|
/**
|
|
@@ -171,77 +145,28 @@ export function resolveEnableThinking(provider, spec) {
|
|
|
171
145
|
const model = (typeof raw === "string" ? raw : "").toLowerCase()
|
|
172
146
|
if (!model.startsWith("qwen") || model.startsWith("qwen3-coder")) return undefined
|
|
173
147
|
if (!isBailianHost(provider?.baseURL)) return undefined
|
|
148
|
+
// 一致性分支(#1659):effort 档位有值且非 "none" ⇒ effort 赢(thinking on)。
|
|
149
|
+
// 百炼约束:enable_thinking:false 时 reasoning_effort 必须为 "none",互斥即 400。
|
|
150
|
+
// 压缩下行(context.mjs:203)显式 thinking:null 但继承 effort(v3 同源裁定)——
|
|
151
|
+
// 无此分支则发出 {enable_thinking:false, reasoning_effort:"medium"} ⇒ 400 ⇒ 压缩三连败。
|
|
152
|
+
if (provider.reasoningEffort && provider.reasoningEffort !== "none") return true
|
|
174
153
|
if (provider.thinking === null) return false
|
|
175
154
|
if (provider.reasoningEffort) return true
|
|
176
155
|
return undefined
|
|
177
156
|
}
|
|
178
157
|
|
|
158
|
+
// 加载期归一 / 清洗纯函数族已迁 `config-normalize.mjs`(§6.16.9 —— C1)——四个公共函数经本档
|
|
159
|
+
// re-export 保持既有导入面;其余四个(sanitizeProviderHeaders ∥ sanitizeConsultModels ∥ 两枚
|
|
160
|
+
// 一次性警告旗)只由 loadConfig 消费。
|
|
161
|
+
import {
|
|
162
|
+
sanitizeConsultModels, warnConsultModelsFiltered, sanitizeSubagentModel, sanitizeSubagentModels,
|
|
163
|
+
warnSubagentModelsFiltered, sanitizeProviderHeaders, normalizeProxy, normalizeTeamSection,
|
|
164
|
+
} from "./config-normalize.mjs"
|
|
165
|
+
export { sanitizeSubagentModel, sanitizeSubagentModels, normalizeProxy, normalizeTeamSection }
|
|
166
|
+
|
|
179
167
|
/** Module-level one-time warn dedupe for invalid providers[].context (PROVIDER.md §15 D-C1). */
|
|
180
168
|
const warnedContextProviders = new Set()
|
|
181
169
|
|
|
182
|
-
/** F-4 (IKCDMR) 软失败清洗(D-S1 范式——loadConfig 是 CLI 启动砖点):consultModels 非法
|
|
183
|
-
* 条目过滤不 throw——非数组 → []、形状非法/未知渠道丢弃、超 5 截断(keep 前 5)。
|
|
184
|
-
* 返回 { keep, dropped }——dropped 供一次性警告;merged.agent.consultModelsFiltered 挂载
|
|
185
|
-
* 供后续首帧引导消费(数据层——本批文件面无消费方——同 providerInvalidReason 载体先例)。 */
|
|
186
|
-
function sanitizeConsultModels(cm, providerNames) {
|
|
187
|
-
if (cm === undefined || cm === null) return { keep: [], dropped: [] }
|
|
188
|
-
if (!Array.isArray(cm)) {
|
|
189
|
-
return { keep: [], dropped: [`agent.consultModels must be an array of { provider, model } entries (got ${typeof cm})`] }
|
|
190
|
-
}
|
|
191
|
-
const names = new Set(providerNames)
|
|
192
|
-
const keep = []
|
|
193
|
-
const dropped = []
|
|
194
|
-
for (const entry of cm) {
|
|
195
|
-
if (keep.length >= 5) { dropped.push(`over the 5-entry cap — dropped ${JSON.stringify(entry)}`); continue }
|
|
196
|
-
if (!entry || typeof entry !== "object" || typeof entry.provider !== "string" || !entry.provider.trim()
|
|
197
|
-
|| typeof entry.model !== "string" || !entry.model.trim()) {
|
|
198
|
-
dropped.push(`invalid entry (expected { provider: string, model: string }) — got ${JSON.stringify(entry)}`)
|
|
199
|
-
continue
|
|
200
|
-
}
|
|
201
|
-
if (!names.has(entry.provider)) {
|
|
202
|
-
dropped.push(`entry "${entry.provider}:${entry.model}" references unknown provider "${entry.provider}" (available: ${[...names].join(", ") || "none"})`)
|
|
203
|
-
continue
|
|
204
|
-
}
|
|
205
|
-
keep.push(entry)
|
|
206
|
-
}
|
|
207
|
-
return { keep, dropped }
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/** F-4:过滤警告——进程级一次性(loadConfig 同进程可多次调用:TUI 启动 + reloadConfig)。 */
|
|
211
|
-
let warnedConsultModels = false
|
|
212
|
-
function warnConsultModelsFiltered(dropped, path) {
|
|
213
|
-
if (warnedConsultModels || dropped.length === 0) return
|
|
214
|
-
warnedConsultModels = true
|
|
215
|
-
console.warn(`[config] agent.consultModels: ${dropped.length} invalid entr${dropped.length === 1 ? "y ignored" : "ies ignored"} (filtered — startup continues; no crash):\n` +
|
|
216
|
-
dropped.map((d) => ` - ${d}`).join("\n") +
|
|
217
|
-
`\n Fix: clean agent.consultModels in ${path} or use /config → consult/escalate pool menu.`)
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/** #861(AGENT-LOOP-SUBAGENT.md §6.7.1):subagent 模型覆盖加载期清洗——单源纯函数(核 loadConfig ∥ VSC 端壳 raw 读点同消费);非法形态不 throw(`subagentModel` 非法 ⇒ null、`subagentModels` 非对象 ⇒ {}、值非法 ⇒ 剔除该键)。 */
|
|
221
|
-
export function sanitizeSubagentModel(v) {
|
|
222
|
-
const ok = typeof v === "string" && v.trim()
|
|
223
|
-
const dropped = ok || v == null ? [] : [`agent.subagentModel must be a non-empty string ("provider:model" | model) — got ${Array.isArray(v) ? "array" : typeof v} — ignored (inherits the parent provider)`]
|
|
224
|
-
return { value: ok ? v : null, dropped }
|
|
225
|
-
}
|
|
226
|
-
export function sanitizeSubagentModels(v) {
|
|
227
|
-
if (v == null) return { value: {}, dropped: [] }
|
|
228
|
-
if (typeof v !== "object" || Array.isArray(v)) return { value: {}, dropped: [`agent.subagentModels must be an object mapping role → model override — got ${Array.isArray(v) ? "array" : typeof v} — ignored`] }
|
|
229
|
-
const value = {}, dropped = []
|
|
230
|
-
for (const [role, val] of Object.entries(v)) {
|
|
231
|
-
if (typeof val === "string" && val.trim()) { value[role] = val; continue }
|
|
232
|
-
dropped.push(`agent.subagentModels.${role} must be a non-empty string ("provider:model" | model) — got ${Array.isArray(val) ? "array" : typeof val} — dropped`)
|
|
233
|
-
}
|
|
234
|
-
return { value, dropped }
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
let warnedSubagentModels = false // #861:清洗警告——进程级一次性(同 consultModels 先例)
|
|
238
|
-
function warnSubagentModelsFiltered(dropped, path) {
|
|
239
|
-
if (warnedSubagentModels || dropped.length === 0) return
|
|
240
|
-
warnedSubagentModels = true
|
|
241
|
-
console.warn(`[config] agent.subagentModel/subagentModels: ${dropped.length} invalid entr${dropped.length === 1 ? "y ignored" : "ies ignored"} (filtered — startup continues; no crash):\n` +
|
|
242
|
-
`${dropped.map((d) => ` - ${d}`).join("\n")}\n Fix: clean it in ${path} — "provider:model" | model.`)
|
|
243
|
-
}
|
|
244
|
-
|
|
245
170
|
/**
|
|
246
171
|
* Find provider by name in providers[].
|
|
247
172
|
* Throws if name is non-empty but not found — a typo in the caller-supplied name (e.g. `defaultModel`) silently falling to the first provider would use the wrong key on the wrong endpoint.
|
|
@@ -257,47 +182,13 @@ export function findProvider(providers, name) {
|
|
|
257
182
|
return providers[0] ?? { name: "default", baseURL: "" }
|
|
258
183
|
}
|
|
259
184
|
|
|
260
|
-
/** Normalize proxy config to { uri, web } or undefined (uri/url both accepted; invalid types dropped) */
|
|
261
|
-
export function normalizeProxy(proxy) {
|
|
262
|
-
if (typeof proxy === "string") return proxy ? { uri: proxy, web: true } : undefined
|
|
263
|
-
if (!proxy || typeof proxy !== "object" || Array.isArray(proxy)) return undefined
|
|
264
|
-
const uri = proxy.uri || proxy.url || ""
|
|
265
|
-
if (typeof uri !== "string" || !uri) return undefined
|
|
266
|
-
return { uri, web: proxy.web !== false }
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/** 团队登录段加载归一(B1 批——`docs/core/design/TEAM.md` §2.1 ∥ CONFIG.md §6.4):非对象 ⇒ null(软失败
|
|
270
|
-
* 不阻启动——沿 proxy 段先例);键级 = trim 非空字符串(非串 ∥ 空 ⇒ null);`member` 浅形 `{ username, name }`。
|
|
271
|
-
* **不入 `DEFAULTS`**(settings 工具面零涉——agent 不可写;`team.token` 经读面到达 = 敏感段词表遮罩)。 */
|
|
272
|
-
export function normalizeTeamSection(value) {
|
|
273
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) return null
|
|
274
|
-
const str = (v) => (typeof v === "string" && v.trim() !== "" ? v.trim() : null)
|
|
275
|
-
const raw = value.member
|
|
276
|
-
const member = raw !== null && typeof raw === "object" && !Array.isArray(raw) ? { username: str(raw.username), name: str(raw.name) } : null
|
|
277
|
-
return { server: str(value.server), member, label: str(value.label), token: str(value.token) }
|
|
278
|
-
}
|
|
279
|
-
|
|
280
185
|
/**
|
|
281
186
|
* Load configuration.
|
|
282
187
|
* No env-var overrides — config.json is the single source of truth
|
|
283
188
|
* (API keys, baseURL, defaultModel all come from the file).
|
|
189
|
+
* loadConfig 内联迁移核已迁 config-migrate.mjs(500 行硬限拆分——VSC 同构)——
|
|
190
|
+
* migrateLegacyModelFields 纯函数 + 本文件 import/写回编排(折中 C 见其头注释)。
|
|
284
191
|
*/
|
|
285
|
-
/** Keep only { header: "string value" } pairs from a provider's headers field — anything
|
|
286
|
-
* else (null, arrays, nested objects) is dropped so it can never reach a fetch call.
|
|
287
|
-
* Authorization is built-in and cannot be overridden from headers (core.mjs spreads first). */
|
|
288
|
-
function sanitizeProviderHeaders(p) {
|
|
289
|
-
if (p.headers == null || typeof p.headers !== "object" || Array.isArray(p.headers)) { delete p.headers; return p }
|
|
290
|
-
const clean = {}
|
|
291
|
-
for (const [k, v] of Object.entries(p.headers)) {
|
|
292
|
-
if (typeof v === "string" && k.toLowerCase() !== "authorization") clean[k] = v
|
|
293
|
-
}
|
|
294
|
-
if (Object.keys(clean).length > 0) p.headers = clean
|
|
295
|
-
else delete p.headers
|
|
296
|
-
return p
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/** loadConfig 内联迁移核已迁 config-migrate.mjs(500 行硬限拆分——VSC 同构)——
|
|
300
|
-
* migrateLegacyModelFields 纯函数 + 本文件 import/写回编排(折中 C 见其头注释)。 */
|
|
301
192
|
export function loadConfig() {
|
|
302
193
|
let config = {}
|
|
303
194
|
const path = cfgPath()
|
|
@@ -411,11 +302,18 @@ export function loadConfig() {
|
|
|
411
302
|
? null
|
|
412
303
|
: plan.reason ?? defaultModelReason(merged.providers, merged.defaultModel)
|
|
413
304
|
|
|
414
|
-
// Compaction threshold
|
|
305
|
+
// Compaction threshold 三档落值(CONTEXT-COMPACTION.md §6.17):数值在场 = 显式(原样,不猜来源);
|
|
306
|
+
// 不在场 = auto——派生值只进 compactThresholdResolved,永不写 agent.compactThreshold、不落盘。
|
|
415
307
|
const explicitThreshold = config.agent?.compactThreshold
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
308
|
+
if (explicitThreshold != null) {
|
|
309
|
+
merged.agent.compactThreshold = explicitThreshold
|
|
310
|
+
merged.agent.compactThresholdResolved = explicitThreshold
|
|
311
|
+
merged.agent.compactThresholdAuto = false
|
|
312
|
+
} else {
|
|
313
|
+
merged.agent.compactThreshold = undefined
|
|
314
|
+
merged.agent.compactThresholdResolved = resolveCompactThreshold(null, merged.provider).value
|
|
315
|
+
merged.agent.compactThresholdAuto = true
|
|
316
|
+
}
|
|
419
317
|
|
|
420
318
|
// Write back to merged for convenient access by upper layers
|
|
421
319
|
// fetch 超时可配置(2026-09-01:agent.fetchTimeoutMs——provider/core.mjs effectiveFetchTimeoutMs 消费)
|
package/declaration.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { existsSync, readdirSync, statSync } from "node:fs"
|
|
9
9
|
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path"
|
|
10
|
-
import { DEFAULT_MANIFEST, MANIFEST_REL, docRootPaths, manifestFilePath, owningProject, readManifest } from "./manifest.mjs"
|
|
10
|
+
import { DEFAULT_MANIFEST, MANIFEST_REL, docRootPaths, manifestFilePath, onManifestInit, owningProject, readManifest } from "./manifest.mjs"
|
|
11
11
|
import { isSkippedRelPath } from "./memory/file-walk.mjs" // 面⑦ 子目录扫描的跳过段谓词单源(零环:file-walk 不引本档)
|
|
12
12
|
import { logEvent } from "./log.mjs"
|
|
13
13
|
|
|
@@ -311,6 +311,10 @@ export function clearDeclarationCache() {
|
|
|
311
311
|
_scopeCache.clear()
|
|
312
312
|
}
|
|
313
313
|
|
|
314
|
+
// #1376:eng 建档路(`manifest.mjs` `resolveEngineeringManifest` init 支)成功 ⇒ 同清两缓存——
|
|
315
|
+
// 反向注册(本档已向依赖 manifest.mjs,其侧零出边保持——KD-M1-33);失败路径不广播、文件面零变。
|
|
316
|
+
onManifestInit(clearDeclarationCache)
|
|
317
|
+
|
|
314
318
|
/** Retired-carrier warning (existence check only — zero content parsing, zero fallback). */
|
|
315
319
|
function warnRetiredCarrier(root) {
|
|
316
320
|
console.warn(`[declaration] ${RETIRED_REL_PATH} is retired and no longer read — move codePaths / index.*Extensions / advisor.{docMap,standardsDoc} into PROJECT-MANIFEST.json`)
|
|
@@ -0,0 +1,142 @@
|
|
|
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 ⇒ **不判**,列报告面 = 终结)→ ⑤ 声明解析根相对(`anchors.roots` 逐根,按声明序)。
|
|
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 { anchorRootError } from "../manifest-schema.mjs"
|
|
19
|
+
import { fenceMark, isExecutableLine } from "./width.mjs"
|
|
20
|
+
|
|
21
|
+
/** 文档指针 token:`(目录段/)*名.md` + 可选行号坐标尾 + 可选节指示(`§` + 编号)。
|
|
22
|
+
* 左界守卫排除更长 token 的尾段;右界排除后续字母数字(`.md` 之后紧邻 `[\w]` ⇒ 非完整 token)。 */
|
|
23
|
+
const ANCHOR_RE = /(?<![A-Za-z0-9_.\-\\/@<>])((?:[A-Za-z0-9_.\-]+\/)*[A-Za-z0-9_.\-]+\.md)(?::\d+(?:-\d+)?)?(?:\s*§\s*(\d+(?:\.\d+)*))?(?![\w])/g
|
|
24
|
+
|
|
25
|
+
/** 约定文件名(**类名**——文档在描述「会读哪些文件」、黑名单名字、`files` 字段值、测试数据):不抽。
|
|
26
|
+
* 依据 = 这些名字在仓根 / 兄弟目录不一定存在,它们指的不是「某个仓内档」。 */
|
|
27
|
+
const FILENAME_CLASSES = new Set([
|
|
28
|
+
"agents.md", "claude.md", "skill.md", "project_rules.md",
|
|
29
|
+
"changelog.md", "agent.md", "advisor.md", "todo.md", "todo-archive.md",
|
|
30
|
+
])
|
|
31
|
+
|
|
32
|
+
/** 占位符词干(示例写法 / 反例样本——`X.md` / `file.md` / `docs/batches/x.md` 类):不抽。 */
|
|
33
|
+
const PLACEHOLDER_STEMS = new Set([
|
|
34
|
+
"x", "a", "b", "file", "out", "foo", "bar", "example", "sample", "name", "path",
|
|
35
|
+
])
|
|
36
|
+
|
|
37
|
+
/** 文件存在性(目录 / 读错 ⇒ false)。 */
|
|
38
|
+
function isFile(p) { try { return statSync(p).isFile() } catch { return false } }
|
|
39
|
+
|
|
40
|
+
/** 目录存在性(文件 / 读错 ⇒ false)。 */
|
|
41
|
+
function isDir(p) { try { return statSync(p).isDirectory() } catch { return false } }
|
|
42
|
+
|
|
43
|
+
/** 标题编号(标题文本去 `^#{1,6}\s` 后的首个 `\d+(?:\.\d+)*`);非标题行 ⇒ null。 */
|
|
44
|
+
function headingNumber(line) {
|
|
45
|
+
const m = /^#{1,6}\s+/.exec(line)
|
|
46
|
+
if (!m) return null
|
|
47
|
+
const n = /\d+(?:\.\d+)*/.exec(line.slice(m[0].length))
|
|
48
|
+
return n ? n[0] : null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 节在盘:目标档内任一标题行的编号 = 指针节号(字符串逐字等值——两处同归一形)。 */
|
|
52
|
+
function sectionOnDisk(path, num) {
|
|
53
|
+
let text = ""
|
|
54
|
+
try { text = readFileSync(path, "utf8") } catch { return false }
|
|
55
|
+
for (const l of text.split("\n")) if (headingNumber(l) === num) return true
|
|
56
|
+
return false
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 档解析(判据 ① 的解析序 ①–⑤):命中 ⇒ `{ ok, path }`;同名 ≥2 ⇒ `{ ambiguous, candidates }`;零命中 ⇒ `{ ok:false }`。
|
|
60
|
+
* 行号坐标尾(`:N`)不参与档解析——token 只取 `.md` 段(抽取面已剥离坐标尾与节指示)。
|
|
61
|
+
* `roots` = 声明解析根(已按值形 ∧ 根在盘过滤)——第 ⑤ 步,口径单源 = `DOC-DISCIPLINE.md` §3.20 判据 ①。 */
|
|
62
|
+
function resolveDoc(token, refFile, base, scanDirs, index, roots) {
|
|
63
|
+
const p1 = resolve(base, token)
|
|
64
|
+
if (isFile(p1)) return { ok: true, path: p1 } // ① 仓根相对
|
|
65
|
+
for (const d of scanDirs) { const p = resolve(base, d, token); if (isFile(p)) return { ok: true, path: p } } // ② 域根相对
|
|
66
|
+
const p3 = resolve(dirname(refFile), token)
|
|
67
|
+
if (isFile(p3)) return { ok: true, path: p3 } // ③ 引用方档所在目录相对
|
|
68
|
+
const cands = index.get(basename(token)) ?? [] // ④ 域内唯一 basename 索引
|
|
69
|
+
if (cands.length === 1) return { ok: true, path: cands[0] }
|
|
70
|
+
if (cands.length > 1) return { ambiguous: true, candidates: cands } // ④ 多义 ⇒ 终结,不进 ⑤
|
|
71
|
+
for (const r of roots) { const p = resolve(base, r, token); if (isFile(p)) return { ok: true, path: p } } // ⑤ 声明解析根相对
|
|
72
|
+
return { ok: false }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** 声明解析根逐元素归类(口径单源 = `DOC-DISCIPLINE.md` §3.20 判据 ① 解析序 ⑤):
|
|
76
|
+
* `usable` = 参与解析的根(声明序);`gaps` = 报告面(不作判)。缺键 ⇒ 本步零效(不产报告行)。 */
|
|
77
|
+
function classifyRoots(base, roots) {
|
|
78
|
+
const out = { usable: [], gaps: [] }
|
|
79
|
+
if (roots === undefined) return out
|
|
80
|
+
if (!Array.isArray(roots)) { out.gaps.push({ kind: "invalid", root: roots, reason: "非字符串数组" }); return out }
|
|
81
|
+
for (const r of roots) {
|
|
82
|
+
const why = anchorRootError(r)
|
|
83
|
+
if (why) { out.gaps.push({ kind: "invalid", root: r, reason: why }); continue }
|
|
84
|
+
if (!isDir(resolve(base, r))) { out.gaps.push({ kind: "missing", root: r }); continue }
|
|
85
|
+
out.usable.push(r)
|
|
86
|
+
}
|
|
87
|
+
return out
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 锚判据主行程:域内逐档逐行抽取 + 解析 + 节在盘判定。
|
|
92
|
+
* @param {string} root 项目根(判据参数已由调用方自 `checkConfig` 读出)
|
|
93
|
+
* @param {{scanDirs?:string[], files?:string[], checkFiles?:string[], roots?:string[]}} o `files` = 域内 `.md` 全集(单源 = `scanDomain`);
|
|
94
|
+
* `checkFiles` = **被判集**(gate 的范围收窄面——缺省 ⇒ `files`):索引仍按 `files` 建(解析基线不缩),
|
|
95
|
+
* 只把「抽哪几档的行」收窄——否则改过的档引用未改档会假报 `档缺`(假阳即机制失效);
|
|
96
|
+
* `roots` = 声明解析根原值(值形 ∥ 根在盘的门归本函数——报告面逐元素弃用 / 列缺位)。
|
|
97
|
+
* @returns {{hits:Array, reports:Array, rootGaps:Array}} hits = 命中(`档缺` / `节缺`);reports = 报告面(多义——不作判);
|
|
98
|
+
* rootGaps = 报告面(解析根弃用 ∥ 缺位——不作判)
|
|
99
|
+
*/
|
|
100
|
+
export function checkAnchors(root, { scanDirs = [], files = [], checkFiles, roots } = {}) {
|
|
101
|
+
const base = resolve(root)
|
|
102
|
+
const dirs = Array.isArray(scanDirs) ? scanDirs : []
|
|
103
|
+
const { usable, gaps } = classifyRoots(base, roots)
|
|
104
|
+
const index = new Map()
|
|
105
|
+
for (const f of files) {
|
|
106
|
+
const k = basename(f)
|
|
107
|
+
if (!index.has(k)) index.set(k, [])
|
|
108
|
+
index.get(k).push(f)
|
|
109
|
+
}
|
|
110
|
+
const hits = []
|
|
111
|
+
const reports = []
|
|
112
|
+
for (const f of checkFiles ?? files) {
|
|
113
|
+
let text = ""
|
|
114
|
+
try { text = readFileSync(f, "utf8") } catch { continue }
|
|
115
|
+
const re = new RegExp(ANCHOR_RE.source, "g") // 每档一份实例(零共享 lastIndex 状态)
|
|
116
|
+
let fence = null // 围栏标记(` ∥ ~);null = 不在块内
|
|
117
|
+
text.split("\n").forEach((l, i) => {
|
|
118
|
+
const mark = fenceMark(l)
|
|
119
|
+
if (mark !== null) { // 围栏块整块跳过:起于围栏行、止于同形围栏行(未闭合 ⇒ 至档尾免)
|
|
120
|
+
if (fence === null) fence = mark
|
|
121
|
+
else if (mark === fence) fence = null
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
if (fence !== null) return
|
|
125
|
+
if (isExecutableLine(l)) return // 可执行行整行跳过
|
|
126
|
+
let m
|
|
127
|
+
while ((m = re.exec(l)) !== null) {
|
|
128
|
+
const token = m[1]
|
|
129
|
+
const num = m[2] ?? null
|
|
130
|
+
const bn = basename(token).toLowerCase()
|
|
131
|
+
const stem = bn.replace(/\.md$/, "")
|
|
132
|
+
// 类名 ∥ 占位符两免:抽取面不把它们当指针(判据 = DOC-DISCIPLINE §3.20 ① 的「档 token」定义面)
|
|
133
|
+
if (FILENAME_CLASSES.has(bn) || stem.length <= 1 || PLACEHOLDER_STEMS.has(stem)) continue
|
|
134
|
+
const r = resolveDoc(token, f, base, dirs, index, usable)
|
|
135
|
+
if (r.ambiguous) { reports.push({ file: f, line: i + 1, token, candidates: r.candidates }); continue }
|
|
136
|
+
if (!r.ok) { hits.push({ file: f, line: i + 1, token, clazz: "档缺" }); continue }
|
|
137
|
+
if (num !== null && !sectionOnDisk(r.path, num)) hits.push({ file: f, line: i + 1, token, clazz: "节缺", section: num, target: r.path })
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
return { hits, reports, rootGaps: gaps }
|
|
142
|
+
}
|