sillyspec 3.26.11 → 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 +1 -1
- package/.claude/skills/sillyspec-execute/SKILL.md +1 -1
- package/.claude/skills/sillyspec-knowledge/SKILL.md +1 -1
- package/.claude/skills/sillyspec-plan/SKILL.md +5 -5
- package/.claude/skills/sillyspec-quick/SKILL.md +5 -1
- package/README.md +7 -5
- package/SKILL.md +5 -4
- package/package.json +1 -1
- package/packages/dashboard/server/executor.js +10 -1
- package/packages/dashboard/server/index.js +108 -46
- package/src/config-schema.js +3 -0
- package/src/contract-matrix.js +22 -13
- package/src/db.js +24 -4
- package/src/dispatch/probe.js +99 -63
- package/src/docs-check.js +58 -13
- package/src/docs-debt.js +6 -2
- package/src/docs-gate.js +1 -0
- package/src/doctor-diagnostics.js +208 -21
- package/src/fs-atomic.js +3 -1
- package/src/git-helper.js +7 -4
- package/src/hooks/worktree-guard.js +8 -2
- package/src/index.js +161 -12
- package/src/init.js +3 -2
- package/src/local-detect.js +9 -1
- package/src/modules.js +7 -5
- package/src/progress/stage-machine.js +589 -578
- package/src/progress/step-store.js +1 -1
- package/src/progress.js +184 -30
- package/src/quicklog.js +47 -16
- package/src/run/command.js +31 -8
- package/src/run/complete-handlers.js +21 -14
- package/src/run/complete.js +1181 -1121
- package/src/run/gates.js +148 -25
- package/src/run/prompt.js +9 -5
- package/src/run/shared.js +138 -58
- package/src/run/stage.js +41 -1
- package/src/setup.js +8 -10
- package/src/sillyhub-mcp/client.js +74 -6
- package/src/spec-dir-typo.js +3 -24
- package/src/spec-sync.js +43 -8
- package/src/stage-contract.js +12 -38
- package/src/stages/archive.js +4 -3
- package/src/stages/brainstorm-auto.js +1 -1
- package/src/stages/brainstorm.js +2 -1
- package/src/stages/doctor.js +28 -13
- package/src/stages/execute.js +164 -153
- package/src/stages/plan-postcheck.js +49 -28
- package/src/stages/plan.js +57 -40
- package/src/stages/quick.js +3 -1
- package/src/stages/verify.js +1 -1
- package/src/sync.js +323 -221
- package/src/task-review.js +246 -61
- package/src/taskcard.js +178 -0
- package/src/verify-postcheck.js +48 -35
- package/src/worktree-apply.js +58 -34
- package/src/worktree-deps.js +120 -20
- package/src/worktree.js +55 -9
- package/.npmrc-publish +0 -2
package/src/dispatch/probe.js
CHANGED
|
@@ -22,6 +22,13 @@ import { setPathAProbeResult } from './backends/sillyhub-mcp.js';
|
|
|
22
22
|
/** 默认负面缓存 TTL(毫秒)。可被 local.yaml dispatch.probe_ttl_ms 或 ttlMs 参数覆盖。 */
|
|
23
23
|
export const DEFAULT_PROBE_TTL_MS = 60_000;
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* HUB-12a:probe 总超时。链路最坏 initialize(10s) + tools/call(10s) + tools/list(10s) 串行,
|
|
27
|
+
* 慢/挂 daemon 下此前要 ~30-50s 才降级,阻塞 execute。默认 25s 截断(可达 daemon 毫秒级
|
|
28
|
+
* 响应,不受影响);opts.totalTimeoutMs 可覆盖。超时按 daemon-unreachable 语义进负面缓存。
|
|
29
|
+
*/
|
|
30
|
+
const DEFAULT_PROBE_TOTAL_TIMEOUT_MS = 25_000;
|
|
31
|
+
|
|
25
32
|
// 负面缓存:configFingerprint → { result: {available,reason}, expiresAt }
|
|
26
33
|
// 仅缓存网络派生的 unavailable 结果(daemon-unreachable),避免 daemon 抖动期反复探测(R-06)。
|
|
27
34
|
// no-config 由 env 同步派生(不入缓存);worktree-outside-root 依赖路径入参(每次重算,不入缓存)。
|
|
@@ -99,27 +106,6 @@ export function detectPathAFromTools(tools) {
|
|
|
99
106
|
return Boolean(props.worktree_path) && Boolean(props.worker_prompt);
|
|
100
107
|
}
|
|
101
108
|
|
|
102
|
-
/**
|
|
103
|
-
* 预热路径A 探测缓存(task-11):probe.js daemon 可达后 best-effort 调 client.listTools,
|
|
104
|
-
* 把 detectPathAFromTools 结果写回 sillyhub-mcp.js 缓存供 isPathASupported 同步读。
|
|
105
|
-
*
|
|
106
|
-
* best-effort:client 无 listTools 方法(旧 mock)/ listTools 异常 / 返回非数组 → 保守写 false
|
|
107
|
-
* (R-04,绝不硬试路径A),**不抛穿 probe**(探测失败不影响 availability 判定)。
|
|
108
|
-
* @param {object} cli - SillyHubMcpClient 实例(或含 listTools 的 mock)
|
|
109
|
-
*/
|
|
110
|
-
async function preheatPathAProbe(cli) {
|
|
111
|
-
try {
|
|
112
|
-
if (!cli || typeof cli.listTools !== 'function') return; // 旧 mock 无 listTools → 不预热(留默认 false)
|
|
113
|
-
const tools = await cli.listTools();
|
|
114
|
-
setPathAProbeResult(detectPathAFromTools(tools));
|
|
115
|
-
} catch (err) {
|
|
116
|
-
// 探测异常 → 保守 false(R-04);best-effort 不阻断 probe 主流程
|
|
117
|
-
const msg = err && err.message ? err.message : String(err);
|
|
118
|
-
console.warn(`[dispatch/probe] 路径A schema 探测异常(保守判 false): ${msg}`);
|
|
119
|
-
setPathAProbeResult(false);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
109
|
/**
|
|
124
110
|
* 能力探测:决定 dispatch 是否可用 SillyHub 后端。
|
|
125
111
|
*
|
|
@@ -127,21 +113,24 @@ async function preheatPathAProbe(cli) {
|
|
|
127
113
|
* 1. env 快速路径(无网络):缺 URL/TOKEN → no-config
|
|
128
114
|
* 2. 负面缓存命中且未过 TTL → 直接返回缓存(无网络,R-06)
|
|
129
115
|
* 3. 连通性探测 client.probeDaemon():false → 缓存 daemon-unreachable 并返回
|
|
130
|
-
* 3.5 路径A schema
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
116
|
+
* 3.5 路径A schema 预热 + root_path 获取(HUB-12a 合并):daemon 可达后**一次**
|
|
117
|
+
* tools/list 同时喂 detectPathAFromTools 预热与 root_path 越界校验(此前
|
|
118
|
+
* listTools + getRootPath 各发一次同 method 请求)
|
|
119
|
+
* 4. worktreePath 越界校验(R-08 best-effort)
|
|
134
120
|
* 5. 全通过 → available=true(不缓存正面结果)
|
|
135
121
|
*
|
|
122
|
+
* 整链路受 totalTimeoutMs 总超时保护(HUB-12a):超时按 daemon-unreachable 进负面缓存。
|
|
123
|
+
*
|
|
136
124
|
* @param {object} [opts]
|
|
137
125
|
* @param {object} [opts.client] - SillyHubMcpClient 实例;缺省 new SillyHubMcpClient()(经 readMcpConfig 读 local.yaml mcp 段 + env)
|
|
138
126
|
* @param {string} [opts.worktreePath] - SillySpec worktree 绝对路径(root_path 校验用)
|
|
139
127
|
* @param {string} [opts.rootPath] - daemon ws.root_path;未传(undefined)时 best-effort 从 daemon
|
|
140
128
|
* 拿(task-12 #5),拿不到则跳过越界校验(不判 unavailable)
|
|
141
129
|
* @param {number} [opts.ttlMs] - 负面缓存 TTL 覆盖(优先于 local.yaml)
|
|
130
|
+
* @param {number} [opts.totalTimeoutMs] - 总超时覆盖(默认 25s,HUB-12a)
|
|
142
131
|
* @returns {Promise<{ available: boolean, reason?: string }>}
|
|
143
132
|
*/
|
|
144
|
-
export async function probeSillyHub({ client, worktreePath, rootPath, ttlMs } = {}) {
|
|
133
|
+
export async function probeSillyHub({ client, worktreePath, rootPath, ttlMs, totalTimeoutMs } = {}) {
|
|
145
134
|
// 1. 同步快速路径:无 local.yaml mcp 段且 env 缺凭据即 unavailable(不发网络,零回归关键)。
|
|
146
135
|
// readMcpConfig 纯 fs + env 读(best-effort 不抛不发网络),返回 null = 两源都缺 → no-config。
|
|
147
136
|
if (!readMcpConfig(process.cwd())) {
|
|
@@ -155,51 +144,98 @@ export async function probeSillyHub({ client, worktreePath, rootPath, ttlMs } =
|
|
|
155
144
|
return cached.result;
|
|
156
145
|
}
|
|
157
146
|
|
|
158
|
-
// 3. 连通性探测:client.probeDaemon 自身 best-effort 不抛;再兜一层 try/catch,
|
|
159
|
-
// 即便 client 契约异常抛出也保守判 unavailable(铁律:绝不抛穿 execute)
|
|
160
147
|
const cli = client || new SillyHubMcpClient({ cwd: process.cwd() });
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
reachable = await cli.probeDaemon();
|
|
164
|
-
} catch (err) {
|
|
165
|
-
const msg = err && err.message ? err.message : String(err);
|
|
166
|
-
console.warn(`[dispatch/probe] probeDaemon 抛异常(保守判 unavailable): ${msg}`);
|
|
167
|
-
reachable = false;
|
|
168
|
-
}
|
|
169
|
-
if (!reachable) {
|
|
170
|
-
const result = { available: false, reason: 'daemon-unreachable' };
|
|
148
|
+
const cacheNegative = (reason) => {
|
|
149
|
+
const result = { available: false, reason };
|
|
171
150
|
const ttl = resolveTtl(ttlMs, process.cwd());
|
|
172
151
|
negativeCache.set(fp, { result, expiresAt: Date.now() + ttl });
|
|
173
152
|
return result;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// HUB-12a:总超时兜底——慢/挂 daemon 不拖 execute。超时后后台请求由 client 自身
|
|
156
|
+
// 10s 请求超时自然终结(有界),probe 侧按不可达降级
|
|
157
|
+
const budgetMs = typeof totalTimeoutMs === 'number' && totalTimeoutMs > 0
|
|
158
|
+
? totalTimeoutMs
|
|
159
|
+
: DEFAULT_PROBE_TOTAL_TIMEOUT_MS;
|
|
160
|
+
|
|
161
|
+
const probeBody = (async () => {
|
|
162
|
+
// 3. 连通性探测:client.probeDaemon 自身 best-effort 不抛;再兜一层 try/catch,
|
|
163
|
+
// 即便 client 契约异常抛出也保守判 unavailable(铁律:绝不抛穿 execute)
|
|
164
|
+
let reachable;
|
|
165
|
+
try {
|
|
166
|
+
reachable = await cli.probeDaemon();
|
|
167
|
+
} catch (err) {
|
|
168
|
+
const msg = err && err.message ? err.message : String(err);
|
|
169
|
+
console.warn(`[dispatch/probe] probeDaemon 抛异常(保守判 unavailable): ${msg}`);
|
|
170
|
+
reachable = false;
|
|
171
|
+
}
|
|
172
|
+
if (!reachable) return cacheNegative('daemon-unreachable');
|
|
173
|
+
|
|
174
|
+
// 3.5+4 合并(HUB-12a):一次 tools/list 同时拿 tools 数组(路径A schema 预热)与
|
|
175
|
+
// root_path(越界校验)。旧 client 无 listToolsWithMeta 时回退 listTools +
|
|
176
|
+
// getRootPath 各一次(兼容旧 mock)
|
|
177
|
+
let tools = null;
|
|
178
|
+
let toolsListRoot = undefined;
|
|
179
|
+
try {
|
|
180
|
+
if (typeof cli.listToolsWithMeta === 'function') {
|
|
181
|
+
const raw = await cli.listToolsWithMeta();
|
|
182
|
+
if (raw) {
|
|
183
|
+
if (Array.isArray(raw)) tools = raw;
|
|
184
|
+
else if (Array.isArray(raw.tools)) tools = raw.tools;
|
|
185
|
+
if (raw && typeof raw === 'object' && typeof raw.root_path === 'string' && raw.root_path) {
|
|
186
|
+
toolsListRoot = raw.root_path;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
} else if (typeof cli.listTools === 'function') {
|
|
190
|
+
tools = await cli.listTools();
|
|
194
191
|
}
|
|
192
|
+
} catch (err) {
|
|
193
|
+
const msg = err && err.message ? err.message : String(err);
|
|
194
|
+
console.warn(`[dispatch/probe] tools/list 异常(路径A 保守判 false): ${msg}`);
|
|
195
|
+
tools = null;
|
|
195
196
|
}
|
|
196
|
-
|
|
197
|
-
|
|
197
|
+
try {
|
|
198
|
+
setPathAProbeResult(detectPathAFromTools(tools));
|
|
199
|
+
} catch (err) {
|
|
200
|
+
const msg = err && err.message ? err.message : String(err);
|
|
201
|
+
console.warn(`[dispatch/probe] 路径A schema 探测异常(保守判 false): ${msg}`);
|
|
202
|
+
setPathAProbeResult(false);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (worktreePath) {
|
|
206
|
+
let effectiveRoot = rootPath;
|
|
207
|
+
if (effectiveRoot === undefined) {
|
|
208
|
+
effectiveRoot = toolsListRoot !== undefined ? toolsListRoot : null;
|
|
209
|
+
if (effectiveRoot === null && typeof cli.getRootPath === 'function') {
|
|
210
|
+
try {
|
|
211
|
+
effectiveRoot = await cli.getRootPath();
|
|
212
|
+
} catch (err) {
|
|
213
|
+
const msg = err && err.message ? err.message : String(err);
|
|
214
|
+
console.warn(`[dispatch/probe] rootPath best-effort 拿取异常(跳过越界校验): ${msg}`);
|
|
215
|
+
effectiveRoot = null;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (effectiveRoot && !isWithinRoot(effectiveRoot, worktreePath)) {
|
|
220
|
+
return { available: false, reason: 'worktree-outside-root' };
|
|
221
|
+
}
|
|
198
222
|
}
|
|
199
|
-
}
|
|
200
223
|
|
|
201
|
-
|
|
202
|
-
|
|
224
|
+
// 5. 全通过:不缓存正面结果,下次重新探测(让 daemon 恢复/退化及时反映)
|
|
225
|
+
return { available: true };
|
|
226
|
+
})();
|
|
227
|
+
|
|
228
|
+
let timeoutId = null;
|
|
229
|
+
let raced;
|
|
230
|
+
try {
|
|
231
|
+
raced = await Promise.race([
|
|
232
|
+
probeBody,
|
|
233
|
+
new Promise((resolve) => { timeoutId = setTimeout(() => resolve(null), budgetMs) }),
|
|
234
|
+
]);
|
|
235
|
+
} finally {
|
|
236
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
237
|
+
}
|
|
238
|
+
return raced || cacheNegative('probe-timeout');
|
|
203
239
|
}
|
|
204
240
|
|
|
205
241
|
/**
|
package/src/docs-check.js
CHANGED
|
@@ -17,8 +17,13 @@ import { readFileSync, writeFileSync, existsSync, readdirSync, statSync } from '
|
|
|
17
17
|
import { join } from 'node:path'
|
|
18
18
|
import jsYaml from 'js-yaml'
|
|
19
19
|
|
|
20
|
-
/** 提取 file.js:line / file.js:start-end 引用(.js/.mjs,反引号包裹与裸文本均命中,全文扫描 D-006
|
|
21
|
-
|
|
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
|
|
22
27
|
|
|
23
28
|
/** 缺省扫描范围:docs/ + .sillyspec/docs/(scan/modules 产物同是文档,失效即该暴露;2026-08-16 用户裁决改缺省,见 doc-consistency-debt.md §八) */
|
|
24
29
|
const DEFAULT_DOC_PATHS = ['docs/**/*.md', '.sillyspec/docs/**/*.md']
|
|
@@ -26,10 +31,10 @@ const DEFAULT_DOC_PATHS = ['docs/**/*.md', '.sillyspec/docs/**/*.md']
|
|
|
26
31
|
/**
|
|
27
32
|
* 读 local.yaml 的 docs-check 段(best-effort,绝不抛;缺文件/无段 → 全缺省)。
|
|
28
33
|
* @param {string} projectRoot 源码仓根(local.yaml 在 <root>/.sillyspec/local.yaml)
|
|
29
|
-
* @returns {{ paths?: string[], skip: string[], keywordAssert: boolean }}
|
|
34
|
+
* @returns {{ paths?: string[], skip: string[], keywordAssert: boolean, crossRepoRoots: Record<string, string> }}
|
|
30
35
|
*/
|
|
31
36
|
export function readDocsCheckConfig(projectRoot) {
|
|
32
|
-
const fallback = { paths: null, skip: [], keywordAssert: true }
|
|
37
|
+
const fallback = { paths: null, skip: [], keywordAssert: true, crossRepoRoots: {} }
|
|
33
38
|
try {
|
|
34
39
|
const p = join(projectRoot, '.sillyspec', 'local.yaml')
|
|
35
40
|
if (!existsSync(p)) return fallback
|
|
@@ -37,10 +42,17 @@ export function readDocsCheckConfig(projectRoot) {
|
|
|
37
42
|
if (!doc || typeof doc !== 'object') return fallback
|
|
38
43
|
const dc = doc['docs-check']
|
|
39
44
|
if (!dc || typeof dc !== 'object') return fallback
|
|
45
|
+
const crr = dc['cross_repo_roots']
|
|
40
46
|
return {
|
|
41
47
|
paths: Array.isArray(dc.paths) ? dc.paths.filter(s => typeof s === 'string') : null,
|
|
42
48
|
skip: Array.isArray(dc.skip) ? dc.skip.filter(s => typeof s === 'string') : [],
|
|
43
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
|
+
: {},
|
|
44
56
|
}
|
|
45
57
|
} catch {
|
|
46
58
|
return fallback
|
|
@@ -63,9 +75,10 @@ export function collectDocRefs(md) {
|
|
|
63
75
|
while ((m = re.exec(md)) !== null) {
|
|
64
76
|
refs.push({
|
|
65
77
|
ref: m[0],
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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),
|
|
69
82
|
docLine: md.slice(0, m.index).split(/\r?\n/).length,
|
|
70
83
|
})
|
|
71
84
|
}
|
|
@@ -268,16 +281,20 @@ function relDisplay(absPath, projectRoot) {
|
|
|
268
281
|
|
|
269
282
|
/**
|
|
270
283
|
* IO 入口:跑一次完整校验。
|
|
271
|
-
* @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
|
|
272
285
|
* projectRoot 源码仓根(候选解析与 glob 的锚点,平台模式也传源码仓根)
|
|
273
286
|
* docs 显式文档相对路径列表(优先于 paths glob 展开结果)
|
|
274
|
-
*
|
|
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 }}
|
|
275
289
|
* @throws {DocsCheckConfigError} glob 形态不支持
|
|
276
290
|
*/
|
|
277
291
|
export function runDocsCheck(opts) {
|
|
278
292
|
// paths/docs 显式传 null(readDocsCheckConfig 无 local.yaml 段时的回退值)须落回缺省 glob,
|
|
279
293
|
// 解构默认值只挡 undefined 不挡 null——docs-check 无配置裸跑曾因此 null.flatMap 崩溃。
|
|
280
|
-
const {
|
|
294
|
+
const {
|
|
295
|
+
projectRoot, docs = null, paths: rawPaths = DEFAULT_DOC_PATHS, skip = [],
|
|
296
|
+
keywordAssert = true, crossRepoRoots = {},
|
|
297
|
+
} = opts || {}
|
|
281
298
|
const paths = Array.isArray(rawPaths) && rawPaths.length > 0 ? rawPaths : DEFAULT_DOC_PATHS
|
|
282
299
|
const warnings = []
|
|
283
300
|
if (keywordAssert === false) warnings.push('关键词断言已关闭(keywordAssert=false),仅做存在性校验')
|
|
@@ -288,6 +305,8 @@ export function runDocsCheck(opts) {
|
|
|
288
305
|
const invalid = []
|
|
289
306
|
let total = 0
|
|
290
307
|
let kwChecked = 0
|
|
308
|
+
// repo:// 跨仓引用:未配映射被跳过的数量(不计入 total/invalid,跨设备零误报)
|
|
309
|
+
let crossRepoSkipped = 0
|
|
291
310
|
|
|
292
311
|
for (const docRel of docFiles) {
|
|
293
312
|
const docAbs = join(projectRoot, docRel)
|
|
@@ -303,8 +322,32 @@ export function runDocsCheck(opts) {
|
|
|
303
322
|
const mdLines = md.split(/\r?\n/)
|
|
304
323
|
|
|
305
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
|
+
}
|
|
306
350
|
total++
|
|
307
|
-
const candidates = resolveCandidates(projectRoot, r.file)
|
|
308
351
|
if (candidates.length === 0) {
|
|
309
352
|
invalid.push({
|
|
310
353
|
doc: docRel, docLine: r.docLine, ref: r.ref,
|
|
@@ -338,9 +381,11 @@ export function runDocsCheck(opts) {
|
|
|
338
381
|
}
|
|
339
382
|
if (tokens.length > 0) kwChecked++
|
|
340
383
|
if (!passedAny) {
|
|
384
|
+
// 跨仓校验失败的 reason 加 repo:// 前缀,用户一眼区分「改外部仓引用」还是「改本仓引用」
|
|
385
|
+
const repoPrefix = r.repo ? `跨仓 repo://${r.repo} → ` : ''
|
|
341
386
|
invalid.push({
|
|
342
387
|
doc: docRel, docLine: r.docLine, ref: r.ref,
|
|
343
|
-
reason: candidateFails.length > 1 ? `多候选全失败 → ${candidateFails.join(' | ')}` : candidateFails[0],
|
|
388
|
+
reason: repoPrefix + (candidateFails.length > 1 ? `多候选全失败 → ${candidateFails.join(' | ')}` : candidateFails[0]),
|
|
344
389
|
// 建议行号(--suggest):token 在首个候选文件的全量命中行,供人工确认改锚——不自动改文件
|
|
345
390
|
suggest: suggestLines(candidates, tokens),
|
|
346
391
|
// 修复分类(--fix 判定依据):全量候选 token 命中统计,唯一命中才 fixable(D-006 保守默认)
|
|
@@ -350,7 +395,7 @@ export function runDocsCheck(opts) {
|
|
|
350
395
|
}
|
|
351
396
|
}
|
|
352
397
|
|
|
353
|
-
return { ok: invalid.length === 0, total, invalid, warnings, kwChecked }
|
|
398
|
+
return { ok: invalid.length === 0, total, invalid, warnings, kwChecked, crossRepoSkipped }
|
|
354
399
|
}
|
|
355
400
|
|
|
356
401
|
/**
|
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) {
|