sillyspec 3.33.3 → 3.33.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.
Files changed (55) hide show
  1. package/.claude/skills/sillyspec-config/SKILL.md +48 -0
  2. package/.claude/skills/sillyspec-flow/SKILL.md +4 -0
  3. package/AGENTS.md +1 -1
  4. package/package.json +1 -1
  5. package/src/config-known-failures.js +258 -0
  6. package/src/config-schema.js +28 -0
  7. package/src/config-validate.js +180 -0
  8. package/src/decision-distill.js +26 -5
  9. package/src/decision-redomain.js +5 -4
  10. package/src/docs-check.js +8 -4
  11. package/src/doctor-diagnostics.js +89 -6
  12. package/src/flow-parity.js +22 -2
  13. package/src/flow-preflight.js +67 -0
  14. package/src/flow.js +252 -55
  15. package/src/fr-index.js +7 -3
  16. package/src/git-helper.js +82 -0
  17. package/src/handoff.js +4 -0
  18. package/src/hooks/worktree-guard.js +12 -3
  19. package/src/hot-cache.js +60 -0
  20. package/src/index.js +111 -7
  21. package/src/knowledge-graph.js +82 -2
  22. package/src/knowledge-match.js +18 -2
  23. package/src/knowledge-quicklog.js +17 -2
  24. package/src/knowledge-vector.js +5 -3
  25. package/src/machine-interface.js +34 -13
  26. package/src/progress/change-registry.js +15 -0
  27. package/src/progress/shared.js +4 -0
  28. package/src/progress/stage-machine.js +15 -2
  29. package/src/progress.js +5 -0
  30. package/src/redomain-unmapped.js +204 -0
  31. package/src/redomain.js +5 -3
  32. package/src/review-dispatch.js +208 -47
  33. package/src/run/bg-sync.js +4 -0
  34. package/src/run/code-face-key.js +4 -1
  35. package/src/run/command.js +20 -5
  36. package/src/run/complete-handlers.js +38 -4
  37. package/src/run/complete.js +185 -14
  38. package/src/run/gates.js +30 -1
  39. package/src/run/prompt.js +9 -1
  40. package/src/run/quick-audit.js +4 -2
  41. package/src/run/shared.js +18 -1
  42. package/src/run/stage.js +22 -19
  43. package/src/run/verify-quality-scan.js +51 -0
  44. package/src/runtime-paths.js +3 -0
  45. package/src/sillyhub-mcp/client.js +11 -6
  46. package/src/spawn-settle.js +47 -0
  47. package/src/spec-sync.js +89 -3
  48. package/src/stages/knowledge.js +25 -3
  49. package/src/timing.js +55 -0
  50. package/src/verify-postcheck.js +153 -11
  51. package/src/watcher.js +4 -0
  52. package/src/worktree-apply.js +67 -4
  53. package/src/worktree.js +80 -11
  54. package/src/yaml-scalar-unquote.js +43 -0
  55. package/templates/agents-instruction.md +1 -1
@@ -0,0 +1,48 @@
1
+ ---
2
+ name: sillyspec:config
3
+ description: 用于 SillySpec local.yaml 配置引导。适合用户说"配置下 sillyspec、连接平台、配 MCP、lint 不跑、测试太慢、加豁免"或 agent 需要写 .sillyspec/local.yaml 时。核心原则:默认零配置可用,只写真需要的键,写完必须 config validate 通过。
4
+ ---
5
+
6
+ ## 何时使用
7
+
8
+ - 用户要求配置 SillySpec(平台凭据/MCP/测试/lint/调优键)
9
+ - agent 判断需要写 `.sillyspec/local.yaml`(任何场景——本技能是唯一引导面)
10
+
11
+ ## 第一原则:默认零配置可用(先读这段再动手)
12
+
13
+ **local.yaml 一个键都不写,SillySpec 就是完整可用的。** 全部 ~70 个注册键中只有平台/MCP 凭据族(4 键)真必填,且仅当用户要用平台功能。测试面按变更动态推断、lint 未配置自动跳过归 pre-push、FR 回归层缺省不跑归 CI——都是零配置最优缺省(2026-10-23 起)。
14
+
15
+ **不要「预防性配置」**:不确定要不要写的键就是不写。写了多余键的唯一效果是偏离缺省、增加审计面。
16
+
17
+ ## 必填场景决策表(只有这些场景才动配置)
18
+
19
+ | 场景 | 症状/触发 | 要写的键 | 怎么获得值 |
20
+ |---|---|---|---|
21
+ | 平台同步/看板 | 用户要用 SillyHub 平台 | `platform.url` + `platform.token` | **不手填**:跑 `sillyspec platform connect <url> --token <t>` 自动写 |
22
+ | MCP 派发 | 评审平台单调用/独立审查派发 | `mcp.url` + `mcp.token` | 同上(connect 成对签发) |
23
+ | 评审单调用 | 用户要评审收口一次完成(省 missing→PASS 往返) | `review_inline_wait: 180` | 依赖 mcp 凭据;平台不可用自动回退任务书面零损失 |
24
+ | dirty 门残渣 | 收口反复被已知残渣文件拦(--accept-dirty-gap 成常态) | `dirty_gate_ignore: <前缀1>, <前缀2>` | 看收口输出点名的文件,取其稳定目录前缀 |
25
+ | lint 收口实测 | 用户要 lint 在收口时跑(缺省跳过归 pre-push) | `commands.lint: <命令>` | 从 package.json scripts / Makefile 抄 |
26
+ | 慢套件超时 | 实测被 600s 默认帽判超时失败 | `commands.test_timeout_sec: 2400` | 收口输出会提示 |
27
+ | 全量测试道 | 用户明确要本地跑全量(罕见) | `test_strategy: full` | 仅显式全量时;缺省动态子集已是最优 |
28
+ | 项目特有评审必查面 | 团队有自己的评审红线 | `review_probes:` 段 | 见 `sillyspec config schema` review_probes 节 |
29
+
30
+ 调优键全清单与语义:`sillyspec config schema`(单一真相;example 列可照抄值)。
31
+
32
+ ## 操作流程
33
+
34
+ 1. **看现状**:`sillyspec config cat`(权威路径链解析——worktree 内也能拿到主仓真实文件;不存在=零配置形态,正常)。
35
+ 2. **对决策表**:确认触发场景真实存在;不在表内的场景**不写任何键**,向用户说明缺省行为已覆盖。
36
+ 3. **获得值**:凭据族走 `sillyspec platform connect`(禁止手填 token 到 yaml);其余键从仓库结构(package.json/pyproject/Makefile)或收口输出提示取值。
37
+ 4. **写文件**:编辑 `.sillyspec/local.yaml`(config cat 显示的路径;整文件 YAML,注意缩进用空格、保存为 UTF-8 无 BOM——PS 下用 Set-Content -Encoding utf8NoBOM 或让 CLI 命令代写)。**只写决策表需要的键,逐键照抄 schema 的 example 形态。**
38
+ 5. **硬门验证(必做,不许跳过)**:跑 `sillyspec config validate`
39
+ - **exit 0 才算配置完成**;exit 1 时按输出的错误行逐条修复后重跑。
40
+ - 有「未知顶层键」警告=键名/层级写错,回 schema 核对。
41
+ - 凭据段报缺键=两键要成对(或整段删除回退)。
42
+ - **禁止**在 validate 未通过时向用户宣称配置完成。
43
+
44
+ ## 边界
45
+
46
+ - 禁止写 `.sillyspec/` 之外的 local.yaml(hook 拦截);项目根没有这个文件。
47
+ - 禁止把 token 写进 local.yaml.example 或任何可提交文件(真实 local.yaml 是 gitignored)。
48
+ - known_failures 豁免键不用手改 yaml:`sillyspec config known-failures add|remove`(行级手术保注释)。
@@ -3,6 +3,10 @@ name: sillyspec:flow
3
3
  description: 轻量变更(默认快道)——需求明确的小/中改动。适合用户说"直接改、快速修、顺手调整、改个文案、修个小 bug、更新一个文件、需求很明确、开干吧、把这个改了"。协议链:flow start → 补 spec 面(FR 绑定+design 四节)→ 批准(flow approve / 用户显式 --autopilot)→ 干活(改代码写测试)→ flow done(实测门+测试绑定+patch 留档)。顺序纪律:先答 spec 面、批准后才动码(读代码探究不算动码)。
