sillyspec 3.27.2 → 3.27.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 (221) hide show
  1. package/.claude/skills/sillyspec-execute/SKILL.md +8 -0
  2. package/.claude/skills/sillyspec-quick/SKILL.md +2 -1
  3. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-archive/SKILL.md +87 -0
  4. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-auto/SKILL.md +93 -0
  5. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -0
  6. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-commit/SKILL.md +67 -0
  7. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-continue/SKILL.md +47 -0
  8. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-doctor/SKILL.md +76 -0
  9. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-execute/SKILL.md +214 -0
  10. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-explore/SKILL.md +102 -0
  11. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-export/SKILL.md +57 -0
  12. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-init/SKILL.md +177 -0
  13. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-knowledge/SKILL.md +270 -0
  14. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-plan/SKILL.md +156 -0
  15. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-quick/SKILL.md +116 -0
  16. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-resume/SKILL.md +70 -0
  17. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-scan/SKILL.md +79 -0
  18. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-state/SKILL.md +64 -0
  19. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-status/SKILL.md +54 -0
  20. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-verify/SKILL.md +87 -0
  21. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-workspace/SKILL.md +175 -0
  22. package/.claude/worktrees/sillyspec-data-model-research/.gitattributes +2 -0
  23. package/.claude/worktrees/sillyspec-data-model-research/.husky/pre-push +20 -0
  24. package/.claude/worktrees/sillyspec-data-model-research/README.md +203 -0
  25. package/.claude/worktrees/sillyspec-data-model-research/SKILL.md +91 -0
  26. package/.claude/worktrees/sillyspec-data-model-research/bin/sillyspec.js +2 -0
  27. package/.claude/worktrees/sillyspec-data-model-research/logo.jpg +0 -0
  28. package/.claude/worktrees/sillyspec-data-model-research/package-lock.json +977 -0
  29. package/.claude/worktrees/sillyspec-data-model-research/package.json +39 -0
  30. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/assets/index-Bq_Z2hne.js +7446 -0
  31. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -0
  32. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/favicon.jpg +0 -0
  33. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/index.html +16 -0
  34. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/logo.jpg +0 -0
  35. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/prototype-dashboard.html +836 -0
  36. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/prototype-overview.html +256 -0
  37. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/package.json +25 -0
  38. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/executor.js +95 -0
  39. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/index.js +679 -0
  40. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/parser.js +526 -0
  41. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/watcher.js +357 -0
  42. package/.claude/worktrees/sillyspec-data-model-research/src/agent-session-log.js +803 -0
  43. package/.claude/worktrees/sillyspec-data-model-research/src/change-list.js +290 -0
  44. package/.claude/worktrees/sillyspec-data-model-research/src/change-risk-profile.js +270 -0
  45. package/.claude/worktrees/sillyspec-data-model-research/src/check-primitives.js +65 -0
  46. package/.claude/worktrees/sillyspec-data-model-research/src/classify-change.js +109 -0
  47. package/.claude/worktrees/sillyspec-data-model-research/src/commit-suggest.js +202 -0
  48. package/.claude/worktrees/sillyspec-data-model-research/src/config-cat.js +88 -0
  49. package/.claude/worktrees/sillyspec-data-model-research/src/config-schema.js +328 -0
  50. package/.claude/worktrees/sillyspec-data-model-research/src/constants.js +97 -0
  51. package/.claude/worktrees/sillyspec-data-model-research/src/contract-matrix.js +480 -0
  52. package/.claude/worktrees/sillyspec-data-model-research/src/datetime.js +20 -0
  53. package/.claude/worktrees/sillyspec-data-model-research/src/db-engine.js +55 -0
  54. package/.claude/worktrees/sillyspec-data-model-research/src/db.js +381 -0
  55. package/.claude/worktrees/sillyspec-data-model-research/src/decision-distill.js +517 -0
  56. package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/backends/local-agent.js +155 -0
  57. package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/backends/sillyhub-mcp.js +245 -0
  58. package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/probe.js +246 -0
  59. package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/strategy.js +121 -0
  60. package/.claude/worktrees/sillyspec-data-model-research/src/docs-check.js +825 -0
  61. package/.claude/worktrees/sillyspec-data-model-research/src/docs-debt.js +393 -0
  62. package/.claude/worktrees/sillyspec-data-model-research/src/docs-gate.js +107 -0
  63. package/.claude/worktrees/sillyspec-data-model-research/src/doctor-diagnostics.js +1099 -0
  64. package/.claude/worktrees/sillyspec-data-model-research/src/endpoint-extractor.js +420 -0
  65. package/.claude/worktrees/sillyspec-data-model-research/src/foreign-declared.js +78 -0
  66. package/.claude/worktrees/sillyspec-data-model-research/src/fs-atomic.js +73 -0
  67. package/.claude/worktrees/sillyspec-data-model-research/src/git-helper.js +150 -0
  68. package/.claude/worktrees/sillyspec-data-model-research/src/hooks/claude-pre-tool-use.cjs +130 -0
  69. package/.claude/worktrees/sillyspec-data-model-research/src/hooks/worktree-guard.js +892 -0
  70. package/.claude/worktrees/sillyspec-data-model-research/src/index.js +2798 -0
  71. package/.claude/worktrees/sillyspec-data-model-research/src/init.js +626 -0
  72. package/.claude/worktrees/sillyspec-data-model-research/src/knowledge-match.js +231 -0
  73. package/.claude/worktrees/sillyspec-data-model-research/src/local-detect.js +128 -0
  74. package/.claude/worktrees/sillyspec-data-model-research/src/local-register.js +104 -0
  75. package/.claude/worktrees/sillyspec-data-model-research/src/machine-interface.js +543 -0
  76. package/.claude/worktrees/sillyspec-data-model-research/src/migrate.js +127 -0
  77. package/.claude/worktrees/sillyspec-data-model-research/src/module-impact.js +166 -0
  78. package/.claude/worktrees/sillyspec-data-model-research/src/module-resolve.js +222 -0
  79. package/.claude/worktrees/sillyspec-data-model-research/src/modules.js +580 -0
  80. package/.claude/worktrees/sillyspec-data-model-research/src/progress/change-registry.js +336 -0
  81. package/.claude/worktrees/sillyspec-data-model-research/src/progress/consistency-doctor.js +355 -0
  82. package/.claude/worktrees/sillyspec-data-model-research/src/progress/shared.js +36 -0
  83. package/.claude/worktrees/sillyspec-data-model-research/src/progress/stage-machine.js +594 -0
  84. package/.claude/worktrees/sillyspec-data-model-research/src/progress/step-store.js +265 -0
  85. package/.claude/worktrees/sillyspec-data-model-research/src/progress.js +1304 -0
  86. package/.claude/worktrees/sillyspec-data-model-research/src/quick-recommend.js +115 -0
  87. package/.claude/worktrees/sillyspec-data-model-research/src/quicklog.js +852 -0
  88. package/.claude/worktrees/sillyspec-data-model-research/src/review-tier.js +77 -0
  89. package/.claude/worktrees/sillyspec-data-model-research/src/run/command.js +1595 -0
  90. package/.claude/worktrees/sillyspec-data-model-research/src/run/complete-handlers.js +1552 -0
  91. package/.claude/worktrees/sillyspec-data-model-research/src/run/complete.js +1259 -0
  92. package/.claude/worktrees/sillyspec-data-model-research/src/run/concurrent-detect.js +156 -0
  93. package/.claude/worktrees/sillyspec-data-model-research/src/run/gates.js +1084 -0
  94. package/.claude/worktrees/sillyspec-data-model-research/src/run/multi-repo-context.js +208 -0
  95. package/.claude/worktrees/sillyspec-data-model-research/src/run/next.js +128 -0
  96. package/.claude/worktrees/sillyspec-data-model-research/src/run/prompt.js +843 -0
  97. package/.claude/worktrees/sillyspec-data-model-research/src/run/quick-audit.js +186 -0
  98. package/.claude/worktrees/sillyspec-data-model-research/src/run/scan-profile.js +269 -0
  99. package/.claude/worktrees/sillyspec-data-model-research/src/run/shared.js +1562 -0
  100. package/.claude/worktrees/sillyspec-data-model-research/src/run/stage.js +584 -0
  101. package/.claude/worktrees/sillyspec-data-model-research/src/run.js +23 -0
  102. package/.claude/worktrees/sillyspec-data-model-research/src/scan-diff.js +388 -0
  103. package/.claude/worktrees/sillyspec-data-model-research/src/scan-postcheck.js +512 -0
  104. package/.claude/worktrees/sillyspec-data-model-research/src/scan-staleness.js +95 -0
  105. package/.claude/worktrees/sillyspec-data-model-research/src/setup.js +414 -0
  106. package/.claude/worktrees/sillyspec-data-model-research/src/sillyhub-mcp/client.js +597 -0
  107. package/.claude/worktrees/sillyspec-data-model-research/src/sillyhub-mcp/config.js +62 -0
  108. package/.claude/worktrees/sillyspec-data-model-research/src/spec-dir-typo.js +50 -0
  109. package/.claude/worktrees/sillyspec-data-model-research/src/spec-sync.js +358 -0
  110. package/.claude/worktrees/sillyspec-data-model-research/src/stage-contract-engine.js +171 -0
  111. package/.claude/worktrees/sillyspec-data-model-research/src/stage-contract-spec.js +550 -0
  112. package/.claude/worktrees/sillyspec-data-model-research/src/stage-contract.js +889 -0
  113. package/.claude/worktrees/sillyspec-data-model-research/src/stage-review.js +597 -0
  114. package/.claude/worktrees/sillyspec-data-model-research/src/stages/archive.js +199 -0
  115. package/.claude/worktrees/sillyspec-data-model-research/src/stages/brainstorm-auto.js +236 -0
  116. package/.claude/worktrees/sillyspec-data-model-research/src/stages/brainstorm.js +617 -0
  117. package/.claude/worktrees/sillyspec-data-model-research/src/stages/cmd-existence.js +145 -0
  118. package/.claude/worktrees/sillyspec-data-model-research/src/stages/doctor.js +540 -0
  119. package/.claude/worktrees/sillyspec-data-model-research/src/stages/execute.js +1275 -0
  120. package/.claude/worktrees/sillyspec-data-model-research/src/stages/explore.js +43 -0
  121. package/.claude/worktrees/sillyspec-data-model-research/src/stages/index.js +29 -0
  122. package/.claude/worktrees/sillyspec-data-model-research/src/stages/knowledge.js +514 -0
  123. package/.claude/worktrees/sillyspec-data-model-research/src/stages/plan-postcheck.js +1344 -0
  124. package/.claude/worktrees/sillyspec-data-model-research/src/stages/plan.js +650 -0
  125. package/.claude/worktrees/sillyspec-data-model-research/src/stages/quick.js +167 -0
  126. package/.claude/worktrees/sillyspec-data-model-research/src/stages/scan.js +608 -0
  127. package/.claude/worktrees/sillyspec-data-model-research/src/stages/status.js +64 -0
  128. package/.claude/worktrees/sillyspec-data-model-research/src/stages/verify.js +246 -0
  129. package/.claude/worktrees/sillyspec-data-model-research/src/sync.js +1470 -0
  130. package/.claude/worktrees/sillyspec-data-model-research/src/task-review.js +1498 -0
  131. package/.claude/worktrees/sillyspec-data-model-research/src/taskcard.js +210 -0
  132. package/.claude/worktrees/sillyspec-data-model-research/src/verify-postcheck.js +1718 -0
  133. package/.claude/worktrees/sillyspec-data-model-research/src/verify-probes.js +277 -0
  134. package/.claude/worktrees/sillyspec-data-model-research/src/version.js +19 -0
  135. package/.claude/worktrees/sillyspec-data-model-research/src/workflow.js +600 -0
  136. package/.claude/worktrees/sillyspec-data-model-research/src/workspace.js +125 -0
  137. package/.claude/worktrees/sillyspec-data-model-research/src/worktree-apply.js +1446 -0
  138. package/.claude/worktrees/sillyspec-data-model-research/src/worktree-deps.js +576 -0
  139. package/.claude/worktrees/sillyspec-data-model-research/src/worktree.js +1715 -0
  140. package/.claude/worktrees/sillyspec-data-model-research/templates/claude-instruction.md +23 -0
  141. package/.claude/worktrees/sillyspec-data-model-research/templates/prompts/taskcard-rules.md +28 -0
  142. package/.claude/worktrees/sillyspec-data-model-research/templates/prompts/testcase-design.md +12 -0
  143. package/.claude/worktrees/sillyspec-data-model-research/templates/prompts/verify-probes.md +32 -0
  144. package/.claude/worktrees/sillyspec-data-model-research/templates/workflows/archive-impact.yaml +79 -0
  145. package/.claude/worktrees/sillyspec-data-model-research/templates/workflows/scan-docs.yaml +132 -0
  146. package/eval/README.md +86 -0
  147. package/eval/eval.config.json +28 -0
  148. package/eval/lib/adapters/cli.mjs +63 -0
  149. package/eval/lib/adapters/dryrun.mjs +27 -0
  150. package/eval/lib/config.mjs +82 -0
  151. package/eval/lib/db.mjs +76 -0
  152. package/eval/lib/report.mjs +120 -0
  153. package/eval/lib/runner.mjs +157 -0
  154. package/eval/lib/tasks.mjs +73 -0
  155. package/eval/lib/util.mjs +107 -0
  156. package/eval/lib/workdir.mjs +30 -0
  157. package/eval/run.mjs +83 -0
  158. package/eval/tasks/001-slugify/files/package.json +6 -0
  159. package/eval/tasks/001-slugify/solution/src/slugify.mjs +26 -0
  160. package/eval/tasks/001-slugify/task.json +6 -0
  161. package/eval/tasks/001-slugify/verify.mjs +28 -0
  162. package/eval/tasks/002-each-day/files/package.json +6 -0
  163. package/eval/tasks/002-each-day/files/src/dates.mjs +4 -0
  164. package/eval/tasks/002-each-day/solution/src/dates.mjs +30 -0
  165. package/eval/tasks/002-each-day/task.json +6 -0
  166. package/eval/tasks/002-each-day/verify.mjs +31 -0
  167. package/eval/tasks/003-word-report/files/data/notes.txt +5 -0
  168. package/eval/tasks/003-word-report/files/package.json +6 -0
  169. package/eval/tasks/003-word-report/solution/bin/report.mjs +36 -0
  170. package/eval/tasks/003-word-report/task.json +6 -0
  171. package/eval/tasks/003-word-report/verify.mjs +38 -0
  172. package/package.json +4 -2
  173. package/src/agent-session-log.js +45 -5
  174. package/src/change-risk-profile.js +83 -14
  175. package/src/config-schema.js +18 -3
  176. package/src/contract-matrix.js +90 -29
  177. package/src/datetime.js +20 -0
  178. package/src/decision-distill.js +517 -0
  179. package/src/dispatch/backends/local-agent.js +4 -4
  180. package/src/docs-check.js +227 -0
  181. package/src/docs-debt.js +187 -24
  182. package/src/doctor-diagnostics.js +102 -2
  183. package/src/foreign-declared.js +78 -0
  184. package/src/index.js +89 -9
  185. package/src/init.js +9 -7
  186. package/src/knowledge-match.js +107 -6
  187. package/src/local-register.js +7 -1
  188. package/src/module-resolve.js +12 -1
  189. package/src/modules.js +2 -1
  190. package/src/plan-adopt-waves.js +126 -0
  191. package/src/quicklog.js +50 -4
  192. package/src/run/command.js +122 -43
  193. package/src/run/complete-handlers.js +48 -19
  194. package/src/run/complete.js +5 -2
  195. package/src/run/gates.js +87 -22
  196. package/src/run/prompt.js +84 -42
  197. package/src/run/quick-audit.js +11 -0
  198. package/src/run/shared.js +152 -10
  199. package/src/run.js +2 -2
  200. package/src/scan-postcheck.js +30 -17
  201. package/src/stage-contract-spec.js +2 -1
  202. package/src/stage-contract.js +68 -4
  203. package/src/stages/archive.js +53 -12
  204. package/src/stages/brainstorm-auto.js +7 -0
  205. package/src/stages/brainstorm.js +11 -0
  206. package/src/stages/doctor.js +157 -1
  207. package/src/stages/execute.js +32 -6
  208. package/src/stages/plan-postcheck.js +76 -17
  209. package/src/stages/plan.js +1 -0
  210. package/src/stages/quick.js +6 -1
  211. package/src/stages/verify.js +31 -14
  212. package/src/sync.js +41 -3
  213. package/src/task-review.js +2 -2
  214. package/src/taskcard-placeholders.js +21 -0
  215. package/src/taskcard.js +10 -1
  216. package/src/verify-postcheck.js +315 -13
  217. package/src/verify-probes.js +46 -4
  218. package/src/worktree-apply.js +184 -7
  219. package/src/worktree-deps.js +57 -0
  220. package/src/worktree.js +201 -58
  221. package/templates/prompts/taskcard-rules.md +1 -0
