@thincoder/core 0.10.9 → 0.10.11
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 +28 -2
- package/advisor/messages.mjs +4 -5
- package/advisor/project-context.mjs +69 -44
- package/agent/completion.mjs +4 -1
- package/agent/family-tools.mjs +12 -10
- package/agent/helpers.mjs +19 -11
- package/agent/light-round.mjs +1 -4
- package/agent/record-results.mjs +6 -1
- package/agent/run-start.mjs +1 -0
- package/agent/setup-reminders.mjs +1 -1
- package/agent/turn-loop.mjs +17 -13
- package/agent/unbooked-pen.mjs +62 -0
- package/agent-tools/advisor.mjs +15 -31
- package/agent-tools/audit-block.mjs +16 -62
- package/agent-tools/batch-gate.mjs +12 -21
- package/agent-tools/checkpoint.mjs +11 -6
- package/agent-tools/context.mjs +1 -1
- package/agent-tools/design-token.mjs +9 -2
- package/agent-tools/digest-budget.mjs +2 -0
- package/agent-tools/doc-check.mjs +17 -23
- package/agent-tools/manifest.mjs +45 -14
- package/agent-tools/review-facts.mjs +35 -81
- package/agent-tools/settings.mjs +65 -5
- package/agent-tools/spawn-gates.mjs +68 -31
- package/agent-tools/subagent-async.mjs +6 -3
- package/agent-tools/subagent-spawn.mjs +76 -80
- package/agent-tools/subagent.mjs +16 -16
- package/agent-tools.mjs +1 -1
- package/browser/launch.mjs +14 -3
- package/browser/queue.mjs +31 -3
- package/browser/session.mjs +4 -0
- package/config-defaults.mjs +8 -0
- package/config-presets.mjs +1 -1
- package/config.mjs +60 -11
- package/declaration.mjs +5 -12
- package/doc-check/anchors.mjs +53 -60
- package/doc-check/index.mjs +106 -140
- package/doc-check/paragraph.mjs +119 -0
- package/fts-text.mjs +4 -1
- package/i18n.mjs +5 -0
- package/ledger-cmd.mjs +17 -4
- package/ledger-db.mjs +14 -8
- package/ledger.mjs +1 -1
- package/manifest-discovery.mjs +19 -10
- package/manifest-schema.mjs +7 -35
- package/manifest.mjs +21 -9
- package/memory/code-search.mjs +5 -14
- package/memory/docs.mjs +5 -14
- package/memory/read-gate.mjs +26 -0
- package/model-context.mjs +98 -0
- package/model-specs.mjs +110 -37
- package/package.json +2 -2
- package/permission.mjs +3 -1
- package/prompts/advisor-design.md +104 -38
- package/prompts/advisor-round1.md +72 -24
- package/prompts/advisor-round2.md +62 -25
- package/prompts/advisor-round3.md +68 -24
- package/prompts/common.md +214 -58
- package/prompts/consult-base.md +10 -3
- package/prompts/discipline-engineering.md +202 -54
- package/prompts/discipline-normal.md +95 -44
- package/prompts/persona-coder.md +3 -1
- package/prompts/persona-eng-coder.md +29 -9
- package/prompts/persona-eng-designer.md +101 -35
- package/prompts/persona-engineering.md +320 -98
- package/prompts/persona-explore.md +1 -1
- package/prompts/persona-normal.md +16 -8
- package/provider/anthropic.mjs +127 -4
- package/provider/core.mjs +16 -6
- package/provider/google.mjs +8 -8
- package/provider/list-models.mjs +37 -5
- package/provider/normalize.mjs +2 -2
- package/provider/responses.mjs +7 -7
- package/provider/sse.mjs +6 -5
- package/proxy-transport.mjs +5 -3
- package/proxy.mjs +2 -1
- package/session-guard.mjs +7 -1
- package/session-lifecycle.mjs +23 -8
- package/session-segments.mjs +1 -0
- package/session-slot-claims.mjs +4 -0
- package/session-slot-write.mjs +6 -0
- package/session-slots-manifest.mjs +4 -0
- package/token-ttl.mjs +10 -0
- package/tool-docs/advisor.md +24 -5
- package/tool-docs/doc_check.md +44 -25
- package/tool-docs/ledger.md +2 -2
- package/tool-docs/manifest-read.md +8 -0
- package/tool-docs/manifest.md +9 -8
- package/tool-docs/subagent.md +41 -16
- package/tools/web-deepseek.mjs +26 -11
- package/doc-check/version.mjs +0 -84
- package/doc-check/width.mjs +0 -132
package/tools/web-deepseek.mjs
CHANGED
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
*
|
|
10
10
|
* The tools never fail because of this backend: every export returns a pure value
|
|
11
11
|
* (`{ status: "ok" | "empty" | "failed" | "unavailable" }`) and never throws.
|
|
12
|
-
* 401/402/403
|
|
13
|
-
*
|
|
12
|
+
* 401/402/403 trip a per-agent breaker for `BREAKER_TTL_MS` (10 min — auto-unseals, no restart);
|
|
13
|
+
* 404 does NOT trip (the target page is missing — not a credential failure, D-WB3); 408/429/5xx
|
|
14
|
+
* retry once (500 ms); everything else falls back to the caller's existing chain.
|
|
14
15
|
*/
|
|
15
16
|
import { URL } from "node:url"
|
|
16
17
|
import { truncate, isPrivateHost } from "./shared.mjs"
|
|
@@ -27,7 +28,8 @@ const DEEPSEEK_HOST = "api.deepseek.com"
|
|
|
27
28
|
const TIMEOUT_MS = 15_000
|
|
28
29
|
const RETRY_DELAY_MS = 500 // D-WB5: fixed single backoff (no Retry-After parsing)
|
|
29
30
|
const COST_PER_CALL = 0.00075 // $0.75 / 1000 successful requests — `count` does not change the price
|
|
30
|
-
const TRIP_STATUS = new Set([401, 402, 403
|
|
31
|
+
const TRIP_STATUS = new Set([401, 402, 403]) // 404 = 目标页面的状态,不是凭据失效(D-WB3)
|
|
32
|
+
const BREAKER_TTL_MS = 600_000 // 熔断限时 10 分钟——到期自动放行(改完 key 无需重启会话)
|
|
31
33
|
const RETRY_STATUS = (() => {
|
|
32
34
|
const codes = new Set([408, 429])
|
|
33
35
|
for (let code = 500; code <= 599; code++) codes.add(code)
|
|
@@ -35,11 +37,16 @@ const RETRY_STATUS = (() => {
|
|
|
35
37
|
})()
|
|
36
38
|
|
|
37
39
|
let _transport = proxyFetch
|
|
38
|
-
let
|
|
40
|
+
let _now = Date.now
|
|
41
|
+
let _breaker = new WeakMap() // agent → expiresAtMs(单域——search ∥ fetch 共享,D-WB3)
|
|
39
42
|
|
|
40
|
-
/** Test seam (CONFIG.md §6.2 ③ — paired): swap the transport; reset also clears the breaker. */
|
|
43
|
+
/** Test seam (CONFIG.md §6.2 ③ — paired): swap the transport ∥ the clock; reset also clears the breaker. */
|
|
41
44
|
export function _setDeepseekTransportForTest(fn) { _transport = typeof fn === "function" ? fn : proxyFetch }
|
|
42
|
-
export function
|
|
45
|
+
export function _setDeepseekClockForTest(fn) { _now = typeof fn === "function" ? fn : Date.now }
|
|
46
|
+
export function _resetDeepseekTransportForTest() { _transport = proxyFetch; _now = Date.now; _breaker = new WeakMap() }
|
|
47
|
+
|
|
48
|
+
/** 熔断名单只读面(用例断言 404 零命中——D-WB3)。 */
|
|
49
|
+
export const _TRIP_STATUS_FOR_TEST = Object.freeze([...TRIP_STATUS])
|
|
43
50
|
|
|
44
51
|
/** `local` skips the DeepSeek backend; anything else (missing ∥ misspelled ∥ non-string) = `auto` (fail-open). */
|
|
45
52
|
export function backendMode(cfg, key) {
|
|
@@ -66,11 +73,19 @@ function deepseekChannel(cfg) {
|
|
|
66
73
|
return null
|
|
67
74
|
}
|
|
68
75
|
|
|
69
|
-
|
|
76
|
+
/** 熔断未到期?到期 ⇒ 删条目并放行(判活读时钟——改完 key 不必重启会话)。 */
|
|
77
|
+
function tripped(agent) {
|
|
78
|
+
if (agent === null || typeof agent !== "object") return false
|
|
79
|
+
const expiresAt = _breaker.get(agent)
|
|
80
|
+
if (typeof expiresAt !== "number") return false
|
|
81
|
+
if (expiresAt > _now()) return true
|
|
82
|
+
_breaker.delete(agent)
|
|
83
|
+
return false
|
|
84
|
+
}
|
|
70
85
|
|
|
71
|
-
/** Break the backend for this agent identity (single domain — search ∥ fetch share it, D-WB3). */
|
|
86
|
+
/** Break the backend for this agent identity for `BREAKER_TTL_MS` (single domain — search ∥ fetch share it, D-WB3). */
|
|
72
87
|
function trip(agent) {
|
|
73
|
-
if (agent !== null && typeof agent === "object") _breaker.
|
|
88
|
+
if (agent !== null && typeof agent === "object") _breaker.set(agent, _now() + BREAKER_TTL_MS)
|
|
74
89
|
}
|
|
75
90
|
|
|
76
91
|
/** One POST. Returns `{ kind: "ok", data }` | `{ kind: "http", status }` | `{ kind: "net", timeout }` | `{ kind: "json" }`. */
|
|
@@ -95,7 +110,7 @@ async function postOnce(url, body, apiKey, proxyUri) {
|
|
|
95
110
|
} finally { clearTimeout(timer) }
|
|
96
111
|
}
|
|
97
112
|
|
|
98
|
-
/** One logical call: retry once (408/429/5xx), trip on 401/402/403
|
|
113
|
+
/** One logical call: retry once (408/429/5xx), trip on 401/402/403, otherwise fall back. */
|
|
99
114
|
async function request(url, body, apiKey, proxyUri, tool, agent) {
|
|
100
115
|
const started = Date.now()
|
|
101
116
|
let r = await postOnce(url, body, apiKey, proxyUri)
|
|
@@ -107,7 +122,7 @@ async function request(url, body, apiKey, proxyUri, tool, agent) {
|
|
|
107
122
|
if (r.kind === "http" && TRIP_STATUS.has(r.status) && agent !== null && typeof agent === "object") {
|
|
108
123
|
trip(agent)
|
|
109
124
|
// Tripping line only (transient fallbacks stay quiet — ⑤).
|
|
110
|
-
logEvent("web:deepseek", { tool, status: r.status, tripped: true, ms: Date.now() - started })
|
|
125
|
+
logEvent("web:deepseek", { tool, status: r.status, tripped: true, ttlMs: BREAKER_TTL_MS, ms: Date.now() - started })
|
|
111
126
|
}
|
|
112
127
|
const reason = r.kind === "http" ? `http ${r.status}`
|
|
113
128
|
: r.kind === "net" ? (r.timeout ? "timeout" : "network")
|
package/doc-check/version.mjs
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* doc-check/version.mjs — 引擎版本一致性的**单源**(纯函数 + 常量;两个消费面共用一个判据)。
|
|
3
|
-
*
|
|
4
|
-
* 权威规格 = `docs/core/design/DOC-DISCIPLINE.md` §3.20「引擎版本一致」(判据 / 比较 / 裁定 / 保守方向;
|
|
5
|
-
* 本档只实现,不重述口径)。消费面两处:工具面 `agent-tools/doc-check.mjs` 的 gate 分支(转口导出、
|
|
6
|
-
* 导出面不变)∥ 结构门 `agent-tools/batch-gate.mjs`(batch 两机械点)——两处不得各持一份清单。
|
|
7
|
-
*
|
|
8
|
-
* 问题形态:`thincoder-core/doc-check/**` 是 ESM,进程启动时加载即缓存;引擎档在启动后被改写 ⇒
|
|
9
|
-
* 本会话仍跑旧模块 ⇒ 假阳(旧误报消不掉)/ 假绿(引擎变严时旧版放过)。故 gate 在跑判据前先判版本。
|
|
10
|
-
*
|
|
11
|
-
* `ENGINE_FILES` 六档 = 判据面:三判据档 + 本档(版本函数单源,自身入清单 = fail-closed)+ 工具面 +
|
|
12
|
-
* 描述档。路径按本档 `import.meta.url` 解析到产品核根,**不随 target 变**。
|
|
13
|
-
*/
|
|
14
|
-
import { statSync } from "node:fs"
|
|
15
|
-
import { resolve } from "node:path"
|
|
16
|
-
import { fileURLToPath } from "node:url"
|
|
17
|
-
|
|
18
|
-
/** 产品核根(版本清单基线——本档住 `doc-check/`,上跳一级)。 */
|
|
19
|
-
const CORE_ROOT = fileURLToPath(new URL("..", import.meta.url))
|
|
20
|
-
|
|
21
|
-
/** 引擎六档 = 版本一致性的判据面(§3.20「引擎版本一致」· 判据)。 */
|
|
22
|
-
export const ENGINE_FILES = [
|
|
23
|
-
"doc-check/index.mjs",
|
|
24
|
-
"doc-check/anchors.mjs",
|
|
25
|
-
"doc-check/width.mjs",
|
|
26
|
-
"doc-check/version.mjs",
|
|
27
|
-
"agent-tools/doc-check.mjs",
|
|
28
|
-
"tool-docs/doc_check.md",
|
|
29
|
-
]
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* 引擎版本一致性(纯函数——时钟 ∥ stat 可注 ⇒ 可测,不碰产品档 mtime):
|
|
33
|
-
* 逐档 `mtimeMs` 与**本进程启动时刻**(`nowMs − uptimeSec × 1000`)比较——晚于启动 ⇒ 该档可能晚于模块加载 ⇒ stale。
|
|
34
|
-
* 不可 `stat` 的档同样入 stale(fail-closed,不静默忽略)。`mtime ≤ 启动` 一定不比缓存新;
|
|
35
|
-
* `mtime > 启动` 只是**可能**晚于加载 —— 取 fail-closed 方向(误判代价 = 一次会话重启)。
|
|
36
|
-
* @param {{nowMs?:number, uptimeSec?:number, stat?:Function}} [o] 注入缝(缺省 = 真时钟 / 真 `statSync`)
|
|
37
|
-
* @returns {{startMs:number, stale:{file:string, mtimeMs:number|null}[]}} `file` = 相对 `thincoder-core/` 的正斜杠路径
|
|
38
|
-
*/
|
|
39
|
-
export function staleEngineFiles({ nowMs = Date.now(), uptimeSec = process.uptime(), stat = statSync } = {}) {
|
|
40
|
-
const startMs = nowMs - uptimeSec * 1000
|
|
41
|
-
const stale = []
|
|
42
|
-
for (const file of ENGINE_FILES) {
|
|
43
|
-
let mtimeMs = null
|
|
44
|
-
try { mtimeMs = stat(resolve(CORE_ROOT, file)).mtimeMs } catch { stale.push({ file, mtimeMs: null }); continue }
|
|
45
|
-
if (typeof mtimeMs !== "number" || !Number.isFinite(mtimeMs)) { stale.push({ file, mtimeMs: null }); continue }
|
|
46
|
-
if (mtimeMs > startMs) stale.push({ file, mtimeMs })
|
|
47
|
-
}
|
|
48
|
-
return { startMs, stale }
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** stale 列表形(`<文件> mtime <ISO 8601>`,`;` 连列)——gate ERROR 行 ∥ report 注记行共用一处格式。 */
|
|
52
|
-
function staleParts(stale) {
|
|
53
|
-
const when = (t) => new Date(t).toISOString()
|
|
54
|
-
return stale.map((s) => `${s.file} mtime ${s.mtimeMs === null ? "不可读(stat 失败)" : when(s.mtimeMs)}`)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** 引擎版本 ERROR 报面(固定句形):多档 stale ⇒ 逐档 `<文件> mtime <时间>` 以 `;` 连列——**不产 PASS**。 */
|
|
58
|
-
export function gateEngineError({ startMs, stale }) {
|
|
59
|
-
const when = (t) => new Date(t).toISOString()
|
|
60
|
-
return [
|
|
61
|
-
`DOC-CHECK GATE: ERROR(引擎已更新(${staleParts(stale).join(";")} > 进程启动 ${when(startMs)}),请重启会话后复跑)`,
|
|
62
|
-
"原因:本会话进程加载的引擎模块早于盘上引擎档——旧引擎读数不可信(假阳:旧误报消不掉 ∥ 假绿:引擎变严时旧版放过)",
|
|
63
|
-
"处置:重启会话后复跑 gate(`report` 不裁定、不产 verdict(陈旧时只在首行前加注记))",
|
|
64
|
-
].join("\n")
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** 报告态引擎陈旧注记(纯函数):stale 空 ⇒ `null`;非空 ⇒ 一行注记(逐字单源 = `DOC-DISCIPLINE.md`
|
|
68
|
-
* §3.20「gate 裁定 · report 注记」——多档以 `;` 连列,与 `gateEngineError` 同形)。 */
|
|
69
|
-
export function reportEngineNote({ startMs, stale } = {}) {
|
|
70
|
-
if (!Array.isArray(stale) || stale.length === 0) return null
|
|
71
|
-
const when = (t) => new Date(t).toISOString()
|
|
72
|
-
return `[doc_check note] 引擎陈旧(${staleParts(stale).join(";")} > 进程启动 ${when(startMs)})——本次读数由会话启动时加载的旧引擎产出,不可作为交付判据;重启会话后复跑。`
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/** 报面 + 注记:有注记 ⇒ 注记行 + 原报面;无 ⇒ 原报面逐字返回(三不改:既有行 ∥ verdict ∥ PASS 语义)。 */
|
|
76
|
-
export function withEngineNote(report, version) {
|
|
77
|
-
const note = reportEngineNote(version ?? {})
|
|
78
|
-
return note ? `${note}\n${report}` : report
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/** 报面首行取形(唯一机判串 = 首行 `DOC-CHECK GATE:` 前缀——消费面共用一个取法,不各自 split)。 */
|
|
82
|
-
export function firstVerdictLine(report) {
|
|
83
|
-
return String(report ?? "").split("\n")[0] ?? ""
|
|
84
|
-
}
|
package/doc-check/width.mjs
DELETED
|
@@ -1,132 +0,0 @@
|
|
|
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
|
-
* `checkFiles`(可选)= **被判集**(gate 的范围收窄面);缺省 ⇒ `files`(全域,报告态遍历面零变)。 */
|
|
102
|
-
export function checkDocWidths(base, { lineWidth, scanDirs, exclude, exemptZones = [], files, checkFiles } = {}) {
|
|
103
|
-
const zones = (Array.isArray(exemptZones) ? exemptZones : []).filter((z) => typeof z === "string" && z !== "")
|
|
104
|
-
const hits = []
|
|
105
|
-
// 被判集优先(范围只收窄被判档);`files` 给定时直接用(调用方已扫同一域 ⇒ 域只走一遍);缺省自扫。
|
|
106
|
-
for (const f of checkFiles ?? files ?? scanDomain(base, { scanDirs, exclude })) {
|
|
107
|
-
let text = ""
|
|
108
|
-
try { text = readFileSync(f, "utf8") } catch { continue }
|
|
109
|
-
const stack = [] // 标题栈 —— { level, exempt };exempt = 自身 ∥ 祖先命中区带
|
|
110
|
-
let fence = null // 围栏标记(` ∥ ~);null = 不在块内
|
|
111
|
-
text.split("\n").forEach((l, i) => {
|
|
112
|
-
const mark = fenceMark(l)
|
|
113
|
-
if (mark !== null) {
|
|
114
|
-
if (fence === null) fence = mark
|
|
115
|
-
else if (mark === fence) fence = null
|
|
116
|
-
return // 围栏行自身免
|
|
117
|
-
}
|
|
118
|
-
if (fence !== null) return // 块内整块免(未闭合 ⇒ 至档尾免)
|
|
119
|
-
const hm = /^(#{1,6})\s+(.*)$/.exec(l)
|
|
120
|
-
if (hm) {
|
|
121
|
-
while (stack.length && stack[stack.length - 1].level >= hm[1].length) stack.pop()
|
|
122
|
-
const self = isZoneHead(hm[2].trim(), zones)
|
|
123
|
-
stack.push({ level: hm[1].length, exempt: self || (stack[stack.length - 1]?.exempt ?? false) })
|
|
124
|
-
return
|
|
125
|
-
}
|
|
126
|
-
const inZone = stack[stack.length - 1]?.exempt === true
|
|
127
|
-
const len = [...l].length
|
|
128
|
-
if (len > lineWidth && !isTableRow(l) && !isExecutableLine(l) && !isQuoteLine(l) && !inZone) hits.push({ file: f, line: i + 1, len })
|
|
129
|
-
})
|
|
130
|
-
}
|
|
131
|
-
return hits
|
|
132
|
-
}
|