sillyspec 3.28.4 → 3.28.5
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/AGENTS.md +1 -0
- package/package.json +1 -1
- package/results-review-tier.md +18 -0
- package/src/agent-session-log.js +4 -3
- package/src/archive-delta.js +2 -2
- package/src/change-list.js +11 -1
- package/src/commit-suggest.js +52 -1
- package/src/config-cat.js +6 -1
- package/src/decision-distill.js +45 -5
- package/src/decisions-io.js +96 -0
- package/src/design-facts.js +3 -0
- package/src/dispatch/probe.js +31 -8
- package/src/docs-check.js +68 -3
- package/src/index.js +426 -17
- package/src/mcp-server.js +144 -0
- package/src/module-impact.js +8 -1
- package/src/modules.js +46 -34
- package/src/progress/change-registry.js +18 -0
- package/src/progress.js +13 -1
- package/src/quicklog.js +60 -6
- package/src/review-dispatch.js +377 -0
- package/src/run/archive-distill.js +48 -0
- package/src/run/command.js +19 -5
- package/src/run/complete-handlers.js +63 -3
- package/src/run/complete.js +139 -2
- package/src/run/gate-snapshot.js +88 -0
- package/src/run/gates.js +115 -55
- package/src/run/prompt.js +54 -9
- package/src/run/quick-audit.js +38 -13
- package/src/run/shared.js +109 -6
- package/src/run/stage.js +17 -2
- package/src/run/verify-quality-scan.js +336 -0
- package/src/scope-audit.js +735 -0
- package/src/sillyhub-mcp/client.js +58 -3
- package/src/stage-review-checklist.js +44 -0
- package/src/stage-review.js +122 -4
- package/src/stages/archive.js +57 -164
- package/src/stages/brainstorm.js +5 -9
- package/src/stages/execute.js +13 -10
- package/src/stages/plan.js +3 -10
- package/src/stages/verify.js +14 -33
- package/src/sync.js +51 -10
- package/src/task-review.js +108 -16
- package/src/taskcard.js +46 -3
- package/src/validate-artifacts.js +163 -0
- package/src/verify-postcheck.js +90 -10
- package/src/verify-probes.js +134 -12
- package/src/worktree-apply.js +119 -14
- package/templates/agents-instruction.md +1 -0
package/AGENTS.md
CHANGED
|
@@ -22,3 +22,4 @@
|
|
|
22
22
|
15. **quicklog 结构化落盘**:末步 `--done` 用四参数 `--req/--cause/--solution/--result`(CLI 合成结构化 output 并自动提取标题/四段分行);文件括注用 `--file-notes "path::注 || path2"`。骨架由 CLI 接管,`--done` 后按需核对即可,勿手拼模板。
|
|
23
23
|
16. **代码可能随时在修改**(多 agent 并行),Edit 前重跑 + 查最新态;破坏性 git op 前先备份。
|
|
24
24
|
17. **不奉承用户**,禁止"你说得对"类话术,直接给结论、依据、方案。
|
|
25
|
+
18. **禁止目录级 git add / git add -A**(多会话共享仓):.sillyspec/ 等目录里有并行会话的进行中文件与已提交文档,目录级暂存会夹带他者改动甚至误删已提交文件(2026-09-10 实证)。提交一律用显式 pathspec(git add -- 文件1 文件2),提交前 git status 核对暂存面只含本会话文件。
|
package/package.json
CHANGED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Worker 审查报告:src/review-tier.js(77 行,只读审查)
|
|
2
|
+
|
|
3
|
+
## 三行审查结论
|
|
4
|
+
|
|
5
|
+
1. **职责**:classifyReviewTier 是审查分级器(CLI done 门禁与 prompt 注入两侧共用同一函数防判定漂移),按 plan_level 确定性映射(none/light→self,full→independent),无 plan_level 时退「design.md 变更文件数 ≤3」启发式,designPath 完全未传(fileCount=null)时 fail-safe 判 independent(宁严勿松)。
|
|
6
|
+
2. **风险**:tier 判定权完全下放 agent 自报的 plan_level——agent 在 plan step1 低估规模报 light 即可绕过独立审查(自审自批),CLI 无第二道防线;这是 2026-08-14 tier-plan-level 改造的已知设计取舍(消除两套标准打架),但意味着分级质量上限受制于 agent 自律。
|
|
7
|
+
3. **结论**:**pass** —— 纯函数、双侧共用判定、fail-safe 方向正确(无法证明规模→从严)、注释与实现一致(含 fileCount=0 按"真无变更=极小"的字面处理);未发现逻辑缺陷,无需改动。
|
|
8
|
+
|
|
9
|
+
## 结构化摘要
|
|
10
|
+
|
|
11
|
+
- **发现**:
|
|
12
|
+
- 分级四步短路判定顺序清晰:none→self、light→self、full→independent、无 plan_level→文件数启发式(≤SELF_REVIEW_FILE_THRESHOLD=3→self)。
|
|
13
|
+
- fail-safe 双分支处理完备:fileCount=0(design 有但清单空)按极小判 self;fileCount=null(designPath 未传)判 independent。
|
|
14
|
+
- 注释记录了改造动机(此前 light+7 文件被文件数强制 independent,两套标准打架),可追溯性好。
|
|
15
|
+
- 唯一依赖 parseFileChangeList(designPath).size(Set),无其他副作用,纯函数可测性好。
|
|
16
|
+
- **结论**:pass,无阻塞问题;风险项为设计取舍而非缺陷,如实登记。
|
|
17
|
+
- **产出文件路径**:results-review-tier.md(本文件)
|
|
18
|
+
- **风险**:见三行结论第 2 条(plan_level 自报低报可绕独立审查,无 CLI 二次防线)。
|
package/src/agent-session-log.js
CHANGED
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
* - cursor / opencode:loose 探测器(无 cwd/env 归属线索,仅 precise 全落空时启用)
|
|
31
31
|
* - 其他 CLI:env SILLYSPEC_AGENT_LOG 显式指定(绝对路径)
|
|
32
32
|
*
|
|
33
|
-
* 上报配置来源(优先级):env SILLYHUB_PLATFORM_URL + SILLYHUB_PLATFORM_TOKEN
|
|
34
|
-
*
|
|
33
|
+
* 上报配置来源(优先级):env SILLYHUB_PLATFORM_URL + SILLYHUB_PLATFORM_TOKEN(预留
|
|
34
|
+
* 通道,daemon 未实现注入、需显式 export)> local.yaml platform 段(与进度同步同源)。**不受平台模式 sentinel 限制**
|
|
35
35
|
* (链路 A 进度同步在平台模式跳过是因为 daemon 自有进度回传链路;agent 日志没有
|
|
36
36
|
* daemon 链路,本上报就是它的主通道)。env SILLYSPEC_AGENT_LOG_PUSH=0 可关。
|
|
37
37
|
*
|
|
@@ -559,7 +559,8 @@ export function detectAgentLogEntries({
|
|
|
559
559
|
* 不 import sync.js 防拖 ProgressManager 重链)。
|
|
560
560
|
*
|
|
561
561
|
* 优先级:env SILLYHUB_PLATFORM_URL + SILLYHUB_PLATFORM_TOKEN(两键齐全才生效——
|
|
562
|
-
* daemon
|
|
562
|
+
* 预留的外部注入通道,daemon 未实现注入、需显式 export;平台模式 specRoot 的
|
|
563
|
+
* local.yaml 无 platform 段时本就无凭据可上报,勿指望 env 兜底)>
|
|
563
564
|
* specBase/local.yaml platform 段(url + token,与链路 A 同源)。
|
|
564
565
|
* @returns {{url:string, token:string}|null} 未配置返回 null(合法状态,静默跳过上报)
|
|
565
566
|
*/
|
package/src/archive-delta.js
CHANGED
|
@@ -489,7 +489,7 @@ export function auditModuleImpactAgainstDiff({ cwd, changeName, specDir = null }
|
|
|
489
489
|
if (inDocNotDiff.length > 0) mismatches.push(`module-impact 列而 diff 无(${inDocNotDiff.length}):${inDocNotDiff.slice(0, 5).join('、')}${inDocNotDiff.length > 5 ? ' …' : ''}`)
|
|
490
490
|
// 第三重:module-map 归属一致性(2026-09-09 外部审核 P2-1——「三重核对」此前名实不符:
|
|
491
491
|
// 矩阵行的模块列 vs map 前缀推导——误标模块点名;未命中 map 的归 unmatched 提示不判红)
|
|
492
|
-
const moduleMisattrib = auditImpactModuleAttribution(impactText,
|
|
492
|
+
const moduleMisattrib = auditImpactModuleAttribution(impactText, specBase)
|
|
493
493
|
mismatches.push(...moduleMisattrib)
|
|
494
494
|
return {
|
|
495
495
|
ok: mismatches.length === 0,
|
|
@@ -504,7 +504,7 @@ export function auditModuleImpactAgainstDiff({ cwd, changeName, specDir = null }
|
|
|
504
504
|
* 第三重核对(P2-1):矩阵行「| <模块> | `文件` |」的模块列 vs _module-map.yaml 前缀推导。
|
|
505
505
|
* 只读 fail-soft:map 不可得 → 返回 [](前两重仍有效,注入文案会带 map 跳过注记)。
|
|
506
506
|
*/
|
|
507
|
-
function auditImpactModuleAttribution(impactText,
|
|
507
|
+
function auditImpactModuleAttribution(impactText, specBase) {
|
|
508
508
|
try {
|
|
509
509
|
// 找 _module-map.yaml(docs/<p>/modules/ 扫描——detectModuleDocHealth 同款)
|
|
510
510
|
let mapText = null
|
package/src/change-list.js
CHANGED
|
@@ -259,7 +259,17 @@ function _parseFileListDetailed(designMdPath, { keepSillyspecDocs = false } = {}
|
|
|
259
259
|
// 分类列表项:`- path` / `- \`path\``
|
|
260
260
|
const listItem = line.match(/^\s*-\s+(.+)/)
|
|
261
261
|
if (listItem) {
|
|
262
|
-
|
|
262
|
+
// 坑 brainstorm-gate-agent-unavailable-and-list-path-parse 坑2:列表项「路径:描述」
|
|
263
|
+
// 整行当路径 existsSync 假阴性(文件存在却报幻觉路径)——剥首个中/英冒号及之后描述。
|
|
264
|
+
// 仅列表分支剥、且冒号前段须 looksLikePath:normalizePath 全局剥会毁 Windows 绝对路径
|
|
265
|
+
// (C:\...),列表项是仓根相对路径无盘符,安全。
|
|
266
|
+
let rawItem = listItem[1]
|
|
267
|
+
const colonIdx = rawItem.search(/[::]/)
|
|
268
|
+
if (colonIdx > 0) {
|
|
269
|
+
const head = rawItem.slice(0, colonIdx).trim()
|
|
270
|
+
if (looksLikePath(head)) rawItem = head
|
|
271
|
+
}
|
|
272
|
+
const filePath = normalizePath(rawItem)
|
|
263
273
|
if (isPlaceholder(filePath) || (filePath.startsWith('.sillyspec/') && !(keepSillyspecDocs && filePath.startsWith('.sillyspec/docs/')))) continue
|
|
264
274
|
if (!looksLikePath(filePath)) continue // 脏描述兜底
|
|
265
275
|
const incidental = INCIDENTAL_RE.test(listItem[1])
|
package/src/commit-suggest.js
CHANGED
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { existsSync, readdirSync, readFileSync, statSync } from 'fs'
|
|
20
20
|
import { join } from 'path'
|
|
21
|
-
import {
|
|
21
|
+
import { DatabaseSync } from 'node:sqlite'
|
|
22
|
+
import { gitQuiet, safeGit, stageArchiveArtifacts } from './git-helper.js'
|
|
22
23
|
import { resolveSpecDir } from './run/shared.js'
|
|
23
24
|
|
|
24
25
|
function listActiveChanges(specBase) {
|
|
@@ -222,3 +223,53 @@ export function collectCommitContext({ cwd, specDir = null }) {
|
|
|
222
223
|
suggestion,
|
|
223
224
|
}
|
|
224
225
|
}
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* archive 语境判定(P1-6,noai-ir-roadmap §4):commit --apply 只在归档阶段内放行
|
|
230
|
+
* (changes.status=archived 或 current_stage=archive)——那里文件已验收、pathspec 显式
|
|
231
|
+
* (stageArchiveArtifacts 三路径);其余阶段一律拒绝(agent 上下文「需人确认」是伪约束,
|
|
232
|
+
* 通用自动 git 在多会话仓是历史重灾区)。DB 直查避免 read 对注销变更的视图差异。
|
|
233
|
+
*/
|
|
234
|
+
export function resolveArchiveCommitScope({ cwd, specDir = null, changeName }) {
|
|
235
|
+
const specBase = resolveSpecDir(cwd, { specDir })
|
|
236
|
+
let status = null
|
|
237
|
+
let currentStage = null
|
|
238
|
+
try {
|
|
239
|
+
const db = new DatabaseSync(join(specBase, '.runtime', 'sillyspec.db'))
|
|
240
|
+
const row = db.prepare('SELECT status, current_stage FROM changes WHERE name = ?').get(changeName)
|
|
241
|
+
db.close()
|
|
242
|
+
if (row) { status = row.status; currentStage = row.current_stage }
|
|
243
|
+
} catch { /* DB 缺失/损坏 → 视为不可判定(拒绝) */ }
|
|
244
|
+
return { specBase, status, currentStage, inArchiveScope: status === 'archived' || currentStage === 'archive' }
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* 执行归档提交(P1-6):语义来源(QUICKLOG/勾选 task → conventional message)× 显式
|
|
249
|
+
* pathspec(归档产物既有清单)。共享 index 安全:git add 用显式 pathspec(规则 18),
|
|
250
|
+
* git commit 带 `-- <paths>` pathspec 限定——同 main 并行会话暂存的其他文件不会被扫入
|
|
251
|
+
* (pathspec commit 只提交指定路径的内容,与暂存区其余部分无关)。
|
|
252
|
+
*/
|
|
253
|
+
export function applyArchiveCommit({ cwd, specDir = null, changeName }) {
|
|
254
|
+
const scope = resolveArchiveCommitScope({ cwd, specDir, changeName })
|
|
255
|
+
if (!scope.inArchiveScope) {
|
|
256
|
+
return { ok: false, reason: `变更 ${changeName} 不在归档语境(status=${scope.status}, current_stage=${scope.currentStage})——commit --apply 只限归档阶段内使用(文件已验收 + pathspec 显式);其余阶段请手写 message 手动提交` }
|
|
257
|
+
}
|
|
258
|
+
const cc = collectCommitContext({ cwd, specDir })
|
|
259
|
+
if (!cc.suggestion || !cc.suggestion.subject) {
|
|
260
|
+
return { ok: false, reason: '无语义来源(QUICKLOG/勾选 task 均无新条目)——请结合 git status 手写 message 后手动提交' }
|
|
261
|
+
}
|
|
262
|
+
const art = stageArchiveArtifacts(cwd)
|
|
263
|
+
const paths = (art.staged || []).filter((p) => existsSync(p))
|
|
264
|
+
if (paths.length === 0) {
|
|
265
|
+
return { ok: false, reason: '无归档产物路径可提交(stageArchiveArtifacts 候选全部不存在)' }
|
|
266
|
+
}
|
|
267
|
+
const add = safeGit(cwd, ['add', '--', ...paths])
|
|
268
|
+
if (add.error) return { ok: false, reason: `git add 失败: ${add.error}` }
|
|
269
|
+
const commitArgs = ['commit', '-m', cc.suggestion.subject]
|
|
270
|
+
if (cc.suggestion.body) commitArgs.push('-m', cc.suggestion.body)
|
|
271
|
+
commitArgs.push('--', ...paths)
|
|
272
|
+
const commit = safeGit(cwd, commitArgs)
|
|
273
|
+
if (commit.error) return { ok: false, reason: `git commit 失败: ${commit.error}`, paths }
|
|
274
|
+
return { ok: true, subject: cc.suggestion.subject, paths, output: commit.value }
|
|
275
|
+
}
|
package/src/config-cat.js
CHANGED
|
@@ -49,10 +49,15 @@ export function resolveLocalYaml(dir, opts = {}) {
|
|
|
49
49
|
const home = homedir()
|
|
50
50
|
const originBelowHome = start !== home && start.startsWith(home + sep)
|
|
51
51
|
if (opts.specBase) push(join(opts.specBase, 'local.yaml'), 'spec 根(--spec-dir/平台 pointer/最近 .sillyspec)')
|
|
52
|
+
let passedHome = false
|
|
52
53
|
for (let d = start; ; d = dirname(d)) {
|
|
53
|
-
|
|
54
|
+
// passedHome:经过 home 后不再入候选(resolveSpecDir e4f2855 同款语义)——套件隔离下
|
|
55
|
+
// HOME 与 fixture 可能同根(HOME=suiteTmp、fixture=suiteTmp/x),遍历越过 home 层后
|
|
56
|
+
// 仍会撞真实 home 的 ~/.sillyspec/local.yaml,须整段截断。
|
|
57
|
+
if (!(passedHome || (originBelowHome && d === home))) {
|
|
54
58
|
push(join(d, '.sillyspec', 'local.yaml'), 'cwd 祖先链(worktree 内向上命中主仓;home 层受守卫跳过)')
|
|
55
59
|
}
|
|
60
|
+
if (d === home) passedHome = true
|
|
56
61
|
const parent = dirname(d)
|
|
57
62
|
if (parent === d) break
|
|
58
63
|
}
|
package/src/decision-distill.js
CHANGED
|
@@ -69,8 +69,8 @@ function dispositionOf(entry) {
|
|
|
69
69
|
/** decisions.md 列表字段标签 → entry 字段(兼容中英标签与中英冒号;其余字段仅留在 raw) */
|
|
70
70
|
function applyField(entry, label, value) {
|
|
71
71
|
switch (label) {
|
|
72
|
-
case 'type': entry.type = firstToken(value); break
|
|
73
|
-
case 'status': entry.status = firstToken(value); break
|
|
72
|
+
case 'type': case '类型': entry.type = firstToken(value); break
|
|
73
|
+
case 'status': case '状态': entry.status = firstToken(value); break
|
|
74
74
|
case 'question': case '问题': entry.question = value; break
|
|
75
75
|
case 'answer': case '答案': entry.answer = value; break
|
|
76
76
|
case '锚点': case 'anchor': entry.anchor = value; break
|
|
@@ -111,6 +111,11 @@ export function parseDecisions(changeDir) {
|
|
|
111
111
|
entries.push(cur)
|
|
112
112
|
}
|
|
113
113
|
for (const line of content.replace(/\r\n/g, '\n').split('\n')) {
|
|
114
|
+
// 条目开头两形态(坑 decision-flat-list-silent-zero,2026-09-10 驾驭小结第四批②,用户实证:
|
|
115
|
+
// 扁平列表格式 decisions.md 解析 0 条静默放行):
|
|
116
|
+
// ①标题式(模板正统):`## D-001@v2 标题`,正文行 `- 字段:值`
|
|
117
|
+
// ②扁平列表式(agent 实际产出):`- D-001@v2:标题` 或 `- D-001@v2 | 状态:implemented | 模块域:x`
|
|
118
|
+
// ——行内 | 分段即字段;后续缩进子项 ` - 字段:值` 同样进字段
|
|
114
119
|
const h = line.match(/^##\s+(D-\d+)(?:@v(\d+))?\s*(.*)$/)
|
|
115
120
|
if (h) {
|
|
116
121
|
flush()
|
|
@@ -118,15 +123,44 @@ export function parseDecisions(changeDir) {
|
|
|
118
123
|
cur = { id: `${h[1]}@v${version}`, number: h[1], version, title: h[3].trim(), rawLines: [line] }
|
|
119
124
|
continue
|
|
120
125
|
}
|
|
126
|
+
const b = line.match(/^[-*]\s+(D-\d+)(?:@v(\d+))?\s*(?:[::]\s*|\s+—\s*|\s*——\s*|\s*\|\s*)?\s*(.*)$/)
|
|
127
|
+
if (b) {
|
|
128
|
+
flush()
|
|
129
|
+
const version = b[2] ? parseInt(b[2], 10) : 1
|
|
130
|
+
cur = { id: `${b[1]}@v${version}`, number: b[1], version, title: '', rawLines: [line] }
|
|
131
|
+
// 行内 | 分段字段:`- D-001@v1 标题 | 状态:implemented | 模块域:core`
|
|
132
|
+
// 首个非「字段:值」形态的分段为标题;每段独立过 applyField(未知标签忽略)。
|
|
133
|
+
const rest = (b[3] || '').trim()
|
|
134
|
+
const segs = rest.split(/[||]/).map(s => s.trim()).filter(Boolean)
|
|
135
|
+
const nonFieldSegs = []
|
|
136
|
+
for (const seg of segs) {
|
|
137
|
+
const inline = seg.match(/^([^::]{1,20})[::]\s*(.*)$/)
|
|
138
|
+
if (inline && FIELD_LABEL_RE.test(inline[1].trim())) {
|
|
139
|
+
applyField(cur, inline[1].trim().toLowerCase(), inline[2].trim())
|
|
140
|
+
} else {
|
|
141
|
+
nonFieldSegs.push(seg)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
cur.title = nonFieldSegs.join(' | ')
|
|
145
|
+
continue
|
|
146
|
+
}
|
|
121
147
|
if (!cur) continue
|
|
122
148
|
cur.rawLines.push(line)
|
|
123
|
-
|
|
124
|
-
|
|
149
|
+
// 字段行:标题式条目的顶格 `- 字段:值` + 扁平式条目的缩进子项 ` - 字段:值`(缩进至多 4 空格,
|
|
150
|
+
// 不吞正文嵌套散文——字段标签白名单外的行不进字段,见 applyField default 忽略)
|
|
151
|
+
const f = line.match(/^(?: {1,4})?[-*]\s+([^\s::]{1,20})\s*[::]\s*(.*)$/)
|
|
152
|
+
if (f && FIELD_LABEL_RE.test(f[1])) applyField(cur, f[1].toLowerCase(), f[2].trim())
|
|
125
153
|
}
|
|
126
154
|
flush()
|
|
127
|
-
|
|
155
|
+
// 0 条告警素材(消费方 distillIntoKnowledge 据此显式 warn,不再静默放行):
|
|
156
|
+
// 文件非空、正文出现 D-xxx 形态却一条都没解析出 = 格式两种都不匹配,必须可见
|
|
157
|
+
const hasDecisionLike = /D-\d+/.test(content)
|
|
158
|
+
return { entries, missing: false, zeroWithContent: hasDecisionLike && entries.length === 0 }
|
|
128
159
|
}
|
|
129
160
|
|
|
161
|
+
/** applyField 认识的字段标签白名单(行内 | 分段与缩进子项只按白名单进字段,散文误伤为零) */
|
|
162
|
+
const FIELD_LABEL_RE = /^(type|types|status|question|问题|answer|答案|锚点|anchor|模块域|模块|domains|domain|否决理由|reject_reason|rejectreason|复潮条件|revisit_when|revisitwhen|supersedes|impacts|影响|normalized_requirement|状态|类型)$/i
|
|
163
|
+
|
|
130
164
|
// ---------------------------------------------------------------------------
|
|
131
165
|
// 域三级兜底(FR-03)
|
|
132
166
|
// ---------------------------------------------------------------------------
|
|
@@ -434,6 +468,12 @@ export function distillIntoKnowledge(changeDir, knowledgeRoot, headHash, moduleI
|
|
|
434
468
|
if (parsed.missing) {
|
|
435
469
|
return { written: [], skipped: `decisions.md 不存在(${join(changeDir, 'decisions.md')}),零输出`, needsWait: null }
|
|
436
470
|
}
|
|
471
|
+
// 0 条告警(坑 decision-flat-list-silent-zero,2026-09-10 驾驭小结第四批②):文件有内容、
|
|
472
|
+
// 正文含 D-xxx 形态却 0 条可解析 = 标题式/扁平列表式两种格式都不匹配——此前静默放行
|
|
473
|
+
// (skipped 文案还误导成「解析 0 条,0 条入选」),决策知识凭空丢失不可见。显式 warn 留痕。
|
|
474
|
+
if (parsed.zeroWithContent) {
|
|
475
|
+
console.warn(`⚠️ decisions.md 存在且含 D-xxx 条目形态,但 0 条可解析(既非「## D-xxx」标题式、也非「- D-xxx@vN」扁平列表式)——格式不识别,本次提炼零输出;请核对格式(标题式 \`## D-001@v1 标题\` + 正文 \`- 字段:值\`,或扁平式 \`- D-001@v1:标题 | 状态:implemented\`)`)
|
|
476
|
+
}
|
|
437
477
|
const selected = parsed.entries.filter(e => e.selected)
|
|
438
478
|
if (selected.length === 0) {
|
|
439
479
|
return { written: [], skipped: `无入选条目(解析 ${parsed.entries.length} 条,0 条入选:scope/非五类 type/非 confirmed|accepted|rejected 状态均不入选),零输出`, needsWait: null }
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* decisions-io.js(P1-5,noai-ir-roadmap §4):decisions.md 的 CLI 单一写入者。
|
|
3
|
+
*
|
|
4
|
+
* 背景:decisions.md 此前由 agent 手拼自由 markdown,13 处解析点里它占两处
|
|
5
|
+
* (decision-distill parseDecisions × design-facts 镜像),历史事故全在「手写格式走样」:
|
|
6
|
+
* 扁平列表静默 0 条(decision-flat-list-silent-zero)、CRLF 状态行恒失败、字段标签拼写漂移。
|
|
7
|
+
* 单一写入者 = canonical 格式只此一家:标题式条目(`## D-001@v2 标题` + `- 字段:值`,
|
|
8
|
+
* 字段标签用 applyField 白名单的中文正统名),读取侧 parseDecisions 零改动双向兼容。
|
|
9
|
+
*
|
|
10
|
+
* 语义:
|
|
11
|
+
* - upsertDecision:同 id@v 条目整块替换(幂等,重跑同输入零 diff);新条目追加文末。
|
|
12
|
+
* 不碰 frontmatter 与其他条目;EOL 跟随原文件;原子写。
|
|
13
|
+
* - 校验:number 形态 D-xxx、version 正整数、status/type 白名单外的值拒绝
|
|
14
|
+
* (fail-fast——写入者先于解析者挡格式错误,这正是单一写入者的意义)。
|
|
15
|
+
* - agent 仍可手写正文散文(决策动机等),但结构化字段一律走本写入者。
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
18
|
+
import { join } from 'node:path'
|
|
19
|
+
import { writeAtomicSync } from './fs-atomic.js'
|
|
20
|
+
|
|
21
|
+
const STATUS_VALUES = new Set(['confirmed', 'accepted', 'implemented', 'rejected', 'proposed', 'superseded', 'draft'])
|
|
22
|
+
const TYPE_VALUES = new Set(['architecture', 'compatibility', 'boundary', 'definition', 'process', 'scope'])
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* canonical 条目渲染(正文散文行原样保留在 body 数组)。
|
|
26
|
+
* @param {{ number: string, version: number, title: string, body?: string[], status?: string, type?: string, question?: string, answer?: string, anchor?: string, domains?: string[], rejectReason?: string, revisitWhen?: string, supersedes?: string, impacts?: string, normalizedRequirement?: string }} d
|
|
27
|
+
* @returns {string} 条目 markdown 块(不含首尾空行)
|
|
28
|
+
*/
|
|
29
|
+
export function renderDecisionEntry(d) {
|
|
30
|
+
if (!d || !/^D-\d+$/.test(d.number || '')) throw new Error(`决策编号形态非法(期望 D-xxx,实得 ${JSON.stringify(d && d.number)})`)
|
|
31
|
+
const version = parseInt(d.version, 10)
|
|
32
|
+
if (!Number.isInteger(version) || version < 1) throw new Error(`决策版本非法(期望正整数,实得 ${JSON.stringify(d.version)})`)
|
|
33
|
+
if (d.status && !STATUS_VALUES.has(d.status)) throw new Error(`status 白名单外:${d.status}(合法:${[...STATUS_VALUES].join(' / ')})`)
|
|
34
|
+
if (d.type && !TYPE_VALUES.has(d.type)) throw new Error(`type 白名单外:${d.type}(合法:${[...TYPE_VALUES].join(' / ')})`)
|
|
35
|
+
const lines = [`## ${d.number}@v${version} ${String(d.title || '').trim()}`]
|
|
36
|
+
if (d.status) lines.push(`- 状态:${d.status}`)
|
|
37
|
+
if (d.type) lines.push(`- 类型:${d.type}`)
|
|
38
|
+
if (Array.isArray(d.domains) && d.domains.length > 0) lines.push(`- 模块域:${d.domains.join(', ')}`)
|
|
39
|
+
if (d.question) lines.push(`- 问题:${d.question}`)
|
|
40
|
+
if (d.answer) lines.push(`- 答案:${d.answer}`)
|
|
41
|
+
if (d.anchor) lines.push(`- 锚点:${d.anchor}`)
|
|
42
|
+
if (d.supersedes) lines.push(`- supersedes:${d.supersedes}`)
|
|
43
|
+
if (d.impacts) lines.push(`- 影响:${d.impacts}`)
|
|
44
|
+
if (d.rejectReason) lines.push(`- 否决理由:${d.rejectReason}`)
|
|
45
|
+
if (d.revisitWhen) lines.push(`- 复潮条件:${d.revisitWhen}`)
|
|
46
|
+
if (d.normalizedRequirement) lines.push(`- normalized_requirement:${d.normalizedRequirement}`)
|
|
47
|
+
for (const prose of (Array.isArray(d.body) ? d.body : [])) {
|
|
48
|
+
if (typeof prose === 'string' && prose.trim() !== '') lines.push(prose.trimEnd())
|
|
49
|
+
}
|
|
50
|
+
return lines.join('\n')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* upsert 一条决策:同 id@v 整块替换,否则文末追加。返回 { path, action: 'replaced'|'appended' }。
|
|
55
|
+
* 文件不存在 → 以新文件创建(含空 frontmatter 占位不写——decisions.md 可无 frontmatter,
|
|
56
|
+
* 但本仓惯例有 author/created_at;创建时写最小 frontmatter 由调用方 flag 补充)。
|
|
57
|
+
*/
|
|
58
|
+
export function upsertDecision(changeDir, decision) {
|
|
59
|
+
const filePath = join(changeDir, 'decisions.md')
|
|
60
|
+
const block = renderDecisionEntry(decision)
|
|
61
|
+
const id = `${decision.number}@v${parseInt(decision.version, 10)}`
|
|
62
|
+
if (!existsSync(filePath)) {
|
|
63
|
+
const content = `# 决策记录\n\n${block}\n`
|
|
64
|
+
writeAtomicSync(filePath, content)
|
|
65
|
+
return { path: filePath, action: 'appended' }
|
|
66
|
+
}
|
|
67
|
+
const raw = readFileSync(filePath, 'utf8')
|
|
68
|
+
const eol = raw.includes('\r\n') ? '\r\n' : '\n'
|
|
69
|
+
const normalized = raw.replace(/\r\n/g, '\n')
|
|
70
|
+
const lines = normalized.split('\n')
|
|
71
|
+
const header = `## ${id} `
|
|
72
|
+
const startIdx = lines.findIndex((l) => l.startsWith(`## ${id} `) || l.trim() === `## ${id}`)
|
|
73
|
+
if (startIdx === -1) {
|
|
74
|
+
const next = normalized.replace(/\n*$/, '\n') + '\n' + block + '\n'
|
|
75
|
+
writeAtomicSync(filePath, eol === '\n' ? next : next.replace(/\n/g, '\r\n'))
|
|
76
|
+
return { path: filePath, action: 'appended' }
|
|
77
|
+
}
|
|
78
|
+
let endIdx = lines.length
|
|
79
|
+
for (let i = startIdx + 1; i < lines.length; i++) {
|
|
80
|
+
if (lines[i].startsWith('## ')) { endIdx = i; break }
|
|
81
|
+
}
|
|
82
|
+
// 保留原条目正文散文(非「- 字段:值」白名单行)随替换块回填——CLI 拥有字段,散文归作者
|
|
83
|
+
const preservedProse = []
|
|
84
|
+
for (const line of lines.slice(startIdx + 1, endIdx)) {
|
|
85
|
+
const t = line.trim()
|
|
86
|
+
if (t === '' ) continue
|
|
87
|
+
if (/^(?: {1,4})?[-*]\s+(状态|类型|模块域|问题|答案|锚点|否决理由|复潮条件|影响)\s*[::]/.test(t)) continue
|
|
88
|
+
if (/^(?: {1,4})?[-*]\s+(supersedes|impacts|normalized_requirement|status|type|domains|domain|question|answer|anchor|reject_reason|revisit_when)\s*[::]/i.test(t)) continue
|
|
89
|
+
preservedProse.push(line)
|
|
90
|
+
}
|
|
91
|
+
const merged = { ...decision, body: [...(decision.body || []), ...preservedProse] }
|
|
92
|
+
const mergedBlock = renderDecisionEntry(merged)
|
|
93
|
+
const next = [...lines.slice(0, startIdx), ...mergedBlock.split('\n'), '', ...lines.slice(endIdx)].join('\n')
|
|
94
|
+
writeAtomicSync(filePath, eol === '\n' ? next : next.replace(/\n/g, '\r\n'))
|
|
95
|
+
return { path: filePath, action: 'replaced' }
|
|
96
|
+
}
|
package/src/design-facts.js
CHANGED
|
@@ -261,6 +261,9 @@ export function generateDesignSkeleton({ changeName, decisionsText, author, now
|
|
|
261
261
|
lines.push('---')
|
|
262
262
|
lines.push(`author: ${String(author || '').trim() || 'TODO(git 用户名)'}`)
|
|
263
263
|
lines.push(`created_at: ${String(now || '').trim() || 'TODO(ISO 时间)'}`)
|
|
264
|
+
// P2-e provenance 戳(noai-ir-roadmap §5):CLI 骨架出品的可审计标记——铁律 8 收窄为
|
|
265
|
+
//「骨架优先,仅手写补文档才手填元数据」的依据(validateMetadata 未来可据此只认 CLI 戳)
|
|
266
|
+
lines.push(`generated_by: sillyspec-design-init`)
|
|
264
267
|
lines.push('scale: large')
|
|
265
268
|
lines.push('---')
|
|
266
269
|
lines.push('')
|
package/src/dispatch/probe.js
CHANGED
|
@@ -128,26 +128,31 @@ export function detectPathAFromTools(tools) {
|
|
|
128
128
|
* 拿(task-12 #5),拿不到则跳过越界校验(不判 unavailable)
|
|
129
129
|
* @param {number} [opts.ttlMs] - 负面缓存 TTL 覆盖(优先于 local.yaml)
|
|
130
130
|
* @param {number} [opts.totalTimeoutMs] - 总超时覆盖(默认 25s,HUB-12a)
|
|
131
|
-
* @
|
|
131
|
+
* @param {string} [opts.cwd] - local.yaml/env 定位根(默认 process.cwd())。测试
|
|
132
|
+
* 注入干净 tmp 目录隔离「dev 仓自身已连平台」的环境
|
|
133
|
+
* (坑 probe-no-config-cwd-leak:本仓 local.yaml 带 mcp
|
|
134
|
+
* 段时 no-config 用例读到真配置,同步快速路径判定漂移)
|
|
135
|
+
* @returns {Promise<{ available: boolean, reason?: string, daemonOnline?: boolean|null }>}
|
|
132
136
|
*/
|
|
133
|
-
export async function probeSillyHub({ client, worktreePath, rootPath, ttlMs, totalTimeoutMs } = {}) {
|
|
137
|
+
export async function probeSillyHub({ client, worktreePath, rootPath, ttlMs, totalTimeoutMs, cwd } = {}) {
|
|
138
|
+
const probeCwd = cwd || process.cwd();
|
|
134
139
|
// 1. 同步快速路径:无 local.yaml mcp 段且 env 缺凭据即 unavailable(不发网络,零回归关键)。
|
|
135
140
|
// readMcpConfig 纯 fs + env 读(best-effort 不抛不发网络),返回 null = 两源都缺 → no-config。
|
|
136
|
-
if (!readMcpConfig(
|
|
141
|
+
if (!readMcpConfig(probeCwd)) {
|
|
137
142
|
return { available: false, reason: 'no-config' };
|
|
138
143
|
}
|
|
139
144
|
|
|
140
145
|
// 2. 负面缓存命中(未过 TTL)→ 直接返回,不发网络(R-06 抖动期免反复探测)
|
|
141
|
-
const fp = configFingerprint(
|
|
146
|
+
const fp = configFingerprint(probeCwd);
|
|
142
147
|
const cached = negativeCache.get(fp);
|
|
143
148
|
if (cached && cached.expiresAt > Date.now()) {
|
|
144
149
|
return cached.result;
|
|
145
150
|
}
|
|
146
151
|
|
|
147
|
-
const cli = client || new SillyHubMcpClient({ cwd:
|
|
152
|
+
const cli = client || new SillyHubMcpClient({ cwd: probeCwd });
|
|
148
153
|
const cacheNegative = (reason) => {
|
|
149
154
|
const result = { available: false, reason };
|
|
150
|
-
const ttl = resolveTtl(ttlMs,
|
|
155
|
+
const ttl = resolveTtl(ttlMs, probeCwd);
|
|
151
156
|
negativeCache.set(fp, { result, expiresAt: Date.now() + ttl });
|
|
152
157
|
return result;
|
|
153
158
|
};
|
|
@@ -202,6 +207,23 @@ export async function probeSillyHub({ client, worktreePath, rootPath, ttlMs, tot
|
|
|
202
207
|
setPathAProbeResult(false);
|
|
203
208
|
}
|
|
204
209
|
|
|
210
|
+
// 3.6 daemon 在线层(2026-09-10 平台侧 P1-3 新工具 get_daemon_status):daemon_online
|
|
211
|
+
// =false 是确定性「派了也无人认领」信号(spike 实证:无 daemon 认领时 worker 0.2s
|
|
212
|
+
// 快速失败 no_online_daemon)——正面探明,替代「真派发一次才知道」的浪费。fail-open
|
|
213
|
+
// 三态:true/false 明确;null(无工具=旧 backend / token 缺 read scope / 调用异常)不判
|
|
214
|
+
// unavailable,回退旧口径(可用性未知按可用走)。不进负面缓存(活状态随 daemon 起停
|
|
215
|
+
// 变化,与 worktree-outside-root 同类,非网络派生)。
|
|
216
|
+
let daemonOnline = null;
|
|
217
|
+
try {
|
|
218
|
+
if (typeof cli.getDaemonStatus === 'function') {
|
|
219
|
+
const st = await cli.getDaemonStatus();
|
|
220
|
+
if (st && typeof st.online === 'boolean') daemonOnline = st.online;
|
|
221
|
+
}
|
|
222
|
+
} catch { daemonOnline = null; }
|
|
223
|
+
if (daemonOnline === false) {
|
|
224
|
+
return { available: false, reason: 'daemon-offline', daemonOnline };
|
|
225
|
+
}
|
|
226
|
+
|
|
205
227
|
if (worktreePath) {
|
|
206
228
|
let effectiveRoot = rootPath;
|
|
207
229
|
if (effectiveRoot === undefined) {
|
|
@@ -221,8 +243,9 @@ export async function probeSillyHub({ client, worktreePath, rootPath, ttlMs, tot
|
|
|
221
243
|
}
|
|
222
244
|
}
|
|
223
245
|
|
|
224
|
-
// 5. 全通过:不缓存正面结果,下次重新探测(让 daemon
|
|
225
|
-
|
|
246
|
+
// 5. 全通过:不缓存正面结果,下次重新探测(让 daemon 恢复/退化及时反映);
|
|
247
|
+
// daemonOnline 透传(true/null——调用方可区分「探明在线」与「未知」)
|
|
248
|
+
return { available: true, daemonOnline };
|
|
226
249
|
})();
|
|
227
250
|
|
|
228
251
|
let timeoutId = null;
|
package/src/docs-check.js
CHANGED
|
@@ -36,8 +36,11 @@ import { parseModuleMapSimple } from './modules.js'
|
|
|
36
36
|
* 展开循环每次迭代必含括号段 → 划分唯一 → 线性。 */
|
|
37
37
|
const REF_RE = /(?:repo:\/\/([A-Za-z0-9_.\-]+)\/)?([A-Za-z0-9_.\-\/]*(?:\([A-Za-z0-9_.\-\/]+\)[A-Za-z0-9_.\-\/]*)*\.(?:js|mjs|cjs|ts|tsx|jsx|py|java|go)):(\d+)(?:-(\d+))?/g
|
|
38
38
|
|
|
39
|
-
/** 缺省扫描范围:docs/ + .sillyspec/docs/(scan/modules 产物同是文档,失效即该暴露;2026-08-16 用户裁决改缺省,见 doc-consistency-debt.md §八)
|
|
40
|
-
|
|
39
|
+
/** 缺省扫描范围:docs/ + .sillyspec/docs/(scan/modules 产物同是文档,失效即该暴露;2026-08-16 用户裁决改缺省,见 doc-consistency-debt.md §八)
|
|
40
|
+
* + .sillyspec/changes/ + .sillyspec/knowledge/(P1-1,noai-ir-roadmap §4:最活跃、最易产生
|
|
41
|
+
* 漂移引用的变更目录与决策库此前零校验——纳入后 verify/archive 收尾的 autoReanchorDocRefs
|
|
42
|
+
* 同时覆盖变更文档(archive/ 路径段与 doc_type: snapshot 双通道豁免不变,归档件不回检)。 */
|
|
43
|
+
export const DEFAULT_DOC_PATHS = ['docs/**/*.md', '.sillyspec/docs/**/*.md', '.sillyspec/changes/**/*.md', '.sillyspec/knowledge/**/*.md']
|
|
41
44
|
|
|
42
45
|
/**
|
|
43
46
|
* 读 local.yaml 的 docs-check 段(best-effort,绝不抛;缺文件/无段 → 全缺省)。
|
|
@@ -73,6 +76,32 @@ export function readDocsCheckConfig(projectRoot) {
|
|
|
73
76
|
/** 配置错误(glob 形态不支持等)→ CLI exit 2 */
|
|
74
77
|
export class DocsCheckConfigError extends Error {}
|
|
75
78
|
|
|
79
|
+
/**
|
|
80
|
+
* 符号锚正则(P1-2,noai-ir-roadmap §4):`file.js::symbol` 形态——校验时解析符号定义行,
|
|
81
|
+
* 天然免疫行号漂移(重锚从「每次代码移动」降为「仅改名时」)。文件段与 REF_RE 同款(含
|
|
82
|
+
* repo:// 跨仓前缀与括号路径组展开),`::` 后只认 ASCII 标识符(自然语言/中文括注不触发,
|
|
83
|
+
* QUICKLOG file-notes 的 `path::注` 约定因注解非标识符而天然互斥)。与 REF_RE 互斥:
|
|
84
|
+
* `::ident` 无数字不匹配行号式;`file.js:12` 无 `::` 不匹配符号式。
|
|
85
|
+
*/
|
|
86
|
+
const SYMBOL_REF_RE = /(?:repo:\/\/([A-Za-z0-9_.\-]+)\/)?([A-Za-z0-9_.\-\/]*(?:\([A-Za-z0-9_.\-\/]+\)[A-Za-z0-9_.\-\/]*)*\.(?:js|mjs|cjs|ts|tsx|jsx|py|java|go))::([A-Za-z_$][A-Za-z0-9_$]*)/g
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 符号定义行定位(与 classifyFix 的 defRe 同款形态,单一判定口径):function/const/let/
|
|
90
|
+
* class/def(含 export/async 前缀)后的精确符号名。命中返回 1-based 行号;文件内仅
|
|
91
|
+
* 「使用」未「定义」(import 消费/注释提及)不算命中——符号锚指向定义文件。
|
|
92
|
+
* @param {string[]} lines 源文件行数组
|
|
93
|
+
* @param {string} symbol 符号名(须为合法标识符)
|
|
94
|
+
* @returns {number|null}
|
|
95
|
+
*/
|
|
96
|
+
export function findSymbolDefLine(lines, symbol) {
|
|
97
|
+
if (!Array.isArray(lines) || !symbol || !/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(symbol)) return null
|
|
98
|
+
const defRe = new RegExp(`(?:export\\s+)?(?:async\\s+)?(?:function\\*?\\s+|class\\s+|(?:const|let|var)\\s+|def\\s+)${escapeReLocal(symbol)}\\b`)
|
|
99
|
+
for (let i = 0; i < lines.length; i++) {
|
|
100
|
+
if (defRe.test(lines[i])) return i + 1
|
|
101
|
+
}
|
|
102
|
+
return null
|
|
103
|
+
}
|
|
104
|
+
|
|
76
105
|
/**
|
|
77
106
|
* 纯函数:从 markdown 全文提取全部 file:line 引用。
|
|
78
107
|
* @param {string} md 文档全文
|
|
@@ -101,6 +130,22 @@ export function collectDocRefs(md) {
|
|
|
101
130
|
docLine: line,
|
|
102
131
|
})
|
|
103
132
|
}
|
|
133
|
+
// 符号锚引用(P1-2):独立正则二遍扫描(与行号式互斥,无重叠去重需求)。
|
|
134
|
+
// 符号式引用量级远小于行号式,docLine 逐个 O(index) 计算不构成退化面。
|
|
135
|
+
const symRe = new RegExp(SYMBOL_REF_RE.source, 'g')
|
|
136
|
+
let sm
|
|
137
|
+
while ((sm = symRe.exec(md)) !== null) {
|
|
138
|
+
refs.push({
|
|
139
|
+
ref: sm[0],
|
|
140
|
+
repo: sm[1] !== undefined ? sm[1] : null,
|
|
141
|
+
file: sm[2],
|
|
142
|
+
symbol: sm[3],
|
|
143
|
+
kind: 'symbol',
|
|
144
|
+
start: null,
|
|
145
|
+
end: null,
|
|
146
|
+
docLine: md.slice(0, sm.index).split('\n').length,
|
|
147
|
+
})
|
|
148
|
+
}
|
|
104
149
|
return refs
|
|
105
150
|
}
|
|
106
151
|
|
|
@@ -452,6 +497,8 @@ export function runDocsCheck(opts) {
|
|
|
452
497
|
let skippedExempt = 0
|
|
453
498
|
// 模糊路径引用数(含 `...` 省略号,跳过校验——FR-1.2)
|
|
454
499
|
let skippedFuzzy = 0
|
|
500
|
+
// 符号锚(P1-2)命中数(计入 total,单独计数供可见性)
|
|
501
|
+
let symbolChecked = 0
|
|
455
502
|
// per-call 缓存:裸名引用的 src/ 全树扫描结果 + 候选文件行数组——100 文档 × 10 裸名引用
|
|
456
503
|
// 此前 = 1000 次全树 walk + 同文件 N 次重读(2026-08-21 性能审查 PERF-5)。仅本次调用
|
|
457
504
|
// 生命周期内有效,不跨调用(防测试流程中途改树后读到陈旧结果)
|
|
@@ -522,6 +569,24 @@ export function runDocsCheck(opts) {
|
|
|
522
569
|
})
|
|
523
570
|
continue
|
|
524
571
|
}
|
|
572
|
+
// 符号锚(P1-2):按「候选文件内存在符号定义行」判定——不校验行号(校验时解析,
|
|
573
|
+
// 行漂移天然免疫),层 2 关键词断言由符号本身命中自动满足。失效只可能是符号拼错/
|
|
574
|
+
// 文件归属错/符号被删改名——均非重锚能修,fix 显式不可自动。
|
|
575
|
+
if (r.kind === 'symbol') {
|
|
576
|
+
let hit = false
|
|
577
|
+
for (const candAbs of candidates) {
|
|
578
|
+
const lines = readLines(candAbs, linesCache)
|
|
579
|
+
if (lines === null) continue
|
|
580
|
+
if (findSymbolDefLine(lines, r.symbol) !== null) { hit = true; break }
|
|
581
|
+
}
|
|
582
|
+
if (hit) { symbolChecked++; continue }
|
|
583
|
+
invalid.push({
|
|
584
|
+
doc: docRel, docLine: r.docLine, ref: r.ref,
|
|
585
|
+
reason: `符号锚未命中:候选文件内无 \`${r.symbol}\` 的定义行(function/const/let/var/class/def 形态)——符号锚不随行号漂移,失效即符号拼错/归属错/已删改名`,
|
|
586
|
+
fix: { fixable: false, reason: '符号锚失效非重锚可修:核对符号拼写或文件归属(改名请同步文档符号)' },
|
|
587
|
+
})
|
|
588
|
+
continue
|
|
589
|
+
}
|
|
525
590
|
// 多候选宽容:逐候选跑层1+层2,任一全过即通过
|
|
526
591
|
const tokens = keywordAssert
|
|
527
592
|
? extractExpectedTokensFromLine(mdLines[r.docLine - 1] || '')
|
|
@@ -558,7 +623,7 @@ export function runDocsCheck(opts) {
|
|
|
558
623
|
}
|
|
559
624
|
}
|
|
560
625
|
|
|
561
|
-
return { ok: invalid.length === 0, total, invalid, warnings, kwChecked, crossRepoSkipped, skippedExempt, skippedFuzzy }
|
|
626
|
+
return { ok: invalid.length === 0, total, invalid, warnings, kwChecked, crossRepoSkipped, skippedExempt, skippedFuzzy, symbolChecked }
|
|
562
627
|
}
|
|
563
628
|
|
|
564
629
|
/**
|