package/src/docs-check.js CHANGED
@@ -16,6 +16,7 @@
16
16
  import { readFileSync, writeFileSync, existsSync, readdirSync, statSync } from 'node:fs'
17
17
  import { join } from 'node:path'
18
18
  import jsYaml from 'js-yaml'
19
+ import { parseModuleMapSimple } from './modules.js'
19
20
 
20
21
  /** 提取 file.js:line / file.js:start-end 引用(.js/.mjs,反引号包裹与裸文本均命中,全文扫描 D-006)。
21
22
  * repo:// 前缀(2026-08-20):跨仓引用显式标记——`repo://<仓库名>/<路径>.js:行`。
@@ -596,3 +597,229 @@ export function applyFixes(projectRoot, fixes, opts = {}) {
596
597
 
597
598
  return { applied, skipped }
598
599
  }
600
+
601
+ // ---------------------------------------------------------------------------
602
+ // 决策规则族(advisory,change: 2026-08-23-adopt-harness-practices task-05,FR-06 / D-003@v1)
603
+ //
604
+ // 校验对象:knowledge/decisions/<模块域>.md 的 D-xxx@vN 条目(producer = decision-distill
605
+ // 归档提炼,条目字段行 状态/锚点/最近确认 是本处的机械解析契约)。两项规则:
606
+ // 1) 锚点校验:implemented 条目「锚点:src/…」路径存在性(锚点:未记录 → 补录提示,非失效);
607
+ // 2) behind 复核:锚定模块源码在「最近确认」commit 后的前进数超阈值 → 决策待复核提示
608
+ // (computeModuleBehind 复用 docs-debt 既有 git 口径,C-10)。
609
+ // 铁律(docs-consistency 模块卡):advisory 只 warn 不进 runDocsCheck 的 invalid 阻断链——
610
+ // ok/invalid 判定与 docs gate 阻断行为不受影响;校验链路只读零写盘;无 decisions 库 /
611
+ // 无超阈 → findings 空(无信号零输出)。豁免走 known_failures 新键 decisions.* 命名空间
612
+ // (C-17:条目级语义键匹配,不复用 docs-gate 的 baseline ratchet)。
613
+ // ---------------------------------------------------------------------------
614
+
615
+ /** behind 复核缺省阈值(local.yaml decisions.behind_threshold 可调;schema 注册归 task-11,
616
+ * 本处读键按「存在则读、不存在用缺省」容错,不依赖 schema 注册)。dogfood 校准前取 10。 */
617
+ export const DECISIONS_DEFAULT_BEHIND_THRESHOLD = 10
618
+
619
+ /**
620
+ * known_failures 键提取——读法逐字对齐 verify-postcheck.extractKnownFailures(块式 + 流式,
621
+ * 剥引号/行内注释,CRLF 归一;注释互指防两处口径漂移)。此处独立复刻不 import
622
+ * verify-postcheck(重模块静态依赖;本模块仅消费 decisions.* 前缀键,见 isDecisionExempt)。
623
+ * @param {string} yamlText local.yaml 全文
624
+ * @returns {string[]} 键列表;无声明返回 []
625
+ */
626
+ function extractKnownFailureKeys(yamlText) {
627
+ if (!yamlText) return []
628
+ const yaml = String(yamlText).replace(/\r\n?/g, '\n')
629
+ const inline = yaml.match(/^known_failures:\s*\[([^\]]*)\]\s*(?:#.*)?$/m)
630
+ if (inline) {
631
+ return inline[1].split(',').map(s => s.trim().replace(/^['"]|['"]$/g, '')).filter(Boolean)
632
+ }
633
+ const block = yaml.match(/^known_failures:\s*\n((?:[ \t]+-[ \t].+\n?)+)/m)
634
+ if (block) {
635
+ return (block[1].match(/^[ \t]+-[ \t]+(.+)/gm) || [])
636
+ .map(s => s.replace(/^[ \t]+-[ \t]+/, '').trim().replace(/^['"]|['"]$/g, '').replace(/#.*$/, '').trim())
637
+ .filter(Boolean)
638
+ }
639
+ return []
640
+ }
641
+
642
+ /**
643
+ * 读 local.yaml 的决策规则配置(best-effort 绝不抛;读取模式与 readDocsCheckConfig 一致:
644
+ * 缺文件/无段 → 全缺省)。decisions.behind_threshold 存在则读(数值校验),不存在用缺省 10
645
+ * ——schema 注册(config-schema.js)归 task-11,本处不依赖。
646
+ * @param {string} projectRoot 源码仓根(local.yaml 在 <root>/.sillyspec/local.yaml)
647
+ * @returns {{ behindThreshold: number, knownFailures: string[] }}
648
+ */
649
+ export function readDecisionRulesConfig(projectRoot) {
650
+ const fallback = { behindThreshold: DECISIONS_DEFAULT_BEHIND_THRESHOLD, knownFailures: [] }
651
+ try {
652
+ const p = join(projectRoot, '.sillyspec', 'local.yaml')
653
+ if (!existsSync(p)) return fallback
654
+ const raw = readFileSync(p, 'utf8')
655
+ const knownFailures = extractKnownFailureKeys(raw)
656
+ const doc = jsYaml.load(raw)
657
+ if (!doc || typeof doc !== 'object') return { ...fallback, knownFailures }
658
+ const d = doc['decisions']
659
+ if (!d || typeof d !== 'object' || Array.isArray(d)) return { ...fallback, knownFailures }
660
+ const t = d['behind_threshold']
661
+ return {
662
+ behindThreshold: typeof t === 'number' && Number.isFinite(t) && t > 0 ? t : fallback.behindThreshold,
663
+ knownFailures,
664
+ }
665
+ } catch {
666
+ return fallback
667
+ }
668
+ }
669
+
670
+ /** 豁免键匹配(本变更范围先支持键匹配,非子串):规则级 `decisions.<id>.<kind>` 精确命中,
671
+ * 或条目级 `decisions.<id>` 伞形命中(一条豁免该决策全部规则)。无 decisions. 前缀的键不参与。 */
672
+ function isDecisionExempt(knownFailures, entryId, kind) {
673
+ const ruleKey = `decisions.${entryId}.${kind}`
674
+ const entryKey = `decisions.${entryId}`
675
+ return knownFailures.some(k => k === ruleKey || k === entryKey)
676
+ }
677
+
678
+ /**
679
+ * 纯函数:解析 knowledge/decisions/<域>.md 的 D-xxx@vN 条目(与 decision-distill.renderBlockLines
680
+ * 的写入契约对偶:producer=decision-distill → consumer=本规则族;字段行非列表项,顶层
681
+ * 「状态:/锚点:/最近确认:」标签精确匹配,防误吞正文)。旧格式字段缺失容错为 null。
682
+ * @param {string} md 域文件全文
683
+ * @returns {Array<{ id: string, title: string, status: string|null, anchor: string|null, lastConfirmed: string|null }>}
684
+ */
685
+ export function parseDecisionEntries(md) {
686
+ const entries = []
687
+ let cur = null
688
+ const flush = () => { if (cur) entries.push(cur) }
689
+ for (const line of String(md || '').replace(/\r\n?/g, '\n').split('\n')) {
690
+ const h = line.match(/^##\s+(D-\d+)(?:@v(\d+))?\s*(.*)$/)
691
+ if (h) {
692
+ flush()
693
+ const version = h[2] ? parseInt(h[2], 10) : 1
694
+ cur = { id: `${h[1]}@v${version}`, title: h[3].trim(), status: null, anchor: null, lastConfirmed: null }
695
+ continue
696
+ }
697
+ if (!cur) continue
698
+ const f = line.match(/^(状态|锚点|最近确认)\s*[::]\s*(.*)$/)
699
+ if (!f) continue
700
+ if (f[1] === '状态') cur.status = f[2].trim().split(/[\s((]/)[0] || null
701
+ else if (f[1] === '锚点') cur.anchor = f[2].trim()
702
+ else cur.lastConfirmed = f[2].trim()
703
+ }
704
+ flush()
705
+ return entries
706
+ }
707
+
708
+ /** 锚点值 → 源码路径:剥「:行号 / :行号-行号 / :符号」后缀(锚点契约 `<src 路径>:<行号或符号>`;
709
+ * 旧条目可能只写路径)。「未记录」/空 → null(补录提示路径,非失效)。
710
+ * export(2026-08-24-decision-touch-cli-drift task-01):docs-debt.computeDecisionTouches
711
+ * 复用同一剥离口径(勿复刻正则防两处漂移);仅加关键字,行为不变。 */
712
+ export function anchorFilePath(anchor) {
713
+ const a = String(anchor || '').trim()
714
+ if (!a || a === '未记录') return null
715
+ return a.replace(/:(?:\d+(?:-\d+)?|[A-Za-z_$][A-Za-z0-9_$]*)$/, '').replace(/\\/g, '/') || null
716
+ }
717
+
718
+ /** specBase/docs 下任一项目的 modules/_module-map.yaml 尽力发现(对齐
719
+ * decision-distill.discoverModuleIndex 口径:首个命中;失败 → null,域模块源码集退化为锚点文件兜底)。 */
720
+ function discoverModuleMapIndex(specBase) {
721
+ try {
722
+ const docsDir = join(specBase, 'docs')
723
+ if (!existsSync(docsDir)) return null
724
+ for (const d of readdirSync(docsDir, { withFileTypes: true })) {
725
+ if (!d.isDirectory()) continue
726
+ const mapPath = join(docsDir, d.name, 'modules', '_module-map.yaml')
727
+ if (existsSync(mapPath)) return parseModuleMapSimple(readFileSync(mapPath, 'utf8'))
728
+ }
729
+ } catch { /* 尽力而为,失败归 null */ }
730
+ return null
731
+ }
732
+
733
+ /**
734
+ * IO 入口(advisory):跑一次决策规则族。只产出 findings(warn 语义),不进 runDocsCheck 的
735
+ * invalid 阻断链、不写任何文件——无 decisions 库 / 无信号 → findings 空(零输出)。
736
+ * computeModuleBehind 经动态 import 取自 docs-debt(本模块被 docs-debt 静态 import,静态互引
737
+ * 成 ESM 环——对齐 run/shared.js 对 docs-debt 的动态 import 先例)。
738
+ * @param {{ projectRoot: string, specBase?: string, behindThreshold?: number, knownFailures?: string[], moduleIndex?: object|null }} opts
739
+ * projectRoot 源码仓根(锚点解析与 git 锚);specBase 缺省 = <projectRoot>/.sillyspec;
740
+ * behindThreshold / knownFailures 缺省读 local.yaml(decisions.behind_threshold / known_failures);
741
+ * moduleIndex 缺省按 specBase/docs 下任一项目的 modules/_module-map.yaml 尽力发现
742
+ * @returns {Promise<{ empty: boolean, domains: number, entries: number, implemented: number,
743
+ * threshold: number, findings: Array<{ id: string, domain: string, file: string,
744
+ * kind: 'anchor'|'behind', behind?: number|null, threshold?: number, anchor?: string|null,
745
+ * message: string }>, exempted: Array<{ key: string, id: string, kind: string }> }>}
746
+ * empty=true = decisions 库不存在(冷启动,R-02 由 doctor 层提示);exempted 为经
747
+ * known_failures decisions.* 键豁免的条目(披露不隐藏)
748
+ */
749
+ export async function runDecisionRules(opts) {
750
+ const { projectRoot, specBase, behindThreshold = null, knownFailures = null, moduleIndex = null } = opts || {}
751
+ const emptyResult = (threshold) => ({ empty: true, domains: 0, entries: 0, implemented: 0, threshold, findings: [], exempted: [] })
752
+ if (!projectRoot) return emptyResult(DECISIONS_DEFAULT_BEHIND_THRESHOLD)
753
+
754
+ const cfg = readDecisionRulesConfig(projectRoot)
755
+ const threshold = typeof behindThreshold === 'number' && Number.isFinite(behindThreshold) && behindThreshold > 0
756
+ ? behindThreshold
757
+ : cfg.behindThreshold
758
+ const failures = Array.isArray(knownFailures) ? knownFailures : cfg.knownFailures
759
+
760
+ const kb = specBase || join(projectRoot, '.sillyspec')
761
+ const decisionsDir = join(kb, 'knowledge', 'decisions')
762
+ let domainFiles = []
763
+ try {
764
+ domainFiles = readdirSync(decisionsDir).filter(f => f.endsWith('.md')).sort()
765
+ } catch {
766
+ return emptyResult(threshold) // 库不存在 → 零信号(doctor 层另行提示空库)
767
+ }
768
+
769
+ // 模块域 → 源码路径集(module-map paths∪core_files;未命中 map 的域退化为锚点文件兜底)
770
+ const mapIndex = moduleIndex || discoverModuleMapIndex(kb)
771
+ const flatIndex = mapIndex && mapIndex.modules ? mapIndex.modules : mapIndex
772
+ const { computeModuleBehind } = await import('./docs-debt.js')
773
+
774
+ const findings = []
775
+ const exempted = []
776
+ let entriesTotal = 0
777
+ let implementedTotal = 0
778
+ const treeCache = new Map() // resolveCandidates 全树扫描复用(同 docs check 主链路口径)
779
+
780
+ const emit = (id, domain, file, kind, extra, message) => {
781
+ if (isDecisionExempt(failures, id, kind)) {
782
+ exempted.push({ key: `decisions.${id}.${kind}`, id, kind })
783
+ return
784
+ }
785
+ findings.push({ id, domain, file, kind, ...extra, message })
786
+ }
787
+
788
+ for (const f of domainFiles) {
789
+ const domain = f.replace(/\.md$/, '')
790
+ const rel = `knowledge/decisions/${f}`
791
+ let entries
792
+ try { entries = parseDecisionEntries(readFileSync(join(decisionsDir, f), 'utf8')) } catch { continue }
793
+ for (const e of entries) {
794
+ entriesTotal++
795
+ if ((e.status || '').toLowerCase() !== 'implemented') continue // rejected/未知状态不做锚点与 behind 校验
796
+ implementedTotal++
797
+ const anchorPath = anchorFilePath(e.anchor)
798
+ // 规则 1:锚点存在性(未记录 → 补录提示;路径不存在 → 失效提示)
799
+ if (!anchorPath) {
800
+ emit(e.id, domain, rel, 'anchor', { anchor: e.anchor || null },
801
+ `「${e.id}」锚点未记录(${rel})——旧格式条目,补「锚点:src/…」后可机械校验(advisory)`)
802
+ } else if (resolveCandidates(projectRoot, anchorPath, treeCache).length === 0) {
803
+ emit(e.id, domain, rel, 'anchor', { anchor: e.anchor },
804
+ `「${e.id}」锚点文件不存在:${anchorPath}(${rel})——决策落点已漂移,人工复核(advisory)`)
805
+ }
806
+ // 规则 2:behind 复核(「最近确认」后锚定模块源码前进数超阈值 → 决策待复核;
807
+ // 「最近确认」未记录/域无源码集(无 map 无锚点)→ 无法计算,静默跳过——advisory 不产噪声)
808
+ if (!e.lastConfirmed || !/^[0-9a-f]{4,40}$/i.test(e.lastConfirmed)) continue
809
+ let srcPaths = null
810
+ const m = flatIndex && typeof flatIndex === 'object' ? flatIndex[domain] : null
811
+ if (m) srcPaths = [...(Array.isArray(m.paths) ? m.paths : []), ...(Array.isArray(m.core_files) ? m.core_files : [])]
812
+ if ((!srcPaths || srcPaths.length === 0) && anchorPath) srcPaths = [anchorPath]
813
+ if (!srcPaths || srcPaths.length === 0) continue
814
+ const { behind } = computeModuleBehind(domain, e.lastConfirmed, { projectRoot, srcPaths })
815
+ if (behind !== null && behind > threshold) {
816
+ emit(e.id, domain, rel, 'behind', { behind, threshold },
817
+ `「${e.id}」决策待复核:锚定模块 ${domain} 在最近确认 ${e.lastConfirmed} 后源码已前进 ${behind} commit,超阈值 ${threshold}(${rel})——复核后更新「最近确认」`)
818
+ }
819
+ }
820
+ }
821
+
822
+ findings.sort((a, b) => a.file.localeCompare(b.file) || a.id.localeCompare(b.id))
823
+ exempted.sort((a, b) => a.key.localeCompare(b.key))
824
+ return { empty: false, domains: domainFiles.length, entries: entriesTotal, implemented: implementedTotal, threshold, findings, exempted }
825
+ }
package/src/docs-debt.js CHANGED
@@ -10,15 +10,20 @@
10
10
  * behind = rev-list --count docCommit..srcCommit;untracked 卡 behind=null 显式"卡片从未提交";
11
11
  * git 失败/超时 5s → 该模块 behind=null + 降级注记,不抛。
12
12
  */
13
- import { existsSync, readFileSync, readdirSync } from 'node:fs'
13
+ import { existsSync, readFileSync, readdirSync, realpathSync } 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, readDocsCheckConfig } from './docs-check.js'
17
+ import { runDocsCheck, readDocsCheckConfig, parseDecisionEntries, anchorFilePath } from './docs-check.js'
18
18
 
19
19
  /** safeGit 超时(大仓 git log 慢时降级该模块,不挂起 Wave 渲染) */
20
20
  const GIT_TIMEOUT_MS = 5000
21
21
 
22
+ /** realpath 归一(失败回退原值——路径不可及时保持调用方原有行为) */
23
+ function realpathSafe(p) {
24
+ try { return realpathSync(p) } catch { return p }
25
+ }
26
+
22
27
  /**
23
28
  * 纯函数:把 changedFiles 归属到模块。
24
29
  * @param {string[]} changedFiles 变更文件(POSIX 相对路径)
@@ -90,7 +95,31 @@ export function matchFilesToModules(changedFiles, moduleIndex, opts = {}) {
90
95
  return { byModule, unmapped }
91
96
  }
92
97
 
93
- /** 单模块欠账:双 commit 时间戳判方向 + rev-list 计数。全失败 → behind null + 注记。
98
+ /** "h ct" 输出解析(git log -1 --format=%h %ct 的单行值)→ { h, ct };空输出 → null。
99
+ * 刻意不额外校验 ct 可析性——与 moduleDebt 原内联解析逐字等价(含畸形输出 NaN ct 形态),
100
+ * 保证公共抽取零行为漂移。 */
101
+ function parseHct(out) {
102
+ if (!out || !out.trim()) return null
103
+ const [h, ct] = out.trim().split(' ')
104
+ return { h, ct: parseInt(ct, 10) }
105
+ }
106
+
107
+ /** behind 公共口径(D-004;2026-08-23-adopt-harness-practices task-05 抽公共实现):双 commit
108
+ * 时间戳判方向 + rev-list 计数。src.ct >= ref.ct → rev-list --count ref..src(git 失败 →
109
+ * behind=null + degraded=true,不抛);src 时间戳落后 ref → behind=0。ct 相等也进对账
110
+ * (同一秒内的提交——测试/快速序列场景),由 rev-list count 分晓。
111
+ * moduleDebt 与 computeModuleBehind 共用——behind 口径单一真相源。 */
112
+ function behindWithDirection(gitRoot, srcCommit, refCommit) {
113
+ if (srcCommit.ct >= refCommit.ct) {
114
+ const cnt = safeGit(gitRoot, ['rev-list', '--count', `${refCommit.h}..${srcCommit.h}`], { timeout: GIT_TIMEOUT_MS })
115
+ const behind = cnt.error ? null : parseInt(cnt.value, 10)
116
+ return { behind, degraded: behind === null }
117
+ }
118
+ return { behind: 0, degraded: false }
119
+ }
120
+
121
+ /** 单模块欠账:双 commit 时间戳判方向 + rev-list 计数(复用 parseHct/behindWithDirection 公共
122
+ * 实现,抽取前后逐字等价,C-10)。全失败 → behind null + 注记。
94
123
  * docGitPath:模块卡的 git 相对路径(specBase 相对 projectRoot 换算——specBase 在项目内时
95
124
  * 是 .sillyspec/docs/<p>/modules/<id>.md;平台模式 specBase 在仓外时卡片不在 git 内 →
96
125
  * docLog 恒失败 → 走"从未提交"形态,如实呈现)。 */
@@ -102,41 +131,58 @@ function moduleDebt(projectRoot, moduleId, entry, docGitPath, docGitRoot) {
102
131
  let srcCommit = null
103
132
  let docCommit = null
104
133
  let neverCommitted = false
105
- if (!srcLog.error && srcLog.value && srcLog.value.trim()) {
106
- const [h, ct] = srcLog.value.trim().split(' ')
107
- srcCommit = { h, ct: parseInt(ct, 10) }
108
- } else {
109
- notes.push('源码 commit 历史不可得(新文件或 git 失败)')
110
- }
134
+ srcCommit = (!srcLog.error && srcLog.value) ? parseHct(srcLog.value) : null
135
+ if (!srcCommit) notes.push('源码 commit 历史不可得(新文件或 git 失败)')
111
136
  if (docGitPath) {
112
137
  // 卡片历史在卡片所在 git 仓查(docGitRoot;linked worktree 场景 = 主仓根),
113
138
  // 源码历史在 projectRoot 查——两侧 commit 同属一个仓库历史可 rev-list 对账
114
139
  const docGitCwd = docGitRoot || projectRoot
115
140
  const docLog = safeGit(docGitCwd, ['log', '-1', '--format=%h %ct', '--', docGitPath], { timeout: GIT_TIMEOUT_MS })
116
- if (!docLog.error && docLog.value && docLog.value.trim()) {
117
- const [h, ct] = docLog.value.trim().split(' ')
118
- docCommit = { h, ct: parseInt(ct, 10) }
119
- } else {
120
- neverCommitted = true
121
- }
141
+ docCommit = (!docLog.error && docLog.value) ? parseHct(docLog.value) : null
142
+ if (!docCommit) neverCommitted = true
122
143
  }
123
144
  if (srcCommit && !docCommit) {
124
145
  // 有源码历史但卡片从未提交 → 欠账最重形态
125
146
  return { module: moduleId, ...entry, srcCommit: srcCommit.h, docCommit: null, behind: null, neverCommitted: true, notes }
126
147
  }
127
148
  if (srcCommit && docCommit) {
128
- // ct 相等也进对账(同一秒内的提交——测试/快速序列场景),由 rev-list count 分晓
129
- if (srcCommit.ct >= docCommit.ct) {
130
- const cnt = safeGit(docGitRoot || projectRoot, ['rev-list', '--count', `${docCommit.h}..${srcCommit.h}`], { timeout: GIT_TIMEOUT_MS })
131
- behind = cnt.error ? null : parseInt(cnt.value, 10)
132
- if (behind === null) notes.push('rev-list 失败降级')
133
- } else {
134
- return { module: moduleId, ...entry, srcCommit: srcCommit.h, docCommit: docCommit.h, behind: 0, neverCommitted: false, notes }
135
- }
149
+ behind = behindWithDirection(docGitRoot || projectRoot, srcCommit, docCommit).behind
150
+ if (behind === null) notes.push('rev-list 失败降级')
136
151
  }
137
152
  return { module: moduleId, ...entry, srcCommit: srcCommit?.h || null, docCommit: docCommit?.h || null, behind, neverCommitted, notes }
138
153
  }
139
154
 
155
+ /**
156
+ * 公共导出(C-10,2026-08-23-adopt-harness-practices task-05):模块源码在参考 commit 后的
157
+ * 前进数(behind)。口径同 moduleDebt(D-004:双 commit 时间戳判方向 + rev-list 计数,复用
158
+ * behindWithDirection;git 失败/输入缺失 → behind=null + degraded=true,不抛)。
159
+ * 消费方:docs-check 决策规则 behind 复核(决策锚定模块源码在「最近确认」commit 后的前进数)。
160
+ * 不改 moduleDebt 对外行为与既有返回结构(computeDocsDebt / matchFilesToModules 输出不变)。
161
+ * @param {string} moduleId 模块 id(_module-map.yaml 键;决策规则里 = 条目所在域文件名)
162
+ * @param {string} lastConfirmedCommit 参考 commit(决策「最近确认」hash;空/「未记录」/非 hash 串 → degraded)
163
+ * @param {{ projectRoot?: string, srcPaths?: string[], moduleIndex?: object }} [opts]
164
+ * projectRoot git 锚(缺省 process.cwd());srcPaths 显式源码路径集(优先);
165
+ * moduleIndex _module-map 解析结果(平铺或 {modules} 包裹均可,取该模块 paths∪core_files 兜底)
166
+ * @returns {{ behind: number|null, degraded: boolean }} degraded=true = behind 不可得
167
+ * (路径集为空 / 参考 commit 不可解析 / git 失败),调用方降级呈现不阻断
168
+ */
169
+ export function computeModuleBehind(moduleId, lastConfirmedCommit, opts = {}) {
170
+ const projectRoot = opts.projectRoot || process.cwd()
171
+ let srcPaths = Array.isArray(opts.srcPaths) ? opts.srcPaths.filter(Boolean).map(p => String(p)) : null
172
+ if ((!srcPaths || srcPaths.length === 0) && opts.moduleIndex) {
173
+ const flat = opts.moduleIndex.modules ? opts.moduleIndex.modules : opts.moduleIndex
174
+ const m = flat && typeof flat === 'object' ? flat[moduleId] : null
175
+ if (m) srcPaths = [...(Array.isArray(m.paths) ? m.paths : []), ...(Array.isArray(m.core_files) ? m.core_files : [])]
176
+ }
177
+ if (!srcPaths || srcPaths.length === 0) return { behind: null, degraded: true }
178
+ const ref = String(lastConfirmedCommit || '').trim()
179
+ if (!/^[0-9a-f]{4,40}$/i.test(ref)) return { behind: null, degraded: true }
180
+ const srcCommit = parseHct(safeGit(projectRoot, ['log', '-1', '--format=%h %ct', '--', ...srcPaths], { timeout: GIT_TIMEOUT_MS }).value)
181
+ const refCommit = parseHct(safeGit(projectRoot, ['log', '-1', '--format=%h %ct', ref], { timeout: GIT_TIMEOUT_MS }).value)
182
+ if (!srcCommit || !refCommit) return { behind: null, degraded: true }
183
+ return behindWithDirection(projectRoot, srcCommit, refCommit)
184
+ }
185
+
140
186
  /**
141
187
  * IO 入口:算模块文档欠账事实。
142
188
  * @param {{ projectRoot: string, specBase: string, projectName: string, changedFiles: string[] }} opts
@@ -177,7 +223,10 @@ export function computeDocsDebt(opts) {
177
223
  const specTop = safeGit(specBase, ['rev-parse', '--show-toplevel'], { timeout: GIT_TIMEOUT_MS })
178
224
  if (!specTop.error && specTop.value) {
179
225
  docGitRoot = specTop.value.trim()
180
- const rel = relative(docGitRoot, cardsDir)
226
+ // macOS 坑:git 返回 realpath(/var → /private/var 的 symlink 解析),cardsDir 可能是
227
+ // symlink 形态(tmpdir/用户项目路径经符号链接)——两侧不归一时 relative() 产出 ../..
228
+ // 前缀 → 卡片被误判"从未提交"(假账)。两侧 realpathSync 归一后再算(失败回退原值)。
229
+ const rel = relative(realpathSafe(docGitRoot), realpathSafe(cardsDir))
181
230
  if (rel && !rel.startsWith('..')) docPathPrefix = rel.replace(/\\/g, '/')
182
231
  }
183
232
  } catch { /* 保持 null */ }
@@ -228,3 +277,117 @@ function inlineCardInvalidRefs(entry, docGitRoot, docPathPrefix) {
228
277
  return null
229
278
  }
230
279
  }
280
+
281
+ // ---------------------------------------------------------------------------
282
+ // 决策锚点触碰事实(change: 2026-08-24-decision-touch-cli-drift task-01,W-A)
283
+ // 同「CLI 算事实注入」原则:execute 期改到决策锚定文件时立即呈现「触碰 N 条决策锚点」
284
+ // 事实(advisory 不阻断,无触碰零输出)。条目解析/锚点剥离复用 docs-check 口径
285
+ // (parseDecisionEntries + anchorFilePath 静态 import,本文件已 import docs-check 无环),
286
+ // 勿复刻正则防两处口径漂移。
287
+ // ---------------------------------------------------------------------------
288
+
289
+ /** 触碰事实行条数上限(R-05 prompt 体积):超出截断 +「…另有 N 条」 */
290
+ export const DECISION_TOUCH_RENDER_LIMIT = 5
291
+
292
+ /**
293
+ * 纯事实计算:变更文件 × 决策锚点前缀匹配 → 触碰事实(唯一真相形态)。
294
+ * @param {string[]} changedFiles 变更文件(入口统一 POSIX 化——调用方不重复处理)
295
+ * @param {string} knowledgeRoot 知识库根(<specBase>/knowledge;decisions 库在其 decisions/ 子目录,
296
+ * 与 docs-check runDecisionRules 的 <specBase>/knowledge/decisions 同布局)
297
+ * @returns {{ touches: Array<{ id: string, title: string, anchorFile: string, touchedFile: string, file: string }>, empty: boolean }}
298
+ * touches 每项 = 一条 implemented 决策 × 一个命中变更文件(file = 条目来源域文件
299
+ * knowledge/decisions/<域>.md);empty=true = decisions 库不存在(冷启动);
300
+ * 无触碰 → touches=[] 且 empty=false(零输出由渲染层保证)
301
+ */
302
+ export function computeDecisionTouches(changedFiles, knowledgeRoot) {
303
+ const emptyResult = { touches: [], empty: true }
304
+ if (!knowledgeRoot) return emptyResult
305
+ const decisionsDir = join(knowledgeRoot, 'decisions')
306
+ let domainFiles = []
307
+ try {
308
+ domainFiles = readdirSync(decisionsDir).filter(f => f.endsWith('.md')).sort()
309
+ } catch {
310
+ return emptyResult // decisions 库不存在 → 零信号(与 runDecisionRules 冷启动同语义)
311
+ }
312
+ const changed = (Array.isArray(changedFiles) ? changedFiles : [])
313
+ .filter(Boolean).map(f => String(f).replace(/\\/g, '/'))
314
+ const touches = []
315
+ for (const f of domainFiles) {
316
+ const file = `knowledge/decisions/${f}`
317
+ let entries
318
+ try { entries = parseDecisionEntries(readFileSync(join(decisionsDir, f), 'utf8')) } catch { continue }
319
+ for (const e of entries) {
320
+ if ((e.status || '').toLowerCase() !== 'implemented') continue // 仅 implemented(对齐 docs-check.js:793 先例,rejected 不提示)
321
+ const anchorPath = anchorFilePath(e.anchor) // 剥 :行号/:行号-行号/:符号 后缀 + POSIX 化;「未记录」/空 → null 跳过
322
+ if (!anchorPath) continue
323
+ for (const c of changed) {
324
+ // 前缀匹配粒度 = 文件或其子路径(R-03:锚点是文件级非目录级,防同前缀不同文件误报)
325
+ if (c === anchorPath || c.startsWith(anchorPath + '/')) {
326
+ touches.push({ id: e.id, title: e.title, anchorFile: anchorPath, touchedFile: c, file })
327
+ }
328
+ }
329
+ }
330
+ }
331
+ touches.sort((a, b) => a.file.localeCompare(b.file) || a.id.localeCompare(b.id) || a.touchedFile.localeCompare(b.touchedFile))
332
+ return { touches, empty: false }
333
+ }
334
+
335
+ /**
336
+ * 触碰事实 → 事实行文本(advisory):无触碰返回 ''(零输出);≤DECISION_TOUCH_RENDER_LIMIT 条
337
+ * 逐条呈现,超出截断加「…另有 N 条」(R-05)。双渲染点(run/prompt.js {DOCS_DEBT} 注入处 +
338
+ * stages/execute.js buildWavePrompt)共用同一渲染实现,防两处文案漂移。
339
+ * @param {Array<{ id: string, title: string, anchorFile: string, touchedFile: string, file: string }>} touches
340
+ * @returns {string}
341
+ */
342
+ export function renderDecisionTouchFacts(touches) {
343
+ if (!Array.isArray(touches) || touches.length === 0) return ''
344
+ const shown = touches.slice(0, DECISION_TOUCH_RENDER_LIMIT)
345
+ const lines = [`[decision-touch] 本次变更触碰 ${touches.length} 条决策锚点(改动可能使该决策需复核):`]
346
+ for (const t of shown) {
347
+ lines.push(` - ${t.id}(${t.title || '无标题'},锚点 ${t.anchorFile})← 触碰文件 ${t.touchedFile}(${t.file})`)
348
+ }
349
+ if (touches.length > DECISION_TOUCH_RENDER_LIMIT) {
350
+ lines.push(` …另有 ${touches.length - DECISION_TOUCH_RENDER_LIMIT} 条`)
351
+ }
352
+ return lines.join('\n')
353
+ }
354
+
355
+ /**
356
+ * execute 期事实注入的 changedFiles 公共口径(D-003 主/次渲染点同源,勿双写):
357
+ * worktree 根解析(changeName → <specBase>/.runtime/worktrees/<changeName>/ 存在即锚,
358
+ * 否则 cwd/显式 projectRoot)+ porcelain 未提交(剥 .sillyspec/ 前缀、rename 取新路径)∪
359
+ * worktree meta.json baselineCommit..HEAD 已提交并集,POSIX 化。从 run/prompt.js {DOCS_DEBT}
360
+ * 内联实现抽取(行为逐字等价);execute.js buildWavePrompt(主渲染点)同源复用。
361
+ * git 失败/meta 读失败 → 只用可用集合,不抛。
362
+ * @param {{ specBase?: string, changeName?: string|null, cwd?: string|null, projectRoot?: string|null }} opts
363
+ * @returns {{ root: string, changedFiles: string[] }} root = 实际 git 锚(computeDocsDebt 的 projectRoot 同值)
364
+ */
365
+ export function collectExecuteChangedFiles({ specBase, changeName, cwd, projectRoot } = {}) {
366
+ const wtRoot = specBase ? join(specBase, '.runtime', 'worktrees', changeName || '') : null
367
+ const root = projectRoot || (wtRoot && changeName && existsSync(wtRoot) ? wtRoot : cwd) || process.cwd()
368
+ const changed = new Set()
369
+ const statusRes = safeGit(root, ['status', '--porcelain'], { trim: false })
370
+ if (!statusRes.error && statusRes.value) {
371
+ for (const line of statusRes.value.split('\n')) {
372
+ if (!line.trim()) continue
373
+ // "XY path" / rename "R old -> new" 取 new
374
+ const pathPart = line.slice(3).split('->').pop().trim().replace(/"/g, '')
375
+ if (pathPart && !pathPart.startsWith('.sillyspec/')) changed.add(pathPart.replace(/\\/g, '/'))
376
+ }
377
+ }
378
+ const metaPath = wtRoot ? join(wtRoot, 'meta.json') : null
379
+ if (metaPath && changeName && existsSync(metaPath)) {
380
+ try {
381
+ const meta = JSON.parse(readFileSync(metaPath, 'utf8'))
382
+ if (meta.baselineCommit) {
383
+ const diffRes = safeGit(root, ['diff', '--name-only', `${meta.baselineCommit}..HEAD`], { trim: false })
384
+ if (!diffRes.error && diffRes.value) {
385
+ for (const f of diffRes.value.split('\n')) {
386
+ if (f.trim() && !f.trim().startsWith('.sillyspec/')) changed.add(f.trim().replace(/\\/g, '/'))
387
+ }
388
+ }
389
+ }
390
+ } catch { /* meta 读失败只用未提交集 */ }
391
+ }
392
+ return { root, changedFiles: [...changed] }
393
+ }
@@ -23,8 +23,9 @@
23
23
  import { openDatabase, pluckGet, pluckAll } from './db-engine.js';
24
24
  import { existsSync, statSync, readFileSync, readdirSync, mkdirSync, writeFileSync, unlinkSync, rmSync } from 'fs';
25
25
  import { join } from 'path';
26
+ import jsYaml from 'js-yaml';
26
27
  import { CHECK_SEVERITY } from './constants.js';
27
- import { checkPlatformManaged } from './run/shared.js';
28
+ import { checkPlatformManaged, isSelfReferentialSpecRoot, PLATFORM_MANAGED_FILENAME } from './run/shared.js';
28
29
 
29
30
  // db 角色标签
30
31
  const DB_ROLE = {
@@ -580,6 +581,104 @@ function detectExecuteProgressPlanMismatch(authoritySpecRoot, authDb) {
580
581
  return dim;
581
582
  }
582
583
 
584
+ // ── D6 repo-native 断链画像(FR-4,变更 2026-08-23-repo-native-spec-backfill)─────
585
+ //
586
+ // repo-native 工作区的投毒残留三画像(daemon junction 回环:--spec-root 缓存路径指回源项目
587
+ // <cwd>/.sillyspec)。新版 CLI(≥3.27.3)对三者均自动免疫(恢复忽略/写入拦截/声明降级),
588
+ // 本维度把残留显性化给用户/agent——残留本身无阻断风险,凭据缺失才是上行断链的无感知杀手
589
+ //(design 风险 5:内置 sync best-effort 静默跳过)。自指判定复用 run/shared.js 单一数据源
590
+ //(本模块已有 checkPlatformManaged 静态 import 链,无独立脚本复制问题)。
591
+ //
592
+ // 三画像(均 WARNING 级 advisory,doctor 只读绝不自动清理):
593
+ // ① 指针存在且自指(旧 CLI 中毒残留,建议升级 CLI 或删指针文件)
594
+ // ② 接管声明存在但指针缺失且自指(陈旧声明,可 sillyspec platform disconnect 清理)
595
+ // ③ cwd/.sillyspec 存在但 local.yaml 无 platform 段 + 自指残留(凭据缺失,上行静默失败)
596
+
597
+ /** local.yaml 是否有 platform 段(url/token 凭据)。读失败/无文件/无段 → false(宁报缺勿漏报)。 */
598
+ function localYamlHasPlatformSection(cwd) {
599
+ try {
600
+ const p = join(cwd, '.sillyspec', 'local.yaml');
601
+ if (!existsSync(p)) return false;
602
+ const doc = jsYaml.load(readFileSync(p, 'utf8'));
603
+ return !!(doc && typeof doc === 'object' && doc.platform && typeof doc.platform === 'object');
604
+ } catch {
605
+ return false;
606
+ }
607
+ }
608
+
609
+ function detectRepoNativeChain(cwd, pointer) {
610
+ const dim = {
611
+ name: 'repo_native_chain',
612
+ label: 'repo-native 断链画像',
613
+ safe_actions: [],
614
+ findings: [],
615
+ };
616
+ const decl = checkPlatformManaged(cwd);
617
+ const ptrSelfRef = pointer.present && !pointer.corrupted && isSelfReferentialSpecRoot(cwd, pointer.specRoot);
618
+ const declSelfRef = !!decl && isSelfReferentialSpecRoot(cwd, decl.specRoot);
619
+ let triggered = false;
620
+
621
+ // 画像①:自指指针在盘(runCommand 会忽略并按本地模式运行,但残留说明旧 CLI 投过毒)
622
+ if (ptrSelfRef) {
623
+ triggered = true;
624
+ dim.findings.push(
625
+ `repo_native_self_referential_pointer: 平台指针 specRoot 自指回环(realpath = 本地 .sillyspec,` +
626
+ `repo-native junction 投毒残留):${pointer.specRoot}——新版 CLI 已自动忽略按本地模式运行;` +
627
+ `建议删除指针文件 ${pointer.path} 或升级 CLI(≥3.27.3)防旧版再中毒`
628
+ );
629
+ dim.safe_actions.push({
630
+ dimension: 'repo_native_chain',
631
+ action: 'remove_self_referential_pointer',
632
+ target: pointer.path,
633
+ risk: 'confirm_required',
634
+ rationale: '自指指针对新版 CLI 无效但会持续触发忽略告警;删除后本地模式零残留',
635
+ next_step: `rm "${pointer.path}"(保留 local.yaml 平台凭据)`,
636
+ });
637
+ }
638
+
639
+ // 画像②:陈旧自指声明(指针已删/未写,声明残留——runCommand 降级本地模式不阻断)
640
+ if (!pointer.present && declSelfRef) {
641
+ triggered = true;
642
+ dim.findings.push(
643
+ `repo_native_stale_declaration: 自指平台接管声明残留(原 specRoot 自指回环且恢复指针缺失):` +
644
+ `${join(cwd, PLATFORM_MANAGED_FILENAME)}——新版 CLI 已降级按本地模式运行(不 fail-closed);` +
645
+ `可 sillyspec platform disconnect 清理声明残留`
646
+ );
647
+ dim.safe_actions.push({
648
+ dimension: 'repo_native_chain',
649
+ action: 'platform_disconnect_cleanup',
650
+ risk: 'low',
651
+ rationale: '陈旧自指声明只触发降级告警;disconnect 三清(指针/声明/local.yaml platform 段)彻底出清',
652
+ next_step: 'sillyspec platform disconnect(若仍需上行同步,先确认 local.yaml 凭据再重连)',
653
+ });
654
+ }
655
+
656
+ // 画像③:repo-native 凭据缺失 + 自指残留(design 风险 5:内置 sync best-effort 静默跳过,
657
+ // 上行断链且无感知——CLI 本地模式化后 local.yaml platform 段是唯一上行通道)
658
+ if (existsSync(join(cwd, '.sillyspec')) && !localYamlHasPlatformSection(cwd) && (ptrSelfRef || declSelfRef)) {
659
+ triggered = true;
660
+ dim.findings.push(
661
+ `repo_native_missing_credentials: cwd/.sillyspec 存在但 local.yaml 无 platform 段,` +
662
+ `且存在自指指针/声明残留——内置 sync 因凭据缺失静默跳过,本地变更上行将静默失败` +
663
+ `(平台变更中心看不到,无任何告警)。恢复:sillyspec platform connect <url> <token> 写入凭据`
664
+ );
665
+ dim.safe_actions.push({
666
+ dimension: 'repo_native_chain',
667
+ action: 'platform_connect_credentials',
668
+ risk: 'low',
669
+ rationale: 'repo-native 源项目的上行通道是 CLI 内置 sync(local.yaml platform 段凭据 + resolve-by-root-path),凭据缺失即断链',
670
+ next_step: 'sillyspec platform connect <url> <token>(写入 local.yaml platform 段后自动恢复上行)',
671
+ });
672
+ }
673
+
674
+ dim.pass = !triggered;
675
+ dim.severity = triggered ? CHECK_SEVERITY.WARNING : null;
676
+ if (!triggered) {
677
+ dim.findings.push('无 repo-native 自指残留(指针/接管声明均健康或不存在)');
678
+ }
679
+ return dim;
680
+ }
681
+
583
682
  // ── 主入口 ────────────────────────────────────────────────────────────
584
683
 
585
684
  // ── 文档膨胀检测(token 成本优化 P0b/P2b,2026-08-22-token-cost-optimization)──
@@ -650,8 +749,9 @@ export async function runDoctorDiagnostics({ cwd }) {
650
749
  const authDb = (multiDb.dbs || []).find((d) => d.role === DB_ROLE.AUTHORITY);
651
750
  const executeMismatch = detectExecuteProgressPlanMismatch(authoritySpecDir, authDb);
652
751
  const docBloat = detectDocBloat(authoritySpecDir);
752
+ const repoNativeChain = detectRepoNativeChain(cwd, pointer);
653
753
 
654
- const dimensions = [multiDb, pointerHealth, changesSplit, changeDb, executeMismatch, docBloat];
754
+ const dimensions = [multiDb, pointerHealth, changesSplit, changeDb, executeMismatch, docBloat, repoNativeChain];
655
755
 
656
756
  return {
657
757
  dimensions,