sillyspec 3.26.10 → 3.26.13
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/.claude/skills/sillyspec-archive/SKILL.md +85 -85
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -114
- package/.claude/skills/sillyspec-commit/SKILL.md +109 -109
- package/.claude/skills/sillyspec-continue/SKILL.md +45 -45
- package/.claude/skills/sillyspec-doctor/SKILL.md +76 -76
- package/.claude/skills/sillyspec-execute/SKILL.md +200 -200
- package/.claude/skills/sillyspec-explore/SKILL.md +95 -95
- package/.claude/skills/sillyspec-knowledge/SKILL.md +269 -269
- package/.claude/skills/sillyspec-plan/SKILL.md +148 -148
- package/.claude/skills/sillyspec-quick/SKILL.md +115 -111
- package/.claude/skills/sillyspec-resume/SKILL.md +68 -68
- package/.claude/skills/sillyspec-scan/SKILL.md +79 -79
- package/.claude/skills/sillyspec-state/SKILL.md +64 -64
- package/.claude/skills/sillyspec-status/SKILL.md +54 -54
- package/.claude/skills/sillyspec-verify/SKILL.md +83 -83
- package/.claude/skills/sillyspec-workspace/SKILL.md +169 -169
- package/.gitattributes +2 -0
- package/.husky/pre-push +20 -20
- package/README.md +203 -201
- package/SKILL.md +91 -90
- package/package.json +39 -39
- package/packages/dashboard/dist/assets/index-Bq_Z2hne.js +7446 -7446
- package/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -1
- package/packages/dashboard/dist/index.html +16 -16
- package/packages/dashboard/package.json +25 -25
- package/packages/dashboard/server/executor.js +95 -86
- package/packages/dashboard/server/index.js +663 -601
- package/packages/dashboard/server/parser.js +526 -526
- package/src/change-risk-profile.js +254 -254
- package/src/classify-change.js +109 -109
- package/src/config-schema.js +313 -310
- package/src/contract-matrix.js +388 -379
- package/src/db.js +370 -350
- package/src/dispatch/backends/local-agent.js +155 -155
- package/src/dispatch/backends/sillyhub-mcp.js +245 -245
- package/src/dispatch/probe.js +246 -210
- package/src/dispatch/strategy.js +121 -121
- package/src/docs-check.js +188 -16
- package/src/docs-debt.js +6 -2
- package/src/docs-gate.js +1 -0
- package/src/doctor-diagnostics.js +898 -711
- package/src/endpoint-extractor.js +401 -401
- package/src/fs-atomic.js +71 -69
- package/src/git-helper.js +82 -79
- package/src/hooks/claude-pre-tool-use.cjs +125 -125
- package/src/hooks/worktree-guard.js +854 -848
- package/src/index.js +251 -20
- package/src/init.js +624 -623
- package/src/knowledge-match.js +130 -130
- package/src/local-detect.js +128 -120
- package/src/machine-interface.js +543 -543
- package/src/migrate.js +127 -127
- package/src/modules.js +7 -5
- package/src/progress/change-registry.js +297 -260
- package/src/progress/consistency-doctor.js +355 -355
- package/src/progress/shared.js +36 -36
- package/src/progress/stage-machine.js +589 -550
- package/src/progress/step-store.js +261 -261
- package/src/progress.js +1285 -1127
- package/src/quicklog.js +724 -693
- package/src/review-tier.js +77 -77
- package/src/run/command.js +1408 -1364
- package/src/run/complete-handlers.js +1450 -1399
- package/src/run/complete.js +1181 -920
- package/src/run/gates.js +878 -755
- package/src/run/prompt.js +765 -761
- package/src/run/quick-audit.js +155 -146
- package/src/run/shared.js +1348 -1215
- package/src/run/stage.js +527 -479
- package/src/scan-postcheck.js +371 -371
- package/src/setup.js +414 -416
- package/src/sillyhub-mcp/client.js +597 -529
- package/src/spec-dir-typo.js +3 -24
- package/src/spec-sync.js +358 -319
- package/src/stage-contract.js +871 -897
- package/src/stage-review.js +549 -549
- package/src/stages/archive.js +156 -155
- package/src/stages/brainstorm-auto.js +236 -236
- package/src/stages/brainstorm.js +597 -597
- package/src/stages/cmd-existence.js +145 -145
- package/src/stages/doctor.js +386 -371
- package/src/stages/execute.js +1014 -1003
- package/src/stages/explore.js +34 -34
- package/src/stages/index.js +29 -29
- package/src/stages/knowledge.js +498 -498
- package/src/stages/plan-postcheck.js +1323 -1302
- package/src/stages/plan.js +642 -623
- package/src/stages/quick.js +153 -151
- package/src/stages/scan.js +607 -607
- package/src/stages/status.js +64 -64
- package/src/stages/verify.js +264 -264
- package/src/sync.js +1325 -1135
- package/src/task-review.js +1204 -1019
- package/src/taskcard.js +178 -0
- package/src/verify-postcheck.js +1199 -1186
- package/src/workflow.js +600 -600
- package/src/worktree-apply.js +1237 -1136
- package/src/worktree-deps.js +453 -353
- package/src/worktree.js +1527 -1481
- package/templates/workflows/archive-impact.yaml +79 -79
- package/templates/workflows/scan-docs.yaml +132 -132
package/src/dispatch/strategy.js
CHANGED
|
@@ -1,121 +1,121 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 派发策略生成器(task-dispatcher 抽象层)— task-02
|
|
3
|
-
*
|
|
4
|
-
* 定位(D-007 + UB-2):dispatcher **不是 JS 执行体**。本文件 = **派发策略生成器**:
|
|
5
|
-
* 依据 probe 结果选择后端(sillyhub / local),并把对应后端的派发指令模板组合成可注入
|
|
6
|
-
* execute prompt 的「派发指令文本」,告诉 agent 用哪个后端、调什么 tool、传什么参数、
|
|
7
|
-
* 怎么轮询与回收。**实际 tool 调用(本机 Agent tool / SillyHub MCP tool)由 agent 执行,
|
|
8
|
-
* 本模块不 import client、不调任何 tool。**
|
|
9
|
-
*
|
|
10
|
-
* backend 选择(acceptance #1/#2,**由 probe.available 驱动,不由 isPathASupported 驱动**):
|
|
11
|
-
* - `probe.available === true` → backend = `'sillyhub'`(指令走 SillyHub MCP 模板)
|
|
12
|
-
* - 否则(false / undefined / probe 为 null)→ backend = `'local'`(与现状行为一致,零回归)
|
|
13
|
-
*
|
|
14
|
-
* 路径 A 降级(D-003@v2):`isPathASupported()` **不改 backend 标签**(仍由 probe 驱动),
|
|
15
|
-
* 而是影响 SillyHub 指令的**文本内容**:path A 未支持时(当前 stub 恒 false),在 SillyHub
|
|
16
|
-
* 主指令后附加降级提示段(PATH_A_DOWNGRADE_REASON + per-worker 回退 Local 说明),并始终追加
|
|
17
|
-
* Local 兜底指令全文,让 agent 据此 per-worker 回退本机 Agent tool,不硬试 MCP 路径。
|
|
18
|
-
*
|
|
19
|
-
* 铁律(R-09 / UB-7 / D-004):
|
|
20
|
-
* - 纯函数:除 import 的模板生成器外无副作用,易测(task-08 mock probe 验两分支)。
|
|
21
|
-
* - 指令必须明确**单后端**(backend 字段 + 文本只描述选定后端 + 兜底),避免 agent 误执行。
|
|
22
|
-
* - allowedPaths 不物化到 SillyHub(仅写入指令供 SillySpec 侧 assess/apply 校验)——
|
|
23
|
-
* allowedPaths 由各后端模板在指令文本里声明,本策略不额外处理。
|
|
24
|
-
* - 兼容 Win/Linux/macOS:无路径拼接 / 无平台分支,纯模板组合。
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
import { renderLocalInstruction } from './backends/local-agent.js';
|
|
28
|
-
import {
|
|
29
|
-
isPathASupported,
|
|
30
|
-
renderSillyHubInstruction,
|
|
31
|
-
PATH_A_DOWNGRADE_REASON,
|
|
32
|
-
} from './backends/sillyhub-mcp.js';
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* @typedef {import('./backends/local-agent.js').DispatchContract} DispatchContract
|
|
36
|
-
* @see design.md「接口定义」DispatchContract:brief / worktreePath / branch / allowedPaths /
|
|
37
|
-
* readOnly / modelHint / agentProfileHint / runId / missionId
|
|
38
|
-
*/
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* @typedef {object} DispatchInstruction
|
|
42
|
-
* @property {string} instruction - 注入 execute prompt 的派发指令文本(含选定后端 + 兜底)
|
|
43
|
-
* @property {'sillyhub' | 'local'} backend - 选定后端标签(由 probe.available 驱动)
|
|
44
|
-
*/
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* 生成派发指令(注入 execute prompt,agent 据此执行派发)。
|
|
48
|
-
*
|
|
49
|
-
* 决策(D-007 + task-02 acceptance):
|
|
50
|
-
* 1. 解析 probe:`available = !!(probe && probe.available)`;probe 为 null/undefined/缺
|
|
51
|
-
* available 均视为 unavailable → local(零回归)。
|
|
52
|
-
* 2. **backend = available ? 'sillyhub' : 'local'**(acceptance #1/#2,由 probe 驱动)。
|
|
53
|
-
* 3. contract 兜底为 `{}`(renderLocalInstruction / renderSillyHubInstruction 内部已对
|
|
54
|
-
* 缺字段回退醒目占位符,不会崩)。
|
|
55
|
-
* 4. backend==='sillyhub':
|
|
56
|
-
* - 主指令 = renderSillyHubInstruction(contract)(含轮询间隔 / kill lease 防双写 / 回收约定)
|
|
57
|
-
* - 若 !isPathASupported():追加降级提示段(PATH_A_DOWNGRADE_REASON + per-worker 回退 Local)
|
|
58
|
-
* - **始终追加 Local 兜底指令全文**(implementation #4,保证可回退)
|
|
59
|
-
* 5. backend==='local':主指令 = renderLocalInstruction(contract)(与现状行为一致)
|
|
60
|
-
*
|
|
61
|
-
* 注:renderLocalInstruction / renderSillyHubInstruction 内部已自行替换
|
|
62
|
-
* LOCAL_RECYCLE_RULE / SILLYHUB_RECYCLE_RULE 的 `<runId>` 占位符,本策略不重复替换;
|
|
63
|
-
* 仅当额外引用含占位符的常量时才需替换,当前实现未额外引用,故无需处理。
|
|
64
|
-
*
|
|
65
|
-
* @param {DispatchContract} [contract] - 派发契约(缺省视为空对象,模板内部回退占位符)
|
|
66
|
-
* @param {{ available?: boolean, reason?: string } | null} [probe] - probeSillyHub() 的结果
|
|
67
|
-
* @returns {DispatchInstruction} `{ instruction, backend }`
|
|
68
|
-
*/
|
|
69
|
-
export function renderDispatchInstruction(contract, probe) {
|
|
70
|
-
// 1. 解析 probe:null/undefined/缺 available 均视为 unavailable → local(零回归)
|
|
71
|
-
const available = !!(probe && probe.available);
|
|
72
|
-
|
|
73
|
-
// 2. backend 由 probe.available 驱动(acceptance #1/#2,不由 isPathASupported 驱动)
|
|
74
|
-
const backend = available ? 'sillyhub' : 'local';
|
|
75
|
-
|
|
76
|
-
// 3. contract 兜底(模板内部对缺字段已有醒目占位符处理,不会崩)
|
|
77
|
-
const c = contract || {};
|
|
78
|
-
|
|
79
|
-
if (backend === 'sillyhub') {
|
|
80
|
-
// SillyHub 主指令:含一 Wave 一 mission / dispatch_worker 参数 / 终态轮询间隔 / kill lease
|
|
81
|
-
// 防双写 / 回收约定(acceptance #3 轮询间隔与 kill lease 来自此模板)
|
|
82
|
-
const sillyHubInstruction = renderSillyHubInstruction(c);
|
|
83
|
-
|
|
84
|
-
// path A 未支持时的降级提示段(不改 backend 标签,仅追加文本,引导 agent per-worker 回退 Local)
|
|
85
|
-
const pathADowngrade = isPathASupported()
|
|
86
|
-
? ''
|
|
87
|
-
: [
|
|
88
|
-
'',
|
|
89
|
-
'---',
|
|
90
|
-
'',
|
|
91
|
-
'### 路径 A 降级提示(per-worker 回退 Local)',
|
|
92
|
-
'',
|
|
93
|
-
`${PATH_A_DOWNGRADE_REASON}`,
|
|
94
|
-
'',
|
|
95
|
-
'本次派发 backend 标记仍为 `sillyhub`(由 probe 可达驱动,acceptance 要求不改标签),',
|
|
96
|
-
'但实际执行将回退 Local:agent 调 dispatch_worker 传 worktree_path 会被拒/忽略,',
|
|
97
|
-
'请 per-worker 回退本机 Agent tool(见下方 Local 兜底),不要硬试 MCP 路径。',
|
|
98
|
-
'',
|
|
99
|
-
].join('\n');
|
|
100
|
-
|
|
101
|
-
// 始终追加 Local 兜底指令全文(implementation #4「始终附加 Local 兜底」):
|
|
102
|
-
// 保证 SillyHub 路径任一步异常(dispatch 被拒 / 轮询超时 kill 后 fallback / path A 未落地)
|
|
103
|
-
// 时 agent 可立即改用本机 Agent tool 重派,零回归。
|
|
104
|
-
const localFallback = [
|
|
105
|
-
'',
|
|
106
|
-
'---',
|
|
107
|
-
'',
|
|
108
|
-
'### 兜底:Local(本机 Agent tool)— SillyHub 路径异常时回退',
|
|
109
|
-
'',
|
|
110
|
-
'当上方 SillyHub MCP 路径任一步失败,立即改用本机 Agent tool 重派该 task。',
|
|
111
|
-
'Local 后端派发指令全文如下(workdir 强制 = contract.worktreePath):',
|
|
112
|
-
'',
|
|
113
|
-
renderLocalInstruction(c),
|
|
114
|
-
].join('\n');
|
|
115
|
-
|
|
116
|
-
return { instruction: sillyHubInstruction + pathADowngrade + localFallback, backend };
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// backend === 'local':与现状行为一致(acceptance #2),probe 本就 unavailable,无需额外降级提示
|
|
120
|
-
return { instruction: renderLocalInstruction(c), backend };
|
|
121
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* 派发策略生成器(task-dispatcher 抽象层)— task-02
|
|
3
|
+
*
|
|
4
|
+
* 定位(D-007 + UB-2):dispatcher **不是 JS 执行体**。本文件 = **派发策略生成器**:
|
|
5
|
+
* 依据 probe 结果选择后端(sillyhub / local),并把对应后端的派发指令模板组合成可注入
|
|
6
|
+
* execute prompt 的「派发指令文本」,告诉 agent 用哪个后端、调什么 tool、传什么参数、
|
|
7
|
+
* 怎么轮询与回收。**实际 tool 调用(本机 Agent tool / SillyHub MCP tool)由 agent 执行,
|
|
8
|
+
* 本模块不 import client、不调任何 tool。**
|
|
9
|
+
*
|
|
10
|
+
* backend 选择(acceptance #1/#2,**由 probe.available 驱动,不由 isPathASupported 驱动**):
|
|
11
|
+
* - `probe.available === true` → backend = `'sillyhub'`(指令走 SillyHub MCP 模板)
|
|
12
|
+
* - 否则(false / undefined / probe 为 null)→ backend = `'local'`(与现状行为一致,零回归)
|
|
13
|
+
*
|
|
14
|
+
* 路径 A 降级(D-003@v2):`isPathASupported()` **不改 backend 标签**(仍由 probe 驱动),
|
|
15
|
+
* 而是影响 SillyHub 指令的**文本内容**:path A 未支持时(当前 stub 恒 false),在 SillyHub
|
|
16
|
+
* 主指令后附加降级提示段(PATH_A_DOWNGRADE_REASON + per-worker 回退 Local 说明),并始终追加
|
|
17
|
+
* Local 兜底指令全文,让 agent 据此 per-worker 回退本机 Agent tool,不硬试 MCP 路径。
|
|
18
|
+
*
|
|
19
|
+
* 铁律(R-09 / UB-7 / D-004):
|
|
20
|
+
* - 纯函数:除 import 的模板生成器外无副作用,易测(task-08 mock probe 验两分支)。
|
|
21
|
+
* - 指令必须明确**单后端**(backend 字段 + 文本只描述选定后端 + 兜底),避免 agent 误执行。
|
|
22
|
+
* - allowedPaths 不物化到 SillyHub(仅写入指令供 SillySpec 侧 assess/apply 校验)——
|
|
23
|
+
* allowedPaths 由各后端模板在指令文本里声明,本策略不额外处理。
|
|
24
|
+
* - 兼容 Win/Linux/macOS:无路径拼接 / 无平台分支,纯模板组合。
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { renderLocalInstruction } from './backends/local-agent.js';
|
|
28
|
+
import {
|
|
29
|
+
isPathASupported,
|
|
30
|
+
renderSillyHubInstruction,
|
|
31
|
+
PATH_A_DOWNGRADE_REASON,
|
|
32
|
+
} from './backends/sillyhub-mcp.js';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @typedef {import('./backends/local-agent.js').DispatchContract} DispatchContract
|
|
36
|
+
* @see design.md「接口定义」DispatchContract:brief / worktreePath / branch / allowedPaths /
|
|
37
|
+
* readOnly / modelHint / agentProfileHint / runId / missionId
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @typedef {object} DispatchInstruction
|
|
42
|
+
* @property {string} instruction - 注入 execute prompt 的派发指令文本(含选定后端 + 兜底)
|
|
43
|
+
* @property {'sillyhub' | 'local'} backend - 选定后端标签(由 probe.available 驱动)
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 生成派发指令(注入 execute prompt,agent 据此执行派发)。
|
|
48
|
+
*
|
|
49
|
+
* 决策(D-007 + task-02 acceptance):
|
|
50
|
+
* 1. 解析 probe:`available = !!(probe && probe.available)`;probe 为 null/undefined/缺
|
|
51
|
+
* available 均视为 unavailable → local(零回归)。
|
|
52
|
+
* 2. **backend = available ? 'sillyhub' : 'local'**(acceptance #1/#2,由 probe 驱动)。
|
|
53
|
+
* 3. contract 兜底为 `{}`(renderLocalInstruction / renderSillyHubInstruction 内部已对
|
|
54
|
+
* 缺字段回退醒目占位符,不会崩)。
|
|
55
|
+
* 4. backend==='sillyhub':
|
|
56
|
+
* - 主指令 = renderSillyHubInstruction(contract)(含轮询间隔 / kill lease 防双写 / 回收约定)
|
|
57
|
+
* - 若 !isPathASupported():追加降级提示段(PATH_A_DOWNGRADE_REASON + per-worker 回退 Local)
|
|
58
|
+
* - **始终追加 Local 兜底指令全文**(implementation #4,保证可回退)
|
|
59
|
+
* 5. backend==='local':主指令 = renderLocalInstruction(contract)(与现状行为一致)
|
|
60
|
+
*
|
|
61
|
+
* 注:renderLocalInstruction / renderSillyHubInstruction 内部已自行替换
|
|
62
|
+
* LOCAL_RECYCLE_RULE / SILLYHUB_RECYCLE_RULE 的 `<runId>` 占位符,本策略不重复替换;
|
|
63
|
+
* 仅当额外引用含占位符的常量时才需替换,当前实现未额外引用,故无需处理。
|
|
64
|
+
*
|
|
65
|
+
* @param {DispatchContract} [contract] - 派发契约(缺省视为空对象,模板内部回退占位符)
|
|
66
|
+
* @param {{ available?: boolean, reason?: string } | null} [probe] - probeSillyHub() 的结果
|
|
67
|
+
* @returns {DispatchInstruction} `{ instruction, backend }`
|
|
68
|
+
*/
|
|
69
|
+
export function renderDispatchInstruction(contract, probe) {
|
|
70
|
+
// 1. 解析 probe:null/undefined/缺 available 均视为 unavailable → local(零回归)
|
|
71
|
+
const available = !!(probe && probe.available);
|
|
72
|
+
|
|
73
|
+
// 2. backend 由 probe.available 驱动(acceptance #1/#2,不由 isPathASupported 驱动)
|
|
74
|
+
const backend = available ? 'sillyhub' : 'local';
|
|
75
|
+
|
|
76
|
+
// 3. contract 兜底(模板内部对缺字段已有醒目占位符处理,不会崩)
|
|
77
|
+
const c = contract || {};
|
|
78
|
+
|
|
79
|
+
if (backend === 'sillyhub') {
|
|
80
|
+
// SillyHub 主指令:含一 Wave 一 mission / dispatch_worker 参数 / 终态轮询间隔 / kill lease
|
|
81
|
+
// 防双写 / 回收约定(acceptance #3 轮询间隔与 kill lease 来自此模板)
|
|
82
|
+
const sillyHubInstruction = renderSillyHubInstruction(c);
|
|
83
|
+
|
|
84
|
+
// path A 未支持时的降级提示段(不改 backend 标签,仅追加文本,引导 agent per-worker 回退 Local)
|
|
85
|
+
const pathADowngrade = isPathASupported()
|
|
86
|
+
? ''
|
|
87
|
+
: [
|
|
88
|
+
'',
|
|
89
|
+
'---',
|
|
90
|
+
'',
|
|
91
|
+
'### 路径 A 降级提示(per-worker 回退 Local)',
|
|
92
|
+
'',
|
|
93
|
+
`${PATH_A_DOWNGRADE_REASON}`,
|
|
94
|
+
'',
|
|
95
|
+
'本次派发 backend 标记仍为 `sillyhub`(由 probe 可达驱动,acceptance 要求不改标签),',
|
|
96
|
+
'但实际执行将回退 Local:agent 调 dispatch_worker 传 worktree_path 会被拒/忽略,',
|
|
97
|
+
'请 per-worker 回退本机 Agent tool(见下方 Local 兜底),不要硬试 MCP 路径。',
|
|
98
|
+
'',
|
|
99
|
+
].join('\n');
|
|
100
|
+
|
|
101
|
+
// 始终追加 Local 兜底指令全文(implementation #4「始终附加 Local 兜底」):
|
|
102
|
+
// 保证 SillyHub 路径任一步异常(dispatch 被拒 / 轮询超时 kill 后 fallback / path A 未落地)
|
|
103
|
+
// 时 agent 可立即改用本机 Agent tool 重派,零回归。
|
|
104
|
+
const localFallback = [
|
|
105
|
+
'',
|
|
106
|
+
'---',
|
|
107
|
+
'',
|
|
108
|
+
'### 兜底:Local(本机 Agent tool)— SillyHub 路径异常时回退',
|
|
109
|
+
'',
|
|
110
|
+
'当上方 SillyHub MCP 路径任一步失败,立即改用本机 Agent tool 重派该 task。',
|
|
111
|
+
'Local 后端派发指令全文如下(workdir 强制 = contract.worktreePath):',
|
|
112
|
+
'',
|
|
113
|
+
renderLocalInstruction(c),
|
|
114
|
+
].join('\n');
|
|
115
|
+
|
|
116
|
+
return { instruction: sillyHubInstruction + pathADowngrade + localFallback, backend };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// backend === 'local':与现状行为一致(acceptance #2),probe 本就 unavailable,无需额外降级提示
|
|
120
|
+
return { instruction: renderLocalInstruction(c), backend };
|
|
121
|
+
}
|
package/src/docs-check.js
CHANGED
|
@@ -9,15 +9,21 @@
|
|
|
9
9
|
*
|
|
10
10
|
* 纯函数(collectDocRefs/looksLikeCodeSymbol/validateRefLines/extractExpectedTokensFromLine)
|
|
11
11
|
* 无 fs 依赖可单测;resolveCandidates/walkGlob/runDocsCheck 是 IO 面。
|
|
12
|
-
*
|
|
12
|
+
* 校验链路只读(不修改任何被校验文件);applyFixes 是唯一写回面(--fix 显式触发,design §9:
|
|
13
|
+
* 文档非多进程竞争的运行时文件,普通 writeFileSync);纯 Node 内置模块零依赖(D-008:glob 手写 walker,
|
|
13
14
|
* 仅「目录递归」「目录单层」「字面路径」三形态);Windows 路径归一化;兼容 CRLF/LF。
|
|
14
15
|
*/
|
|
15
|
-
import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs'
|
|
16
|
+
import { readFileSync, writeFileSync, existsSync, readdirSync, statSync } from 'node:fs'
|
|
16
17
|
import { join } from 'node:path'
|
|
17
18
|
import jsYaml from 'js-yaml'
|
|
18
19
|
|
|
19
|
-
/** 提取 file.js:line / file.js:start-end 引用(.js/.mjs,反引号包裹与裸文本均命中,全文扫描 D-006
|
|
20
|
-
|
|
20
|
+
/** 提取 file.js:line / file.js:start-end 引用(.js/.mjs,反引号包裹与裸文本均命中,全文扫描 D-006)。
|
|
21
|
+
* repo:// 前缀(2026-08-20):跨仓引用显式标记——`repo://<仓库名>/<路径>.js:行`。
|
|
22
|
+
* 不同设备兄弟仓库位置不同,未配映射时默认跳过本地校验(防跨设备误报);
|
|
23
|
+
* 在本机 .sillyspec/local.yaml 的 docs-check.cross_repo_roots 配
|
|
24
|
+
* `<仓库名>: <本机绝对路径>` 后,走与本地引用完全相同的层1(行号边界)+ 层2(关键词窗口)校验。
|
|
25
|
+
* 正则组:1=仓库名(可选),2=文件,3=start,4=end。 */
|
|
26
|
+
const REF_RE = /(?:repo:\/\/([A-Za-z0-9_.\-]+)\/)?([A-Za-z0-9_.\-\/]+\.(?:js|mjs)):(\d+)(?:-(\d+))?/g
|
|
21
27
|
|
|
22
28
|
/** 缺省扫描范围:docs/ + .sillyspec/docs/(scan/modules 产物同是文档,失效即该暴露;2026-08-16 用户裁决改缺省,见 doc-consistency-debt.md §八) */
|
|
23
29
|
const DEFAULT_DOC_PATHS = ['docs/**/*.md', '.sillyspec/docs/**/*.md']
|
|
@@ -25,10 +31,10 @@ const DEFAULT_DOC_PATHS = ['docs/**/*.md', '.sillyspec/docs/**/*.md']
|
|
|
25
31
|
/**
|
|
26
32
|
* 读 local.yaml 的 docs-check 段(best-effort,绝不抛;缺文件/无段 → 全缺省)。
|
|
27
33
|
* @param {string} projectRoot 源码仓根(local.yaml 在 <root>/.sillyspec/local.yaml)
|
|
28
|
-
* @returns {{ paths?: string[], skip: string[], keywordAssert: boolean }}
|
|
34
|
+
* @returns {{ paths?: string[], skip: string[], keywordAssert: boolean, crossRepoRoots: Record<string, string> }}
|
|
29
35
|
*/
|
|
30
36
|
export function readDocsCheckConfig(projectRoot) {
|
|
31
|
-
const fallback = { paths: null, skip: [], keywordAssert: true }
|
|
37
|
+
const fallback = { paths: null, skip: [], keywordAssert: true, crossRepoRoots: {} }
|
|
32
38
|
try {
|
|
33
39
|
const p = join(projectRoot, '.sillyspec', 'local.yaml')
|
|
34
40
|
if (!existsSync(p)) return fallback
|
|
@@ -36,10 +42,17 @@ export function readDocsCheckConfig(projectRoot) {
|
|
|
36
42
|
if (!doc || typeof doc !== 'object') return fallback
|
|
37
43
|
const dc = doc['docs-check']
|
|
38
44
|
if (!dc || typeof dc !== 'object') return fallback
|
|
45
|
+
const crr = dc['cross_repo_roots']
|
|
39
46
|
return {
|
|
40
47
|
paths: Array.isArray(dc.paths) ? dc.paths.filter(s => typeof s === 'string') : null,
|
|
41
48
|
skip: Array.isArray(dc.skip) ? dc.skip.filter(s => typeof s === 'string') : [],
|
|
42
49
|
keywordAssert: typeof dc.keywordAssert === 'boolean' ? dc.keywordAssert : true,
|
|
50
|
+
// repo://<name> → 本机仓库根(每台设备各自配;local.yaml 是 gitignored,绝对路径不入库)
|
|
51
|
+
crossRepoRoots: crr && typeof crr === 'object' && !Array.isArray(crr)
|
|
52
|
+
? Object.fromEntries(
|
|
53
|
+
Object.entries(crr).filter(([, v]) => typeof v === 'string' && v.trim() !== ''),
|
|
54
|
+
)
|
|
55
|
+
: {},
|
|
43
56
|
}
|
|
44
57
|
} catch {
|
|
45
58
|
return fallback
|
|
@@ -62,9 +75,10 @@ export function collectDocRefs(md) {
|
|
|
62
75
|
while ((m = re.exec(md)) !== null) {
|
|
63
76
|
refs.push({
|
|
64
77
|
ref: m[0],
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
78
|
+
repo: m[1] !== undefined ? m[1] : null,
|
|
79
|
+
file: m[2],
|
|
80
|
+
start: parseInt(m[3], 10),
|
|
81
|
+
end: m[4] !== undefined ? parseInt(m[4], 10) : parseInt(m[3], 10),
|
|
68
82
|
docLine: md.slice(0, m.index).split(/\r?\n/).length,
|
|
69
83
|
})
|
|
70
84
|
}
|
|
@@ -267,16 +281,20 @@ function relDisplay(absPath, projectRoot) {
|
|
|
267
281
|
|
|
268
282
|
/**
|
|
269
283
|
* IO 入口:跑一次完整校验。
|
|
270
|
-
* @param {{ projectRoot: string, docs?: string[], paths?: string[], skip?: string[], keywordAssert?: boolean }} opts
|
|
284
|
+
* @param {{ projectRoot: string, docs?: string[], paths?: string[], skip?: string[], keywordAssert?: boolean, crossRepoRoots?: Record<string, string> }} opts
|
|
271
285
|
* projectRoot 源码仓根(候选解析与 glob 的锚点,平台模式也传源码仓根)
|
|
272
286
|
* docs 显式文档相对路径列表(优先于 paths glob 展开结果)
|
|
273
|
-
*
|
|
287
|
+
* crossRepoRoots repo://<仓库名> → 本机仓库根映射(未映射的跨仓引用跳过不计失效)
|
|
288
|
+
* @returns {{ ok: boolean, total: number, invalid: Array<{doc, docLine, ref, reason, suggest?: number[], fix: {fixable: boolean, newLine?: number, reason: string}}>, warnings: string[], kwChecked: number, crossRepoSkipped: number }}
|
|
274
289
|
* @throws {DocsCheckConfigError} glob 形态不支持
|
|
275
290
|
*/
|
|
276
291
|
export function runDocsCheck(opts) {
|
|
277
292
|
// paths/docs 显式传 null(readDocsCheckConfig 无 local.yaml 段时的回退值)须落回缺省 glob,
|
|
278
293
|
// 解构默认值只挡 undefined 不挡 null——docs-check 无配置裸跑曾因此 null.flatMap 崩溃。
|
|
279
|
-
const {
|
|
294
|
+
const {
|
|
295
|
+
projectRoot, docs = null, paths: rawPaths = DEFAULT_DOC_PATHS, skip = [],
|
|
296
|
+
keywordAssert = true, crossRepoRoots = {},
|
|
297
|
+
} = opts || {}
|
|
280
298
|
const paths = Array.isArray(rawPaths) && rawPaths.length > 0 ? rawPaths : DEFAULT_DOC_PATHS
|
|
281
299
|
const warnings = []
|
|
282
300
|
if (keywordAssert === false) warnings.push('关键词断言已关闭(keywordAssert=false),仅做存在性校验')
|
|
@@ -287,11 +305,16 @@ export function runDocsCheck(opts) {
|
|
|
287
305
|
const invalid = []
|
|
288
306
|
let total = 0
|
|
289
307
|
let kwChecked = 0
|
|
308
|
+
// repo:// 跨仓引用:未配映射被跳过的数量(不计入 total/invalid,跨设备零误报)
|
|
309
|
+
let crossRepoSkipped = 0
|
|
290
310
|
|
|
291
311
|
for (const docRel of docFiles) {
|
|
292
312
|
const docAbs = join(projectRoot, docRel)
|
|
293
313
|
if (!existsSync(docAbs)) {
|
|
294
|
-
invalid.push({
|
|
314
|
+
invalid.push({
|
|
315
|
+
doc: docRel, docLine: 0, ref: '', reason: '文档不存在',
|
|
316
|
+
fix: { fixable: false, reason: '文档不存在,无法自动定位' },
|
|
317
|
+
})
|
|
295
318
|
continue
|
|
296
319
|
}
|
|
297
320
|
const md = readFileSync(docAbs, 'utf8')
|
|
@@ -299,12 +322,37 @@ export function runDocsCheck(opts) {
|
|
|
299
322
|
const mdLines = md.split(/\r?\n/)
|
|
300
323
|
|
|
301
324
|
for (const r of refs) {
|
|
325
|
+
// repo:// 跨仓引用:未配映射 → 跳过(不同设备仓库位置不同,不按本仓解析误报);
|
|
326
|
+
// 配了映射 → 以映射根为唯一候选,走与本地引用相同的层1+层2 校验。
|
|
327
|
+
let candidates
|
|
328
|
+
if (r.repo) {
|
|
329
|
+
const repoRoot = crossRepoRoots && typeof crossRepoRoots[r.repo] === 'string'
|
|
330
|
+
? crossRepoRoots[r.repo]
|
|
331
|
+
: null
|
|
332
|
+
if (!repoRoot) {
|
|
333
|
+
crossRepoSkipped++
|
|
334
|
+
continue
|
|
335
|
+
}
|
|
336
|
+
const abs = join(repoRoot, r.file)
|
|
337
|
+
if (!existsSync(abs)) {
|
|
338
|
+
total++
|
|
339
|
+
invalid.push({
|
|
340
|
+
doc: docRel, docLine: r.docLine, ref: r.ref,
|
|
341
|
+
reason: `跨仓 repo://${r.repo} → 文件不存在(cross_repo_roots 映射根:${repoRoot})`,
|
|
342
|
+
fix: { fixable: false, reason: '跨仓引用无法自动定位' },
|
|
343
|
+
})
|
|
344
|
+
continue
|
|
345
|
+
}
|
|
346
|
+
candidates = [abs]
|
|
347
|
+
} else {
|
|
348
|
+
candidates = resolveCandidates(projectRoot, r.file)
|
|
349
|
+
}
|
|
302
350
|
total++
|
|
303
|
-
const candidates = resolveCandidates(projectRoot, r.file)
|
|
304
351
|
if (candidates.length === 0) {
|
|
305
352
|
invalid.push({
|
|
306
353
|
doc: docRel, docLine: r.docLine, ref: r.ref,
|
|
307
354
|
reason: '文件不存在(含 / 按仓库根解析;裸文件名在 src/ 递归)',
|
|
355
|
+
fix: { fixable: false, reason: '文件不存在,无法自动定位' },
|
|
308
356
|
})
|
|
309
357
|
continue
|
|
310
358
|
}
|
|
@@ -333,17 +381,21 @@ export function runDocsCheck(opts) {
|
|
|
333
381
|
}
|
|
334
382
|
if (tokens.length > 0) kwChecked++
|
|
335
383
|
if (!passedAny) {
|
|
384
|
+
// 跨仓校验失败的 reason 加 repo:// 前缀,用户一眼区分「改外部仓引用」还是「改本仓引用」
|
|
385
|
+
const repoPrefix = r.repo ? `跨仓 repo://${r.repo} → ` : ''
|
|
336
386
|
invalid.push({
|
|
337
387
|
doc: docRel, docLine: r.docLine, ref: r.ref,
|
|
338
|
-
reason: candidateFails.length > 1 ? `多候选全失败 → ${candidateFails.join(' | ')}` : candidateFails[0],
|
|
388
|
+
reason: repoPrefix + (candidateFails.length > 1 ? `多候选全失败 → ${candidateFails.join(' | ')}` : candidateFails[0]),
|
|
339
389
|
// 建议行号(--suggest):token 在首个候选文件的全量命中行,供人工确认改锚——不自动改文件
|
|
340
390
|
suggest: suggestLines(candidates, tokens),
|
|
391
|
+
// 修复分类(--fix 判定依据):全量候选 token 命中统计,唯一命中才 fixable(D-006 保守默认)
|
|
392
|
+
fix: classifyFix(candidates, tokens),
|
|
341
393
|
})
|
|
342
394
|
}
|
|
343
395
|
}
|
|
344
396
|
}
|
|
345
397
|
|
|
346
|
-
return { ok: invalid.length === 0, total, invalid, warnings, kwChecked }
|
|
398
|
+
return { ok: invalid.length === 0, total, invalid, warnings, kwChecked, crossRepoSkipped }
|
|
347
399
|
}
|
|
348
400
|
|
|
349
401
|
/**
|
|
@@ -360,3 +412,123 @@ function suggestLines(candidates, tokens) {
|
|
|
360
412
|
return out.slice(0, 8)
|
|
361
413
|
} catch { return [] }
|
|
362
414
|
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* 修复分类(--fix 判定依据):对 resolveCandidates 全量候选跑 token 命中统计,跨候选合并行号后——
|
|
418
|
+
* 唯一命中 → fixable=true + newLine(自动重锚唯一解);无 token / 零命中 / 多命中 → needs-manual。
|
|
419
|
+
* 与 suggestLines 的差异:suggest 只查首个候选(candidates[0]),本函数查全量候选(design §12 自审存疑 +
|
|
420
|
+
* R-01——多候选同名文件场景下防止定位到另一文件的同名符号)。合并口径按「去重后的行号集合」:
|
|
421
|
+
* 重锚写回的目标是行号,多个候选命中同一行号仍是唯一改写目标。
|
|
422
|
+
* 纯增量分类,不参与层1/层2 判定(D-004 兼容红线)。
|
|
423
|
+
* @param {string[]} candidates resolveCandidates 返回的全量候选绝对路径
|
|
424
|
+
* @param {string[]} tokens 层2 提取的期望 token(keywordAssert=false 时为空数组)
|
|
425
|
+
* @returns {{ fixable: boolean, newLine?: number, reason: string }}
|
|
426
|
+
*/
|
|
427
|
+
function classifyFix(candidates, tokens) {
|
|
428
|
+
if (tokens.length === 0) {
|
|
429
|
+
return { fixable: false, reason: '无 token 符号线索(纯位置引用或关键词断言关闭),无法自动定位' }
|
|
430
|
+
}
|
|
431
|
+
const hitLines = new Set()
|
|
432
|
+
for (const candAbs of candidates) {
|
|
433
|
+
const lines = readLines(candAbs)
|
|
434
|
+
if (lines === null) continue
|
|
435
|
+
lines.forEach((l, i) => { if (tokens.some((t) => l.includes(t))) hitLines.add(i + 1) })
|
|
436
|
+
}
|
|
437
|
+
if (hitLines.size === 0) {
|
|
438
|
+
return { fixable: false, reason: 'token 在全量候选文件零命中,无法自动定位' }
|
|
439
|
+
}
|
|
440
|
+
if (hitLines.size === 1) {
|
|
441
|
+
return { fixable: true, newLine: [...hitLines][0], reason: 'token 在全量候选唯一命中,可自动重锚' }
|
|
442
|
+
}
|
|
443
|
+
const sorted = [...hitLines].sort((a, b) => a - b)
|
|
444
|
+
const shown = sorted.slice(0, 8).join('、') + (sorted.length > 8 ? '…' : '')
|
|
445
|
+
return { fixable: false, reason: `token 多处命中(候选行号:${shown}),歧义需人工确认(D-006 保守默认)` }
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* 将可确定性修复的失效引用写回文档(--fix 消费面,design §7 接口逐字)。
|
|
450
|
+
* 纯机械定点替换:按 docLine + ref 串在该行内的字符偏移替换为 newRef,不解析行号语义、
|
|
451
|
+
* 不改引用文件名与 token(D-003:ref/newRef 一致性由调用方保证);候选定位全部依赖调用方
|
|
452
|
+
* 传入(不读 local.yaml 不解析配置)。范围引用(src/a.js:3-8)newRef 同样由调用方给全串。
|
|
453
|
+
* 同一行多个 fix 按行内偏移从后往前(降序)替换,防前序替换挤偏后序偏移(R-04);
|
|
454
|
+
* 其余字节不动:行内容只动命中片段,行结束符按原文(\r\n / \n 整文件统一口径)原样 join(R-05)。
|
|
455
|
+
* @param {string} projectRoot 源码仓根(doc 相对路径的锚点)
|
|
456
|
+
* @param {Array<{doc: string, docLine: number, ref: string, newRef: string}>} fixes
|
|
457
|
+
* doc 为相对 projectRoot 的文档路径;ref 旧引用串(如 src/a.js:6),newRef 新引用串(如 src/a.js:1)
|
|
458
|
+
* @param {{dryRun?: boolean}} opts dryRun 为真时不写盘仅返回将应用列表
|
|
459
|
+
* @returns {{applied: number, skipped: Array<{ref: string, reason: string}>}}
|
|
460
|
+
* dryRun 时 applied 为「将应用」计数;skipped 条目含 ref 与可读 reason(文档不存在/docLine 越界/
|
|
461
|
+
* 行内找不到 ref 串),跳过不中断批量
|
|
462
|
+
*/
|
|
463
|
+
export function applyFixes(projectRoot, fixes, opts = {}) {
|
|
464
|
+
const dryRun = opts.dryRun === true
|
|
465
|
+
let applied = 0
|
|
466
|
+
const skipped = []
|
|
467
|
+
if (!Array.isArray(fixes) || fixes.length === 0) return { applied, skipped }
|
|
468
|
+
|
|
469
|
+
// 按 doc 分组,每文档一次读改写(design §9:普通 writeFileSync,文档非运行时竞争文件)
|
|
470
|
+
const byDoc = new Map()
|
|
471
|
+
for (const f of fixes) {
|
|
472
|
+
if (!f || typeof f !== 'object' || typeof f.doc !== 'string' || typeof f.docLine !== 'number' ||
|
|
473
|
+
typeof f.ref !== 'string' || typeof f.newRef !== 'string') {
|
|
474
|
+
skipped.push({ ref: f && typeof f.ref === 'string' ? f.ref : '(条目缺失)', reason: 'fix 条目字段不完整(doc/docLine/ref/newRef)' })
|
|
475
|
+
continue
|
|
476
|
+
}
|
|
477
|
+
if (!byDoc.has(f.doc)) byDoc.set(f.doc, [])
|
|
478
|
+
byDoc.get(f.doc).push(f)
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
for (const [docRel, docFixes] of byDoc) {
|
|
482
|
+
const docAbs = join(projectRoot, docRel)
|
|
483
|
+
if (!existsSync(docAbs)) {
|
|
484
|
+
for (const f of docFixes) skipped.push({ ref: f.ref, reason: `文档不存在:${docRel}` })
|
|
485
|
+
continue
|
|
486
|
+
}
|
|
487
|
+
const raw = readFileSync(docAbs, 'utf8')
|
|
488
|
+
// 行结束符检测整文件统一口径(R-05):原文含 \r\n 则 CRLF join,否则 LF——与 readLines 归一逻辑一致
|
|
489
|
+
const eol = raw.includes('\r\n') ? '\r\n' : '\n'
|
|
490
|
+
const lines = raw.split(/\r?\n/)
|
|
491
|
+
let dirty = false
|
|
492
|
+
|
|
493
|
+
// 同一 docLine 的多 fix 按行内偏移从后往前替换(R-04):先按 docLine 分组,
|
|
494
|
+
// 组内按 ref 在该行内的命中偏移降序逐个定点替换
|
|
495
|
+
const byLine = new Map()
|
|
496
|
+
for (const f of docFixes) {
|
|
497
|
+
if (!byLine.has(f.docLine)) byLine.set(f.docLine, [])
|
|
498
|
+
byLine.get(f.docLine).push(f)
|
|
499
|
+
}
|
|
500
|
+
for (const [docLine, lineFixes] of byLine) {
|
|
501
|
+
if (!Number.isInteger(docLine) || docLine < 1 || docLine > lines.length) {
|
|
502
|
+
for (const f of lineFixes) skipped.push({ ref: f.ref, reason: `docLine=${docLine} 越界(文档共 ${lines.length} 行)` })
|
|
503
|
+
continue
|
|
504
|
+
}
|
|
505
|
+
const li = docLine - 1
|
|
506
|
+
const located = []
|
|
507
|
+
let cursor = 0
|
|
508
|
+
for (const f of lineFixes) {
|
|
509
|
+
// 找不到 ref 串、或两次命中同一偏移(重复 fix),该条目跳过不中断批量
|
|
510
|
+
const at = lines[li].indexOf(f.ref, cursor)
|
|
511
|
+
if (at === -1) {
|
|
512
|
+
skipped.push({ ref: f.ref, reason: `docLine=${docLine} 行内未找到引用串` })
|
|
513
|
+
} else {
|
|
514
|
+
located.push({ at, f })
|
|
515
|
+
cursor = at + f.ref.length
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
located.sort((a, b) => b.at - a.at) // 降序:后位先替换,前序替换不挤偏后序偏移
|
|
519
|
+
let line = lines[li]
|
|
520
|
+
for (const { at, f } of located) {
|
|
521
|
+
line = line.slice(0, at) + f.newRef + line.slice(at + f.ref.length)
|
|
522
|
+
applied++
|
|
523
|
+
}
|
|
524
|
+
if (located.length > 0) {
|
|
525
|
+
lines[li] = line
|
|
526
|
+
dirty = true
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (dirty && !dryRun) writeFileSync(docAbs, lines.join(eol))
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
return { applied, skipped }
|
|
534
|
+
}
|
package/src/docs-debt.js
CHANGED
|
@@ -14,7 +14,7 @@ import { existsSync, readFileSync, readdirSync } from 'node:fs'
|
|
|
14
14
|
import { join, relative } from 'node:path'
|
|
15
15
|
import { safeGit } from './git-helper.js'
|
|
16
16
|
import { parseModuleMapSimple } from './modules.js'
|
|
17
|
-
import { runDocsCheck } from './docs-check.js'
|
|
17
|
+
import { runDocsCheck, readDocsCheckConfig } from './docs-check.js'
|
|
18
18
|
|
|
19
19
|
/** safeGit 超时(大仓 git log 慢时降级该模块,不挂起 Wave 渲染) */
|
|
20
20
|
const GIT_TIMEOUT_MS = 5000
|
|
@@ -212,7 +212,11 @@ function inlineCardInvalidRefs(entry, docGitRoot, docPathPrefix) {
|
|
|
212
212
|
if (!docGitRoot || !docPathPrefix || !entry.doc) return null
|
|
213
213
|
try {
|
|
214
214
|
const docGitPath = `${docPathPrefix}/${entry.doc.replace(/^modules\//, '')}`
|
|
215
|
-
|
|
215
|
+
// HUB-06:补传 crossRepoRoots(与 docs gate 同口径),repo:// 跨仓引用不再恒跳过;
|
|
216
|
+
// 配置读取失败(无 local.yaml 段等)→ 不启用跨仓,与原行为一致
|
|
217
|
+
let crossRepoRoots
|
|
218
|
+
try { crossRepoRoots = readDocsCheckConfig(docGitRoot)?.crossRepoRoots } catch { crossRepoRoots = undefined }
|
|
219
|
+
const check = runDocsCheck({ projectRoot: docGitRoot, docs: [docGitPath], crossRepoRoots })
|
|
216
220
|
if (check.ok || !Array.isArray(check.invalid) || check.invalid.length === 0) return null
|
|
217
221
|
const parts = check.invalid.slice(0, 3).map((inv) => {
|
|
218
222
|
const sug = Array.isArray(inv.suggest) && inv.suggest.length > 0 ? `→建议 L${inv.suggest.join('/L')}` : ''
|
package/src/docs-gate.js
CHANGED
|
@@ -76,6 +76,7 @@ export async function runDocsGate(opts = {}, checkOpts = {}) {
|
|
|
76
76
|
paths: checkOpts.paths || cfg.paths,
|
|
77
77
|
skip: checkOpts.skip || cfg.skip,
|
|
78
78
|
keywordAssert: checkOpts.keywordAssert ?? cfg.keywordAssert,
|
|
79
|
+
crossRepoRoots: checkOpts.crossRepoRoots || cfg.crossRepoRoots,
|
|
79
80
|
});
|
|
80
81
|
} catch (e) {
|
|
81
82
|
if (e instanceof DocsCheckConfigError) {
|