4
4
  ---
5
5
 
6
+ ## 交互规范
7
+
8
+ **当需要用户从多个选项中做出选择时,必须使用 Claude Code 内置的 AskUserQuestion 工具,将选项以参数传入。** 不要用编号列表让用户手动输入数字。
9
+
6
10
  ## 何时使用
7
11
 
8
12
  - 需求已含决策:改什么、成功标准说得清(能列 1-3 条可验证标准)
package/AGENTS.md CHANGED
@@ -33,7 +33,7 @@ sillyspec flow start --change <YYYY-MM-DD-名> --input "<一句 10-30 字中文
33
33
  1. **禁止绕过本文件规则和 SillySpec 流程**。所有变更走 sillyspec 流程,不裸改裸提交。
34
34
  2. **改代码前必须先说明依据**——依据的文档路径(design.md / 模块文档 / file-lifecycle.md(如有))或现有代码依据,无依据不改。
35
35
  3. **执行顺序**:文档 → 读代码 → 写测试 → 写实现 → 跑测试 → 验收 → 更新文档(轻量道即:FR 绑定+design 四节+用户批准先于动码,读代码探究不算动码)。测试按层适用:纯逻辑先写测试,行为契约断言用户可见行为,视觉样式与纯配置/文档不写测试(记「不适用:理由」,验收靠规范与人工)。
36
- 4. **实证核验再收口**:触及 `src`/`test` 的改动,CLI 会在收口时亲自实测(实测失败阻断收口;纯 doc/配置自动跳过)。测试面按变更动态推断(本变更测试 ∪ FR 关联回归 ∪ import 依赖,runner 自项目结构推断——无需配置);lint 走 `.sillyspec/local.yaml` 的 `commands.lint`。以落盘文件与测试结果为准,不信口头"已完成"。自验口径=变更测试面(新增测试+受影响既有面)+ lint 即收口证据——全量套件归 pre-push 门,勿每变更重复手跑(共享仓全量红先做归属判别,非本变更引入如实留痕不阻断);跑批输出落 `.sillyspec/.runtime/logs/` 日志回看尾部,勿整段吞掉。
36
+ 4. **实证核验再收口**:触及 `src`/`test` 的改动,CLI 会在收口时亲自实测(实测失败阻断收口;纯 doc/配置自动跳过)。测试面按变更动态推断(本变更测试 ∪ FR 关联回归 ∪ import 依赖,runner 自项目结构推断——无需配置);lint 走 `.sillyspec/local.yaml` 的 `commands.lint`(未配置/标 `unavailable` → 收口跳过并留痕理由,归 pre-push/CI 门——pre-push 已跑同款 lint 的仓可标 unavailable 免重复)。以落盘文件与测试结果为准,不信口头"已完成"。自验口径=变更测试面(新增测试+受影响既有面)即收口证据(lint 已配置时一并实测;标 `unavailable` 的仓归 git 钩子/CI 门)——全量套件归 pre-push 门,勿每变更重复手跑(共享仓全量红先做归属判别,非本变更引入如实留痕不阻断);跑批输出落 `.sillyspec/.runtime/logs/` 日志回看尾部,勿整段吞掉。
37
37
  5. **非测试逻辑本身有误时,禁止改测试来"通过"**——修逻辑,不修测试。
38
38
  6. **git hook 拦截提交时禁止跳过**(如 `.husky/pre-push`),修复问题后再提交。
39
39
  7. **代码必须兼容 Windows / Linux / macOS**(路径 / 换行 / 并发都要顾)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sillyspec",
3
- "version": "3.33.3",
3
+ "version": "3.33.5",
4
4
  "description": "SillySpec CLI — 流程状态机,让 AI 严格按步骤来",
5
5
  "icon": "logo.jpg",
6
6
  "homepage": "https://sillyspec.ppdmq.top/",
@@ -0,0 +1,258 @@
1
+ /**
2
+ * config-known-failures —— local.yaml known_failures 键的行级增删查(2026-10-21-p0-p1-hardening FR-07)
3
+ *
4
+ * 背景(dsh z2 实证):agent 用 PS5.1 `Set-Content -Encoding UTF8` 手改 50KB local.yaml 的
5
+ * known_failures 行 → BOM/mojibake 不可逆损坏(510 个 `?`),恢复等用户人工贴备份 2h13m。
6
+ * known_failures 无 CLI 管理面 = 手改场景必然存在。本模块给 `sillyspec config known-failures
7
+ * list|add|remove` 提供纯逻辑核心:
8
+ * - 行级定向手术(块式/流式两种既有形态都认),保留文件其余内容与注释——禁止 js-yaml 整体
9
+ * 重序列化(丢注释,用户文档性受损);
10
+ * - 改前 YAML 全文可解析性门(jsYaml.load 抛错 → 拒改,坏文件上叠加手术只会更坏);
11
+ * - 写入 writeAtomicSync(tmp+rename 原子替换)+ UTF-8 无 BOM 新写。
12
+ * 读法口径对齐 docs-check.js known_failures 键提取(块式 + 流式)——不 import(该模块拖重链)。
13
+ */
14
+ import { existsSync, readFileSync } from 'node:fs'
15
+ import jsYaml from 'js-yaml'
16
+ import { writeAtomicSync } from './fs-atomic.js'
17
+ import { unquoteYamlScalar, unescapeYamlDoubleQuoted } from './yaml-scalar-unquote.js'
18
+
19
+ /** 块式头行:known_failures: 顶格、后接注释或空(docs-check.js:982 同口径) */
20
+ const BLOCK_HEADER_RE = /^known_failures:\s*(?:#.*)?$/
21
+ /** 流式行:known_failures: [a, b](docs-check.js:971 同口径) */
22
+ const INLINE_HEADER_RE = /^known_failures:\s*\[(.*)\]\s*(?:#.*)?$/
23
+ /** 块式条目行:缩进 + 列表杠 + 键(支持裸/双引/单引形态,尾注释剥除) */
24
+ const ITEM_RE = /^(\s+)-\s*(?:"([^"]*)"|'([^']*)'|([^#]*?))\s*(?:#.*)?$/
25
+
26
+ /** 键的裸写安全性:字母数字/下划线/连字符/点/中文——其余(逗号方括号引号#等)写回时加双引号 */
27
+ const BARE_KEY_SAFE_RE = /^[\w.\-\u4e00-\u9fff]+$/
28
+
29
+ /**
30
+ * 纯函数:从 local.yaml 文本提取 known_failures 键清单(块式 + 流式两形态)。
31
+ * 无 known_failures 段 → 空数组;块头无条目(含 `known_failures: []` 流式空)→ 空数组。
32
+ * @param {string} text
33
+ * @returns {string[]}
34
+ */
35
+ export function parseKnownFailureKeys(text) {
36
+ const src = String(text || '')
37
+ const lines = src.split(/\r?\n/)
38
+ // 流式优先(同键只可能一种形态在场;流式正则更具体)。条目规范化走 yaml-scalar-unquote
39
+ // 单点(2026-10-21-known-failures-unescape):双引号条目反转义(renderKey 写侧按规范转义,
40
+ // 读侧不反转义会把 \\. 留成字面双反斜杠——锚定豁免模式静默失配)。
41
+ for (const l of lines) {
42
+ const m = l.match(INLINE_HEADER_RE)
43
+ if (m) {
44
+ return (m[1] || '')
45
+ .split(',')
46
+ .map((s) => s.trim())
47
+ .filter(Boolean)
48
+ .map((s) => unquoteYamlScalar(s))
49
+ }
50
+ }
51
+ const idx = lines.findIndex((l) => BLOCK_HEADER_RE.test(l))
52
+ if (idx === -1) return []
53
+ const keys = []
54
+ for (let i = idx + 1; i < lines.length; i++) {
55
+ const l = lines[i]
56
+ if (/^\S/.test(l)) break // 下一个顶层键——段结束
57
+ const m = l.match(ITEM_RE)
58
+ if (m) {
59
+ // 双引组内文反转义 / 单引组 ''→' / 兜底组(裸值或正则引号组吃不掉的形态,如内嵌
60
+ // '' 的单引号条目——ITEM_RE 单引号组 [^']* 在第二个引号停)统一过 unquoteYamlScalar
61
+ const k = m[2] !== undefined
62
+ ? unescapeYamlDoubleQuoted(m[2])
63
+ : m[3] !== undefined
64
+ ? m[3].replace(/''/g, "'")
65
+ : unquoteYamlScalar(m[4] || '')
66
+ if (k) keys.push(k)
67
+ }
68
+ }
69
+ return keys
70
+ }
71
+
72
+ function renderKey(k) {
73
+ return BARE_KEY_SAFE_RE.test(k) ? k : `"${String(k).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`
74
+ }
75
+
76
+ /**
77
+ * 纯函数:行级手术增删 known_failures 键。
78
+ * - 增:去重后追加(块式→段尾插条目行;流式→行内数组追加;无段→文件尾整段追加);
79
+ * - 删:移除命中条目(块式删行;流式重拼数组);删空后流式写 `[]`、块式头改写 `known_failures: []`;
80
+ * - 改前全文 jsYaml 可解析门:抛错 → { ok:false }(坏文件上拒绝手术);
81
+ * - 文件其余行(含注释/空行/换行风格)逐字节保留——只动 known_failures 相关行。
82
+ * @param {string} text 原 local.yaml 全文
83
+ * @param {{ add?: string[], remove?: string[] }} edit
84
+ * @returns {{ ok: true, next: string, keys: string[] } | { ok: false, error: string }}
85
+ */
86
+ export function applyKnownFailureEdit(text, { add = [], remove = [] } = {}) {
87
+ const src = String(text || '')
88
+ // 坏 YAML 拒改门(FR-07:mojibake/残缺文件上手术只会更坏——先修文件再管理键)
89
+ try {
90
+ const doc = jsYaml.load(src)
91
+ if (doc !== null && typeof doc !== 'object') {
92
+ return { ok: false, error: 'local.yaml 顶层不是映射(YAML 合法但形态异常)——拒改,请人工核对' }
93
+ }
94
+ } catch (e) {
95
+ return { ok: false, error: `local.yaml YAML 解析失败(${String(e && e.message ? e.message : e).split('\n')[0]})——拒改。先从备份/git 历史恢复或重建文件,再用本命令管理 known_failures` }
96
+ }
97
+ const nl = src.includes('\r\n') ? '\r\n' : '\n'
98
+ const lines = src.split(/\r?\n/)
99
+ // trailing newline 语义:原文以换行结尾 → 重组时补回
100
+ const endedWithNl = /\n$/.test(src) || /\r\n$/.test(src)
101
+
102
+ const addSet = [...add].filter(Boolean)
103
+ const removeSet = new Set([...remove].filter(Boolean))
104
+
105
+ // 流式形态
106
+ const inlineIdx = lines.findIndex((l) => INLINE_HEADER_RE.test(l))
107
+ if (inlineIdx !== -1) {
108
+ const cur = parseKnownFailureKeys(src)
109
+ const curSet = new Set(cur)
110
+ const removeHit = cur.filter((k) => removeSet.has(k))
111
+ let next = cur.filter((k) => !removeSet.has(k))
112
+ for (const k of addSet) {
113
+ if (!next.includes(k) && !curSet.has(k)) next.push(k)
114
+ else if (curSet.has(k) && !removeSet.has(k) && !next.includes(k)) next.push(k)
115
+ }
116
+ const inlineMatch = lines[inlineIdx].match(INLINE_HEADER_RE)
117
+ const trailingComment = (inlineMatch[0].match(/#.*$/)?.[0]) || ''
118
+ lines[inlineIdx] = next.length > 0
119
+ ? `known_failures: [${next.map(renderKey).join(', ')}]${trailingComment ? ` ${trailingComment}` : ''}`
120
+ : `known_failures: []${trailingComment ? ` ${trailingComment}` : ''}`
121
+ const out = lines.join(nl) + (endedWithNl ? nl : '')
122
+ return { ok: true, next: out, keys: next, removedKeys: removeHit }
123
+ }
124
+
125
+ // 块式形态
126
+ const blockIdx = lines.findIndex((l) => BLOCK_HEADER_RE.test(l))
127
+ if (blockIdx !== -1) {
128
+ const cur = parseKnownFailureKeys(src)
129
+ const removeHit = cur.filter((k) => removeSet.has(k))
130
+ const keep = cur.filter((k) => !removeSet.has(k))
131
+ const curSet = new Set(cur)
132
+ for (const k of addSet) if (!keep.includes(k) && !curSet.has(k)) keep.push(k)
133
+ // 段边界:块头行之后连续的缩进行(条目/空行/注释行都属于段——条目行重写,其余保留)
134
+ let end = blockIdx + 1
135
+ while (end < lines.length && !/^\S/.test(lines[end])) end++
136
+ // 重组段:保留段内注释/空行的相对位置有过度工程之嫌——known_failures 段语义就是键清单,
137
+ // 直写键条目行(段内注释罕见,注释保真交给其余 99% 的文件内容)
138
+ const section = keep.map((k) => ` - ${renderKey(k)}`)
139
+ lines.splice(blockIdx + 1, end - (blockIdx + 1), ...section)
140
+ if (keep.length === 0) {
141
+ // 删空:块头改流式空(两种 reader 都认 `[]`;裸块头= null 语义靠 reader 兜底,不赌)
142
+ lines[blockIdx] = 'known_failures: []'
143
+ }
144
+ const out = lines.join(nl) + (endedWithNl ? nl : '')
145
+ return { ok: true, next: out, keys: keep, removedCount: removeHit.length, removedKeys: removeHit }
146
+ }
147
+
148
+ // 无段:文件尾整段追加(add 空且 remove 命不中 → 无事可做,原样返回)
149
+ if (addSet.length === 0) return { ok: true, next: src, keys: [] }
150
+ const keys = addSet.filter((k, i) => addSet.indexOf(k) === i)
151
+ const header = src.length === 0 ? '' : (endedWithNl ? '' : nl)
152
+ const out = `${src}${header}known_failures:${nl}${keys.map((k) => ` - ${renderKey(k)}`).join(nl)}${nl}`
153
+ return { ok: true, next: out, keys }
154
+ }
155
+
156
+ /**
157
+ * 读文件列键(CLI list / doctor 联动用)。
158
+ * @param {string} yamlPath
159
+ * @returns {{ ok: boolean, keys?: string[], error?: string, missing?: boolean }}
160
+ */
161
+ export function listKnownFailures(yamlPath) {
162
+ if (!existsSync(yamlPath)) return { ok: false, missing: true, error: `local.yaml 不存在: ${yamlPath}` }
163
+ let text
164
+ try {
165
+ text = readFileSync(yamlPath, 'utf8')
166
+ } catch (e) {
167
+ return { ok: false, error: `读取失败: ${e && e.message ? e.message : e}` }
168
+ }
169
+ try {
170
+ jsYaml.load(text)
171
+ } catch (e) {
172
+ return { ok: false, error: `local.yaml YAML 解析失败(${String(e && e.message ? e.message : e).split('\n')[0]})——键清单不可信,先修复文件(doctor local_yaml_integrity 维度有指引)` }
173
+ }
174
+ return { ok: true, keys: parseKnownFailureKeys(text) }
175
+ }
176
+
177
+ /**
178
+ * 文件级增删(读 → applyKnownFailureEdit → 原子写)。
179
+ * @param {string} yamlPath
180
+ * @param {{ add?: string[], remove?: string[] }} edit
181
+ * @returns {{ ok: boolean, keys?: string[], error?: string }}
182
+ */
183
+ export function mutateKnownFailuresFile(yamlPath, edit) {
184
+ if (!existsSync(yamlPath)) return { ok: false, error: `local.yaml 不存在: ${yamlPath}——先 sillyspec local detect / platform connect 生成,再管理 known_failures` }
185
+ let text
186
+ try {
187
+ text = readFileSync(yamlPath, 'utf8')
188
+ } catch (e) {
189
+ return { ok: false, error: `读取失败: ${e && e.message ? e.message : e}` }
190
+ }
191
+ const r = applyKnownFailureEdit(text, edit)
192
+ if (!r.ok) return r
193
+ if (r.next !== text) writeAtomicSync(yamlPath, r.next)
194
+ return { ok: true, keys: r.keys, removedKeys: r.removedKeys || [] }
195
+ }
196
+
197
+ /**
198
+ * CLI 入口薄壳(index.js config known-failures 接线消费):打印 + 退出码。
199
+ * @param {{ yamlPath: string, exists: boolean, verb: string, keys: string[], json: boolean }} p
200
+ * @returns {number} process.exitCode
201
+ */
202
+ export function runKnownFailuresCommand({ yamlPath, exists, verb, keys = [], json = false }) {
203
+ const want = verb || 'list'
204
+ if (!['list', 'add', 'remove'].includes(want)) {
205
+ console.error(`❌ 未知动作: config known-failures ${want}`)
206
+ console.error('用法: sillyspec config known-failures list | add <key…> | remove <key…>')
207
+ return 2
208
+ }
209
+ if (want !== 'list' && keys.filter(Boolean).length === 0) {
210
+ console.error(`❌ config known-failures ${want} 需要至少一个键参数(空格分隔多个)`)
211
+ return 2
212
+ }
213
+ if (want === 'list') {
214
+ if (!exists) {
215
+ if (json) console.log(JSON.stringify({ cmd: 'config known-failures list', exists: false }))
216
+ else console.error(`📭 未找到 local.yaml(${yamlPath})——无 known_failures 可列`)
217
+ return 1
218
+ }
219
+ const r = listKnownFailures(yamlPath)
220
+ if (!r.ok) {
221
+ if (json) console.log(JSON.stringify({ cmd: 'config known-failures list', ok: false, error: r.error }))
222
+ else console.error(`❌ ${r.error}`)
223
+ return 1
224
+ }
225
+ if (json) {
226
+ console.log(JSON.stringify({ cmd: 'config known-failures list', ok: true, count: r.keys.length, keys: r.keys }))
227
+ } else if (r.keys.length === 0) {
228
+ console.log('📭 known_failures 为空(无豁免键)')
229
+ } else {
230
+ console.log(`📋 known_failures(${r.keys.length} 个):`)
231
+ for (const k of r.keys) console.log(` - ${k}`)
232
+ }
233
+ return 0
234
+ }
235
+ if (!exists) {
236
+ console.error(`📭 未找到 local.yaml(${yamlPath})——先 sillyspec local detect / platform connect 生成`)
237
+ return 1
238
+ }
239
+ const r = mutateKnownFailuresFile(yamlPath, want === 'add' ? { add: keys } : { remove: keys })
240
+ if (!r.ok) {
241
+ console.error(`❌ 拒改:${r.error}`)
242
+ return 1
243
+ }
244
+ const uniq = [...new Set(keys.filter(Boolean))]
245
+ // remove 诚实文案(2026-10-21-known-failures-unescape FR-02):仅未命中键注记静默忽略——
246
+ // 旧文案恒打「已移除…(未命中键静默忽略)」,全命中时也自称可能有未命中,误导排查。
247
+ const removedSet = new Set(r.removedKeys || [])
248
+ const missed = want === 'remove' ? uniq.filter((k) => !removedSet.has(k)) : []
249
+ if (json) console.log(JSON.stringify({ cmd: `config known-failures ${want}`, ok: true, changed: want === 'remove' ? [...removedSet] : uniq, missed, keys: r.keys }))
250
+ else {
251
+ const note = want === 'remove'
252
+ ? (missed.length > 0 ? `(未命中键静默忽略:${missed.join('、')})` : '')
253
+ : ''
254
+ console.log(`✅ known_failures 已${want === 'add' ? '添加' : '移除'}:${(want === 'remove' ? [...removedSet] : uniq).join('、')}${note}——现共 ${r.keys.length} 个`)
255
+ console.log(` 校验:sillyspec config known-failures list`)
256
+ }
257
+ return 0
258
+ }
@@ -122,6 +122,15 @@ export const LOCAL_YAML_SCHEMA = {
122
122
  keys: [
123
123
  { path: 'modules.<name>.path', type: 'string', optional: true, status: 'live', readers: ['extractModules (src/verify-postcheck.js)', 'parseLocalYamlModules (src/plan-postcheck.js)', 'extractModulePaths (src/worktree-deps.js)'], desc: '子模块目录路径(相对仓库根)。', example: 'frontend/' },
124
124
  { path: 'modules.<name>.test', type: 'string', optional: true, status: 'deprecated', readers: ['extractModules (src/verify-postcheck.js,退役提示用)'], desc: '【2026-09-26-dynamic-test-inference 退役,不再消费】测试面按变更动态推断、runner 自项目结构(pyproject/uv.lock/package.json 就近祖先)推断。模块级 flaky 处置迁 known_failures。块在场时 CLI 打印退役指引。', example: 'cd frontend && pnpm test' },
125
+ { path: 'fr_unmapped_baseline', type: 'integer', optional: true, status: 'live', readers: ['unmapped 大池消音(src/fr-index.js)'], desc: 'unmapped 域 FR 条数消音基线(跨仓历史堆积已清算时设当前值;增量 >10 才警告,缺省 0=无基线恒警告行为)。', example: '720' },
126
+ ],
127
+ },
128
+ {
129
+ id: 'repos',
130
+ title: '跨仓注册表',
131
+ note: '仓键 → 本地工作目录。注册后参与跨仓冻结面/对账/未标注交付探测与跨仓 worktree 落位;条目两种形态:字符串路径或 {path, worktree} 对象(worktree=跨仓 worktree 落位根,优先级见 worktree.crossPlacement)。local.yaml 本就 gitignored,真实本地路径不入库。',
132
+ keys: [
133
+ { path: 'repos.<repo键>', type: 'string', optional: true, status: 'live', readers: ['跨仓冻结/对账(src/cross-repo-reconcile.js)、注册探测(src/local-register.js)、跨仓 worktree(src/worktree-cross.js)'], desc: '仓键 → 该仓本地工作目录绝对路径(或 {path, worktree} 对象形态)。', example: 'shared-lib: C:/path/to/shared-lib' },
125
134
  ],
126
135
  },
127
136
  {
@@ -131,6 +140,7 @@ export const LOCAL_YAML_SCHEMA = {
131
140
  keys: [
132
141
  { path: 'verify.test_rerun', type: 'enum', values: ['incremental', 'full'], optional: true, status: 'live', readers: ['readTestRerunConfig (src/verify-postcheck.js — dynamic-subset 分支增量档)'], desc: '实测门失败后重跑档:incremental(缺省)= 前轮失败后,下轮只跑「失败批测试文件 ∪ 自失败基线以来变更文件」的三源推断面(未触碰绿面复用,返工轮分钟级);full = 恒全子集重跑(2026-10-07 前现状,保守项目用)。env SILLYSPEC_TEST_RERUN 优先。仅作用 dynamic-subset 档(显式 test_strategy: full 不受影响)。', example: 'verify: { test_rerun: full }' },
133
142
  { path: 'test_strategy', type: 'enum', values: ['full', 'module', 'skip', 'evidence-auto'], optional: true, status: 'live', readers: ['extractTestStrategy (src/verify-postcheck.js)', 'resolveTestStrategy (src/verify-postcheck.js)'], desc: '【2026-09-26-dynamic-test-inference 起】full=全量(commands.test 在场生效,否则结构推断);module=已退役折算动态子集(超集覆盖:变更文件面收窄 + FR 关联回归 + import 依赖);skip=真跳过(R-07 不变);evidence-auto=按 module-impact.md 推荐(行为→动态子集、文档/门禁→skip)。缺省=动态子集:三源并集非空实测、空则不硬跑全量(防超时/预存失败面)。', example: 'full' },
143
+ { path: 'fr_regress_cap', type: 'string', optional: true, status: 'live', readers: ['readFrRegressCap (src/verify-postcheck.js)'], desc: '【2026-10-23-fr-regress-default-off 缺省翻转】FR 关联回归层本地实测开关:缺省(不写)=不跑(归 pre-push/CI 门,收口披露行留痕);N(1-2000)=前 N 个绑定文件进实测面;all=全跑旧行为显式恢复;off=同缺省。', example: '40' },
134
144
  ],
135
145
  },
136
146
  {
@@ -188,6 +198,12 @@ export const LOCAL_YAML_SCHEMA = {
188
198
  { path: 'flow.mode', type: 'enum', values: ['thin', 'legacy'], optional: true, status: 'live', readers: ['readFlowConfig (src/flow.js)'], desc: '2-调用轻量协议开关:thin(缺省——2026-09-25-thin-default-flip 翻转,入口归一:常规变更默认轻量跑道,需求不清晰 CLI 拦下指路头脑风暴预段,实测失败自动升厚)= flow start/done 轻量跑道;legacy = 显式回旧道(run <stage> 全族行为逐字不动,flow start 拒跑并指路)。切回一行 yaml。', example: 'thin' },
189
199
  { path: 'flow.edit_ratio_threshold', type: 'number', optional: true, status: 'live', readers: ['readFlowConfig (src/flow.js)'], desc: '机器稿改写比例路由阈值(amend 通道计算的行级 editRatio 超阈 → 厚档提示 route_hint)。缺省 0.5。', example: '0.5' },
190
200
  { path: 'flow.edit_ratio_enforcement', type: 'enum', values: ['advisory', 'block'], optional: true, status: 'live', readers: ['readFlowConfig (src/flow.js)'], desc: 'editRatio 超阈的执行档:advisory(缺省——测绿可轻量档过,只提示+遥测)| block(超阈阻断 flow done)。用户裁定 advisory 定案,dogfood 后可按遥测翻 block。', example: 'advisory' },
201
+ // 以下四键为根级标量(无点 path——2026-10-23-config-validate-skill 补登:此前散落
202
+ // 各 reader 内联正则、schema 发现面缺失;校验器按注册面跟随)
203
+ { path: 'turbo', type: 'enum', values: ['docs-auto'], optional: true, status: 'live', readers: ['readFlowConfig (src/flow.js)'], desc: '【2026-10-23-turbo-docs-auto】纯文档面常设极速授权档:docs-auto=CLI 按提交∪脏面自动判纯文档(代码分类器同源)降档 turbo(实测/评审/UI 探针跳过,授权源留痕);不写=关(--turbo 旗标档不受影响,agent 仍不可自启)。', example: 'docs-auto' },
204
+ { path: 'review_inline_wait', type: 'integer', optional: true, status: 'live', readers: ['readFlowConfig (src/flow.js)'], desc: '【2026-10-23-review-inline-dispatch】薄道评审平台单调用通道等待预算秒数(1-600,钳 300s 帽):>0 时 flow done 评审缺席分支经 SillyHub 派发 readOnly worker 有界轮询回收、同调用登记(missing→PASS 往返消灭);任何失败回退任务书面零损失;不写/坏值=关。', example: '180' },
205
+ { path: 'dirty_gate_ignore', type: 'string', optional: true, status: 'live', readers: ['readDirtyGateIgnore (src/flow-parity.js)'], desc: '【2026-10-23-dirty-gate-ignore】dirty 冻结缺口门的已知残渣前缀忽略清单(单行逗号分隔、posix 前缀匹配,module_gate_ignore 同款):命中面从缺口剔除并每轮 ℹ️ 留痕;三态 fail-soft 未配置=现状。', example: 'quicklog, test/.sillyspec' },
206
+ { path: 'module_gate_ignore', type: 'string', optional: true, status: 'live', readers: ['readFlowConfig (src/flow.js)'], desc: '【2026-10-12-thin-module-gate-and-feed】模块图未登记目录软门的固定忽略清单(单行逗号、目录前缀、posix)——合法未登记面(根级脚本/配置等)免登记签收。', example: 'scripts, .github' },
191
207
  ],
192
208
  },
193
209
  {
@@ -426,6 +442,18 @@ dispatch:
426
442
  # mode: thin # thin=flow start/done 轻量跑道(协议必需交互=2)| legacy=既有 run <stage> 全族(回滚一行)
427
443
  # edit_ratio_threshold: 0.5 # 机器稿改写比例路由阈值(超阈提示厚档;缺省 0.5)
428
444
  # edit_ratio_enforcement: advisory # advisory=测绿可轻量过只提示(缺省)| block=超阈阻断
445
+ #
446
+ # ── 收口调优(根级键,缺省零配置即用——注释态=缺省已最优,确要调优再解注释)──
447
+ # turbo: docs-auto # 纯文档面自动极速档(实测/评审/UI 探针跳过,授权源留痕);不写=关
448
+ # review_inline_wait: 180 # 评审平台单调用等待秒(1-600,需 SillyHub);不写=评审任务书面模式
449
+ # dirty_gate_ignore: quicklog, test/.sillyspec # dirty 门已知残渣前缀(单行逗号、posix 前缀)
450
+ # module_gate_ignore: scripts # 模块图软门固定忽略目录(单行逗号、前缀匹配)
451
+ # fr_regress_cap: 40 # FR 回归层本地开关:缺省不跑归 pre-push/CI 门;N=前 N 个;all=全跑旧行为
452
+ # fr_unmapped_baseline: 720 # unmapped 域 FR 消音基线(跨仓历史堆积已清算时设)
453
+ #
454
+ # ── 跨仓注册表(注册后参与跨仓冻结/对账;local.yaml gitignored 故真实路径不入库)──
455
+ # repos:
456
+ # shared-lib: C:/path/to/shared-lib # 或对象形态 {path: …, worktree: <跨仓落位根>}
429
457
 
430
458
  # ── 阶段说明书下发与会话墙(R16 减负批次)──
431
459
  # stage:
@@ -0,0 +1,180 @@
1
+ /**
2
+ * config-validate.js — local.yaml 逐键校验器(2026-10-23-config-validate-skill)
3
+ *
4
+ * 用户配置哲学(2026-10-23 裁决原话):「能不要配置的就别让用户配置,默认都集成好;实在
5
+ * 不好配置的要生成一个技能去引导 agent 怎么填写,并且 agent 写完要 CLI 验证通过才行」。
6
+ * 本模块是那句「CLI 验证」:sillyspec config validate 经 config-cat 权威路径链解析真实
7
+ * local.yaml(worktree 感知),按 LOCAL_YAML_SCHEMA 单一源逐键校验——校验只消费不生产
8
+ * (schema 注册面新增键照常跟随,无需改本模块)。
9
+ *
10
+ * 判级:错误(exit 1)=YAML 解析失败/顶层非映射/段非映射/枚举越界/结构性类型错(标量位
11
+ * 收到映射或数组)/不可强转标量(number/integer 键收 "abc" 类纯文本、boolean 键收非
12
+ * true|false 串)/段内必填键缺失(凭据族两键齐全才生效)。警告(exit 0)=未知键(可能
13
+ * 无效果)。info(exit 0)=deprecated 键在场/空文件。标量强转语义(评审 P1 修正):数字
14
+ * 串与数字同义("2400"≡2400——YAML 弱型等价),但不可强转的错拼类必拦——防「宽容」变成
15
+ * 放过 FR 承诺要拦的坏值。缺文件=零配置可用 info exit 0(69 键仅 4 凭据键真必填且仅平台
16
+ * 用户——零配置契约锚)。
17
+ */
18
+ import yaml from 'js-yaml'
19
+ import { readFileSync } from 'fs'
20
+ import { LOCAL_YAML_SCHEMA } from './config-schema.js'
21
+ import { resolveLocalYaml } from './config-cat.js'
22
+
23
+ /** schema 注册面索引:path→def、已知顶层键(段 id ∪ 无点 path)、含占位段的段(免未知键误报)。 */
24
+ function buildRegistry() {
25
+ const byPath = new Map()
26
+ const sectionIds = new Set()
27
+ const rootScalars = new Set()
28
+ const placeholderSections = new Set()
29
+ for (const sec of LOCAL_YAML_SCHEMA.sections || []) {
30
+ sectionIds.add(sec.id)
31
+ for (const k of sec.keys || []) {
32
+ byPath.set(k.path, k)
33
+ if (!k.path.includes('.')) rootScalars.add(k.path)
34
+ if (/<[^>]+>/.test(k.path)) placeholderSections.add(sec.id)
35
+ }
36
+ }
37
+ return { byPath, sectionIds, knownTop: new Set([...sectionIds, ...rootScalars]), placeholderSections }
38
+ }
39
+
40
+ const kindOf = (v) => (v === null || v === undefined ? 'null' : Array.isArray(v) ? 'array' : typeof v)
41
+
42
+ /** 类型检查(评审 P1 修正口径:可强转宽容、不可强转判错)——YAML 标量弱型下 24 与 "24"
43
+ * 同义(number/integer 收数字串合法),但 "abc" 这类不可强转错拼必须拦(FR-01 场景);
44
+ * boolean 收非 true/false 串判错;string 收任意标量;结构错配(标量位收映射/数组)恒错。 */
45
+ function checkType(def, v, path, errors) {
46
+ const kind = kindOf(v)
47
+ if (kind === 'null') return
48
+ const scalarSpot = ['string', 'number', 'integer', 'enum', 'boolean', 'json'].includes(def.type)
49
+ if (scalarSpot && (kind === 'object' || kind === 'array')) {
50
+ errors.push(`「${path}」期望${def.type === 'enum' ? `枚举(${(def.values || []).join('|')})` : def.type}标量,实得 ${kind}——嵌套层级写错(该键是叶子值不是段)`)
51
+ return
52
+ }
53
+ if (def.type === 'object' && kind !== 'object') {
54
+ errors.push(`「${path}」期望键值映射,实得 ${kind}`)
55
+ return
56
+ }
57
+ if (def.type === 'array' && kind !== 'array') {
58
+ errors.push(`「${path}」期望列表,实得 ${kind}`)
59
+ return
60
+ }
61
+ if (def.type === 'enum' && !(def.values || []).map(String).includes(String(v))) {
62
+ errors.push(`「${path}」取值越界:${JSON.stringify(v)}(合法:${(def.values || []).join('|')})`)
63
+ return
64
+ }
65
+ if ((def.type === 'number' || def.type === 'integer') && kind === 'string' && !/^-?\d+(\.\d+)?$/.test(v.trim())) {
66
+ errors.push(`「${path}」期望${def.type === 'integer' ? '整数' : '数字'},实得字符串 ${JSON.stringify(v)}(不可强转——数字串可,纯文本不可)`)
67
+ return
68
+ }
69
+ if (def.type === 'integer' && kind === 'string' && !/^-?\d+$/.test(v.trim())) {
70
+ errors.push(`「${path}」期望整数,实得 ${JSON.stringify(v)}`)
71
+ return
72
+ }
73
+ if (def.type === 'boolean' && kind === 'string' && !/^(true|false)$/i.test(v.trim())) {
74
+ errors.push(`「${path}」期望布尔(true/false),实得 ${JSON.stringify(v)}`)
75
+ }
76
+ }
77
+
78
+ /**
79
+ * 校验 local.yaml 文本(纯函数)。@returns {{errors:string[], warnings:string[], infos:string[], keysChecked:number}}
80
+ */
81
+ export function validateLocalYamlContent(text) {
82
+ const { byPath, knownTop, placeholderSections } = buildRegistry()
83
+ const errors = [], warnings = [], infos = []
84
+ let doc
85
+ try { doc = yaml.load(text) } catch (e) {
86
+ return { errors: [`YAML 解析失败:${(e && e.message) || e}——agent 手改易犯缩进/Tab/编码错(doctor 的 local_yaml_integrity 维度查编码)`], warnings, infos, keysChecked: 0 }
87
+ }
88
+ if (doc == null) doc = {}
89
+ const topKind = kindOf(doc)
90
+ if (topKind !== 'object') {
91
+ return { errors: [`local.yaml 顶层必须是键值映射(实得 ${topKind})`], warnings, infos, keysChecked: 0 }
92
+ }
93
+ const sectionIds = new Set((LOCAL_YAML_SCHEMA.sections || []).map((s) => s.id))
94
+ let checked = 0
95
+ const checkDef = (def, v, path) => {
96
+ checked++
97
+ checkType(def, v, path, errors)
98
+ if (def.status === 'deprecated') infos.push(`「${path}」已退役(${String(def.desc || '').slice(0, 80)}…)——新配置勿再写`)
99
+ }
100
+ for (const [k, v] of Object.entries(doc)) {
101
+ if (!knownTop.has(k)) {
102
+ warnings.push(`未知顶层键「${k}」——sillyspec config schema 可查全部已知键(未知键大概率无效果;确认拼写与层级)`)
103
+ continue
104
+ }
105
+ if (sectionIds.has(k) && byPath.has(k) === false) {
106
+ // 纯段(如 project/commands):校验段内键。两种真实形态:键值映射(commands.lint)
107
+ // 与列表-of-对象(review_probes 列表项字段建模为 sec.field 点路径——本仓 dogfood 实证)。
108
+ if (v == null) continue
109
+ const vKind = kindOf(v)
110
+ if (vKind !== 'object' && vKind !== 'array') { errors.push(`段「${k}」必须是键值映射或列表(实得 ${vKind})`); continue }
111
+ const entries = vKind === 'array'
112
+ ? (v.every((it) => kindOf(it) === 'object') ? v.flatMap((it) => Object.entries(it)) : Object.entries(v[0] || {}))
113
+ : Object.entries(v)
114
+ for (const [ik, iv] of entries) {
115
+ const path = `${k}.${ik}`
116
+ const def = byPath.get(path)
117
+ if (!def) {
118
+ // 占位段(modules.<name>.path 族):子键名是用户自定义 id,注册面无法穷举——
119
+ // 未知子键不警告(占位形态本身合法),具体叶子键校验交给占位 path 前缀匹配不到即跳过
120
+ if (!placeholderSections.has(k)) warnings.push(`「${path}」不在注册键表(可能拼错/层级错,或新版键未同步 schema——sillyspec config schema 核对)`)
121
+ continue
122
+ }
123
+ checkDef(def, iv, path)
124
+ }
125
+ } else {
126
+ // 根级标量键(test_strategy/module_gate_ignore/…)
127
+ const def = byPath.get(k)
128
+ if (!def) { warnings.push(`键「${k}」不在注册键表`); continue }
129
+ checkDef(def, v, k)
130
+ }
131
+ }
132
+ // 段内必填闭合(required:true 键所在段在场则须齐全——mcp/platform 凭据族两键成对才生效)
133
+ for (const sec of LOCAL_YAML_SCHEMA.sections || []) {
134
+ if (!(sec.id in doc) || doc[sec.id] == null || kindOf(doc[sec.id]) !== 'object') continue
135
+ for (const kd of sec.keys || []) {
136
+ if (kd.required !== true || !kd.path.startsWith(`${sec.id}.`)) continue
137
+ const leaf = kd.path.slice(sec.id.length + 1)
138
+ if (!(leaf in doc[sec.id])) {
139
+ errors.push(`段「${sec.id}」缺必填键「${leaf}」——${sec.id === 'mcp' || sec.id === 'platform' ? '凭据两键齐全才生效(缺半整段回退缺省/env);补齐或整段删除' : '必填键'}`)
140
+ }
141
+ }
142
+ }
143
+ if (checked === 0 && errors.length === 0) infos.push('local.yaml 在场但无任何已注册键(空段/仅注释)——等于零配置形态')
144
+ return { errors, warnings, infos, keysChecked: checked }
145
+ }
146
+
147
+ /**
148
+ * config validate 子命令体:路径链解析(config-cat 同源,worktree 感知)→ 校验 → 人类/--json
149
+ * 双态渲染。退出码:0=通过(可含警告/info),1=有错误;缺文件=零配置 info exit 0。
150
+ */
151
+ export function cmdConfigValidate({ dir, specBase = null, json = false } = {}) {
152
+ const r = resolveLocalYaml(dir, { specBase })
153
+ if (!r.exists) {
154
+ const payload = { file: null, exists: false, passed: true, errors: [], warnings: [], infos: ['local.yaml 不存在——零配置可用(全部键走缺省;仅平台/MCP 凭据族真必填)'], keysChecked: 0 }
155
+ if (json) console.log(JSON.stringify(payload, null, 2))
156
+ else console.log('✅ local.yaml 不存在——零配置可用(全部键走缺省;仅平台/MCP 凭据族真必填,需要时 sillyspec platform connect 一键写入)。\n 需要调优时:sillyspec config schema 查键 + sillyspec-config 技能引导;写完跑 sillyspec config validate 验证。')
157
+ return 0
158
+ }
159
+ let v
160
+ try { v = validateLocalYamlContent(readFileSync(r.path, 'utf8')) } catch (e) {
161
+ // 读盘竞态兜底(评审 P3 清偿):resolve 与 read 之间文件消失/被锁——优雅报错不裸崩
162
+ if (json) console.log(JSON.stringify({ file: r.path, exists: true, passed: false, errors: [`local.yaml 读取失败:${(e && e.message) || e}(可能刚被移动/删除——重跑)`], warnings: [], infos: [], keysChecked: 0 }, null, 2))
163
+ else console.error(`❌ local.yaml 读取失败:${(e && e.message) || e}(可能刚被移动/删除——重跑 sillyspec config validate)`)
164
+ return 1
165
+ }
166
+ const passed = v.errors.length === 0
167
+ if (json) {
168
+ console.log(JSON.stringify({ file: r.path, exists: true, passed, ...v }, null, 2))
169
+ } else {
170
+ console.log(`${passed ? '✅' : '❌'} local.yaml 校验${passed ? '通过' : '未通过'}(${r.path}${r.source ? `,来源 ${r.source}` : ''};已查 ${v.keysChecked} 个注册键)`)
171
+ for (const e of v.errors) console.error(` 错误:${e}`)
172
+ for (const w of v.warnings) console.warn(` 警告:${w}`)
173
+ for (const i of v.infos) console.log(` ℹ️ ${i}`)
174
+ if (passed && v.warnings.length === 0 && v.errors.length === 0) console.log(' 无警告无未知键。')
175
+ if (!passed) console.error(' 修复后重跑:sillyspec config validate(exit 0 才算配置完成——sillyspec-config 技能硬门)')
176
+ }
177
+ return passed ? 0 : 1
178
+ }
179
+
180
+ export default { validateLocalYamlContent, cmdConfigValidate }