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.
- package/.claude/skills/sillyspec-execute/SKILL.md +8 -0
- package/.claude/skills/sillyspec-quick/SKILL.md +2 -1
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-archive/SKILL.md +87 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-auto/SKILL.md +93 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-commit/SKILL.md +67 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-continue/SKILL.md +47 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-doctor/SKILL.md +76 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-execute/SKILL.md +214 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-explore/SKILL.md +102 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-export/SKILL.md +57 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-init/SKILL.md +177 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-knowledge/SKILL.md +270 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-plan/SKILL.md +156 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-quick/SKILL.md +116 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-resume/SKILL.md +70 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-scan/SKILL.md +79 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-state/SKILL.md +64 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-status/SKILL.md +54 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-verify/SKILL.md +87 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-workspace/SKILL.md +175 -0
- package/.claude/worktrees/sillyspec-data-model-research/.gitattributes +2 -0
- package/.claude/worktrees/sillyspec-data-model-research/.husky/pre-push +20 -0
- package/.claude/worktrees/sillyspec-data-model-research/README.md +203 -0
- package/.claude/worktrees/sillyspec-data-model-research/SKILL.md +91 -0
- package/.claude/worktrees/sillyspec-data-model-research/bin/sillyspec.js +2 -0
- package/.claude/worktrees/sillyspec-data-model-research/logo.jpg +0 -0
- package/.claude/worktrees/sillyspec-data-model-research/package-lock.json +977 -0
- package/.claude/worktrees/sillyspec-data-model-research/package.json +39 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/assets/index-Bq_Z2hne.js +7446 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/favicon.jpg +0 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/index.html +16 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/logo.jpg +0 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/prototype-dashboard.html +836 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/prototype-overview.html +256 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/package.json +25 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/executor.js +95 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/index.js +679 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/parser.js +526 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/watcher.js +357 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/agent-session-log.js +803 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/change-list.js +290 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/change-risk-profile.js +270 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/check-primitives.js +65 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/classify-change.js +109 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/commit-suggest.js +202 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/config-cat.js +88 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/config-schema.js +328 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/constants.js +97 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/contract-matrix.js +480 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/datetime.js +20 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/db-engine.js +55 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/db.js +381 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/decision-distill.js +517 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/backends/local-agent.js +155 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/backends/sillyhub-mcp.js +245 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/probe.js +246 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/strategy.js +121 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/docs-check.js +825 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/docs-debt.js +393 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/docs-gate.js +107 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/doctor-diagnostics.js +1099 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/endpoint-extractor.js +420 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/foreign-declared.js +78 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/fs-atomic.js +73 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/git-helper.js +150 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/hooks/claude-pre-tool-use.cjs +130 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/hooks/worktree-guard.js +892 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/index.js +2798 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/init.js +626 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/knowledge-match.js +231 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/local-detect.js +128 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/local-register.js +104 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/machine-interface.js +543 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/migrate.js +127 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/module-impact.js +166 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/module-resolve.js +222 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/modules.js +580 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/progress/change-registry.js +336 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/progress/consistency-doctor.js +355 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/progress/shared.js +36 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/progress/stage-machine.js +594 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/progress/step-store.js +265 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/progress.js +1304 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/quick-recommend.js +115 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/quicklog.js +852 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/review-tier.js +77 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/command.js +1595 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/complete-handlers.js +1552 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/complete.js +1259 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/concurrent-detect.js +156 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/gates.js +1084 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/multi-repo-context.js +208 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/next.js +128 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/prompt.js +843 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/quick-audit.js +186 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/scan-profile.js +269 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/shared.js +1562 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/stage.js +584 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run.js +23 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/scan-diff.js +388 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/scan-postcheck.js +512 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/scan-staleness.js +95 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/setup.js +414 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/sillyhub-mcp/client.js +597 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/sillyhub-mcp/config.js +62 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/spec-dir-typo.js +50 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/spec-sync.js +358 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stage-contract-engine.js +171 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stage-contract-spec.js +550 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stage-contract.js +889 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stage-review.js +597 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/archive.js +199 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/brainstorm-auto.js +236 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/brainstorm.js +617 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/cmd-existence.js +145 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/doctor.js +540 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/execute.js +1275 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/explore.js +43 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/index.js +29 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/knowledge.js +514 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/plan-postcheck.js +1344 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/plan.js +650 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/quick.js +167 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/scan.js +608 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/status.js +64 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/verify.js +246 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/sync.js +1470 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/task-review.js +1498 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/taskcard.js +210 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/verify-postcheck.js +1718 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/verify-probes.js +277 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/version.js +19 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/workflow.js +600 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/workspace.js +125 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/worktree-apply.js +1446 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/worktree-deps.js +576 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/worktree.js +1715 -0
- package/.claude/worktrees/sillyspec-data-model-research/templates/claude-instruction.md +23 -0
- package/.claude/worktrees/sillyspec-data-model-research/templates/prompts/taskcard-rules.md +28 -0
- package/.claude/worktrees/sillyspec-data-model-research/templates/prompts/testcase-design.md +12 -0
- package/.claude/worktrees/sillyspec-data-model-research/templates/prompts/verify-probes.md +32 -0
- package/.claude/worktrees/sillyspec-data-model-research/templates/workflows/archive-impact.yaml +79 -0
- package/.claude/worktrees/sillyspec-data-model-research/templates/workflows/scan-docs.yaml +132 -0
- package/eval/README.md +86 -0
- package/eval/eval.config.json +28 -0
- package/eval/lib/adapters/cli.mjs +63 -0
- package/eval/lib/adapters/dryrun.mjs +27 -0
- package/eval/lib/config.mjs +82 -0
- package/eval/lib/db.mjs +76 -0
- package/eval/lib/report.mjs +120 -0
- package/eval/lib/runner.mjs +157 -0
- package/eval/lib/tasks.mjs +73 -0
- package/eval/lib/util.mjs +107 -0
- package/eval/lib/workdir.mjs +30 -0
- package/eval/run.mjs +83 -0
- package/eval/tasks/001-slugify/files/package.json +6 -0
- package/eval/tasks/001-slugify/solution/src/slugify.mjs +26 -0
- package/eval/tasks/001-slugify/task.json +6 -0
- package/eval/tasks/001-slugify/verify.mjs +28 -0
- package/eval/tasks/002-each-day/files/package.json +6 -0
- package/eval/tasks/002-each-day/files/src/dates.mjs +4 -0
- package/eval/tasks/002-each-day/solution/src/dates.mjs +30 -0
- package/eval/tasks/002-each-day/task.json +6 -0
- package/eval/tasks/002-each-day/verify.mjs +31 -0
- package/eval/tasks/003-word-report/files/data/notes.txt +5 -0
- package/eval/tasks/003-word-report/files/package.json +6 -0
- package/eval/tasks/003-word-report/solution/bin/report.mjs +36 -0
- package/eval/tasks/003-word-report/task.json +6 -0
- package/eval/tasks/003-word-report/verify.mjs +38 -0
- package/package.json +4 -2
- package/src/agent-session-log.js +45 -5
- package/src/change-risk-profile.js +83 -14
- package/src/config-schema.js +18 -3
- package/src/contract-matrix.js +90 -29
- package/src/datetime.js +20 -0
- package/src/decision-distill.js +517 -0
- package/src/dispatch/backends/local-agent.js +4 -4
- package/src/docs-check.js +227 -0
- package/src/docs-debt.js +187 -24
- package/src/doctor-diagnostics.js +102 -2
- package/src/foreign-declared.js +78 -0
- package/src/index.js +89 -9
- package/src/init.js +9 -7
- package/src/knowledge-match.js +107 -6
- package/src/local-register.js +7 -1
- package/src/module-resolve.js +12 -1
- package/src/modules.js +2 -1
- package/src/plan-adopt-waves.js +126 -0
- package/src/quicklog.js +50 -4
- package/src/run/command.js +122 -43
- package/src/run/complete-handlers.js +48 -19
- package/src/run/complete.js +5 -2
- package/src/run/gates.js +87 -22
- package/src/run/prompt.js +84 -42
- package/src/run/quick-audit.js +11 -0
- package/src/run/shared.js +152 -10
- package/src/run.js +2 -2
- package/src/scan-postcheck.js +30 -17
- package/src/stage-contract-spec.js +2 -1
- package/src/stage-contract.js +68 -4
- package/src/stages/archive.js +53 -12
- package/src/stages/brainstorm-auto.js +7 -0
- package/src/stages/brainstorm.js +11 -0
- package/src/stages/doctor.js +157 -1
- package/src/stages/execute.js +32 -6
- package/src/stages/plan-postcheck.js +76 -17
- package/src/stages/plan.js +1 -0
- package/src/stages/quick.js +6 -1
- package/src/stages/verify.js +31 -14
- package/src/sync.js +41 -3
- package/src/task-review.js +2 -2
- package/src/taskcard-placeholders.js +21 -0
- package/src/taskcard.js +10 -1
- package/src/verify-postcheck.js +315 -13
- package/src/verify-probes.js +46 -4
- package/src/worktree-apply.js +184 -7
- package/src/worktree-deps.js +57 -0
- package/src/worktree.js +201 -58
- package/templates/prompts/taskcard-rules.md +1 -0
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* decision-distill — 决策提炼纯函数(change: 2026-08-23-adopt-harness-practices,FR-02/03/04,D-007@v1)
|
|
3
|
+
*
|
|
4
|
+
* 职责:把变更 decisions.md 中「有实现影响」的决策幂等提炼进 knowledge/decisions/<模块域>.md,
|
|
5
|
+
* 并幂等维护 knowledge/INDEX.md 的 decisions 路由行。写入责任全部在本模块——task-03(archive
|
|
6
|
+
* 接线)只调用,task-04(knowledge-match)只消费,本模块不 import 它们(也不接 DB/网络)。
|
|
7
|
+
*
|
|
8
|
+
* 入选规则(FR-02,rejected 优先):任意 type 的 status=rejected → rejected(留痕防复潮);
|
|
9
|
+
* type∈{architecture,compatibility,boundary,definition,process} 且 status∈{confirmed,accepted}
|
|
10
|
+
* → implemented;type=scope 与 status=superseded 等其余组合不入选(解析保留在 entries 里)。
|
|
11
|
+
* 域三级兜底(FR-03):条目「模块域」字段优先 → 缺失按 impacts 文本中的路径与 _module-map.yaml
|
|
12
|
+
* paths/core_files 前缀匹配(moduleIndex 由调用方注入;未注入时按 knowledgeRoot 同级
|
|
13
|
+
* docs/<项目>/modules/_module-map.yaml 尽力发现)→ 仍未中归 unmapped 域。
|
|
14
|
+
* 条目契约(FR-04,provides decisions_entry 八字段):id/status/anchor/domains/last_confirmed/
|
|
15
|
+
* rationale/reject_reason/revisit_when——implemented/rejected 条目逐字落「状态:/锚点:/最近确认:/
|
|
16
|
+
* 理由:」字段行(rejected 追加「否决理由:/复潮条件:」),domains 由条目所在文件
|
|
17
|
+
* decisions/<域>.md 编码(多域 = 条目扇出到多个域文件)。
|
|
18
|
+
* 幂等:同 ID 同版本重写该条目段落不重复追加;@vN+1 整段替换旧版本段并注 supersedes;旧版本段
|
|
19
|
+
* 散落在其他域文件时同步移除(同号全局只留最高版本;高版本已落库时迟到的低版本不降级)。
|
|
20
|
+
* needsWait:rejected 条目缺否决理由/复潮条件 → 该条目不写盘,needsWait 返回缺失描述
|
|
21
|
+
* (其余条目照常提炼;步骤层把 needsWait 转 --wait 请用户裁决)。
|
|
22
|
+
* 零输出:无 decisions.md 或无入选条目 → skipped 带原因,不写任何文件、不动 INDEX。
|
|
23
|
+
*/
|
|
24
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
|
|
25
|
+
import { dirname, join } from 'node:path'
|
|
26
|
+
|
|
27
|
+
/** FR-02:有实现影响的五类 type(scope 与未知 type 的 confirmed/accepted 不入选) */
|
|
28
|
+
const IMPLEMENTED_TYPES = new Set(['architecture', 'compatibility', 'boundary', 'definition', 'process'])
|
|
29
|
+
/** FR-02:implemented 入选的 status(superseded 等其余状态不入选) */
|
|
30
|
+
const IMPLEMENTED_STATUSES = new Set(['confirmed', 'accepted'])
|
|
31
|
+
/** 三级兜底终点域(仍不中归此;域文件由本模块自管) */
|
|
32
|
+
const UNMAPPED_DOMAIN = 'unmapped'
|
|
33
|
+
/** 旧格式容错占位值(FR-01:缺锚点/缺 headHash 不阻断) */
|
|
34
|
+
const NOT_RECORDED = '未记录'
|
|
35
|
+
|
|
36
|
+
/** 多行/带空白值压成单行字段值(条目行是单行契约) */
|
|
37
|
+
function oneLine(s) {
|
|
38
|
+
return String(s ?? '').replace(/\s*\r?\n\s*/g, ' ').trim()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 取列表字段首 token:`superseded(被 D-005@v2 取代…)` → `superseded` */
|
|
42
|
+
function firstToken(value) {
|
|
43
|
+
return value.split(/[\s((]/)[0] || ''
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 模块域值解析:`[a, b]` / `a、b` / `a b` → ['a','b'] */
|
|
47
|
+
function parseListValue(v) {
|
|
48
|
+
return v.replace(/^\[/, '').replace(/\]$/, '')
|
|
49
|
+
.split(/[,,、\s]+/)
|
|
50
|
+
.map(s => s.trim())
|
|
51
|
+
.filter(Boolean)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 域名净化为安全文件名段(防路径注入;空值归 unmapped) */
|
|
55
|
+
function sanitizeDomain(d) {
|
|
56
|
+
const s = String(d || '').trim().replace(/\.md$/i, '').replace(/[^A-Za-z0-9_\-\u4e00-\u9fff]/g, '-')
|
|
57
|
+
return s || UNMAPPED_DOMAIN
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** FR-02 入选裁决:'implemented' | 'rejected' | null(null = 解析保留但不入选) */
|
|
61
|
+
function dispositionOf(entry) {
|
|
62
|
+
const t = (entry.type || '').toLowerCase()
|
|
63
|
+
const s = (entry.status || '').toLowerCase()
|
|
64
|
+
if (s === 'rejected') return 'rejected' // 任意 type(含 scope)的 rejected 都留痕
|
|
65
|
+
if (IMPLEMENTED_TYPES.has(t) && IMPLEMENTED_STATUSES.has(s)) return 'implemented'
|
|
66
|
+
return null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** decisions.md 列表字段标签 → entry 字段(兼容中英标签与中英冒号;其余字段仅留在 raw) */
|
|
70
|
+
function applyField(entry, label, value) {
|
|
71
|
+
switch (label) {
|
|
72
|
+
case 'type': entry.type = firstToken(value); break
|
|
73
|
+
case 'status': entry.status = firstToken(value); break
|
|
74
|
+
case 'question': case '问题': entry.question = value; break
|
|
75
|
+
case 'answer': case '答案': entry.answer = value; break
|
|
76
|
+
case '锚点': case 'anchor': entry.anchor = value; break
|
|
77
|
+
case '模块域': case '模块': case 'domains': case 'domain': entry.domains = parseListValue(value); break
|
|
78
|
+
case '否决理由': case 'reject_reason': case 'rejectreason': entry.rejectReason = value; break
|
|
79
|
+
case '复潮条件': case 'revisit_when': case 'revisitwhen': entry.revisitWhen = value; break
|
|
80
|
+
case 'supersedes': entry.supersedes = value; break
|
|
81
|
+
case 'impacts': case '影响': entry.impacts = value; break
|
|
82
|
+
case 'normalized_requirement': entry.normalizedRequirement = value; break
|
|
83
|
+
default: break
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 纯函数:解析变更 decisions.md 的 D-xxx@vN 决策条目(FR-01 四字段全可选容旧格式)。
|
|
89
|
+
* @param {string} changeDir 变更目录(含 decisions.md)
|
|
90
|
+
* @returns {{ entries: Array<{
|
|
91
|
+
* id: string, number: string, version: number, title: string,
|
|
92
|
+
* type?: string, status?: string, question?: string, answer?: string,
|
|
93
|
+
* anchor?: string, domains?: string[], rejectReason?: string, revisitWhen?: string,
|
|
94
|
+
* supersedes?: string, impacts?: string, normalizedRequirement?: string,
|
|
95
|
+
* selected: 'implemented'|'rejected'|null, raw: string }>, missing: boolean }}
|
|
96
|
+
* missing=true = decisions.md 不存在;selected = FR-02 入选裁决,null 表示解析保留但不入选。
|
|
97
|
+
*/
|
|
98
|
+
export function parseDecisions(changeDir) {
|
|
99
|
+
const filePath = join(changeDir, 'decisions.md')
|
|
100
|
+
if (!existsSync(filePath)) return { entries: [], missing: true }
|
|
101
|
+
let content
|
|
102
|
+
try { content = readFileSync(filePath, 'utf8') } catch { return { entries: [], missing: true } }
|
|
103
|
+
|
|
104
|
+
const entries = []
|
|
105
|
+
let cur = null
|
|
106
|
+
const flush = () => {
|
|
107
|
+
if (!cur) return
|
|
108
|
+
cur.selected = dispositionOf(cur)
|
|
109
|
+
cur.raw = cur.rawLines.join('\n')
|
|
110
|
+
delete cur.rawLines
|
|
111
|
+
entries.push(cur)
|
|
112
|
+
}
|
|
113
|
+
for (const line of content.replace(/\r\n/g, '\n').split('\n')) {
|
|
114
|
+
const h = line.match(/^##\s+(D-\d+)(?:@v(\d+))?\s*(.*)$/)
|
|
115
|
+
if (h) {
|
|
116
|
+
flush()
|
|
117
|
+
const version = h[2] ? parseInt(h[2], 10) : 1
|
|
118
|
+
cur = { id: `${h[1]}@v${version}`, number: h[1], version, title: h[3].trim(), rawLines: [line] }
|
|
119
|
+
continue
|
|
120
|
+
}
|
|
121
|
+
if (!cur) continue
|
|
122
|
+
cur.rawLines.push(line)
|
|
123
|
+
const f = line.match(/^-\s+([^\s::]+)\s*[::]\s*(.*)$/)
|
|
124
|
+
if (f) applyField(cur, f[1].toLowerCase(), f[2].trim())
|
|
125
|
+
}
|
|
126
|
+
flush()
|
|
127
|
+
return { entries, missing: false }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// 域三级兜底(FR-03)
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
/** moduleIndex 形状归一:parseModuleMapSimple 平铺 {id:{paths}} 或包裹 {modules:{id:{paths}}} */
|
|
135
|
+
function normalizeModules(moduleIndex) {
|
|
136
|
+
if (!moduleIndex) return {}
|
|
137
|
+
return moduleIndex.modules ? moduleIndex.modules : moduleIndex
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** _module-map.yaml 子集解析(仅模块 id → paths/core_files;不复用 modules.js 以保零依赖纯函数) */
|
|
141
|
+
function parseModulePathsSubset(content) {
|
|
142
|
+
const modules = {}
|
|
143
|
+
let cur = null
|
|
144
|
+
let key = null
|
|
145
|
+
for (const line of content.replace(/\r\n/g, '\n').split('\n')) {
|
|
146
|
+
const mm = line.match(/^ ([a-zA-Z0-9_-]+):$/)
|
|
147
|
+
if (mm) { cur = mm[1]; modules[cur] = { paths: [] }; key = null; continue }
|
|
148
|
+
if (!cur) continue
|
|
149
|
+
const inline = line.match(/^ (paths|core_files): \[(.*)\]$/)
|
|
150
|
+
if (inline) {
|
|
151
|
+
modules[cur][inline[1]] = inline[2].split(',').map(s => s.trim().replace(/^['"]|['"]$/g, '')).filter(Boolean)
|
|
152
|
+
key = null; continue
|
|
153
|
+
}
|
|
154
|
+
const block = line.match(/^ (paths|core_files):$/)
|
|
155
|
+
if (block) { key = block[1]; modules[cur][key] = modules[cur][key] || []; continue }
|
|
156
|
+
const item = line.match(/^ - (.+)$/)
|
|
157
|
+
if (item && key) modules[cur][key].push(item[1].trim().replace(/^['"]|['"]$/g, ''))
|
|
158
|
+
}
|
|
159
|
+
return modules
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** 未注入 moduleIndex 时的尽力发现:knowledgeRoot 同级 docs/<项目>/modules/_module-map.yaml。
|
|
163
|
+
* 多子项目仓(如本仓 sillyspec+dashboard)各项目有自己的 map——dogfood 实证(2026-08-24):
|
|
164
|
+
* 只取首个命中会把主项目条目全部误落 unmapped(dashboard 的 map 按字母序先命中)。
|
|
165
|
+
* 改为合并全部项目的 map(同名模块合并 paths/core_files 去重),失败归 null → 全部 unmapped。 */
|
|
166
|
+
function discoverModuleIndex(knowledgeRoot) {
|
|
167
|
+
try {
|
|
168
|
+
const docsDir = join(dirname(knowledgeRoot), 'docs')
|
|
169
|
+
if (!existsSync(docsDir)) return null
|
|
170
|
+
const merged = {}
|
|
171
|
+
for (const d of readdirSync(docsDir, { withFileTypes: true })) {
|
|
172
|
+
if (!d.isDirectory()) continue
|
|
173
|
+
const mapPath = join(docsDir, d.name, 'modules', '_module-map.yaml')
|
|
174
|
+
if (!existsSync(mapPath)) continue
|
|
175
|
+
const sub = parseModulePathsSubset(readFileSync(mapPath, 'utf8'))
|
|
176
|
+
for (const [id, m] of Object.entries(sub)) {
|
|
177
|
+
if (!merged[id]) merged[id] = { paths: m.paths || [], core_files: m.core_files || [] }
|
|
178
|
+
else {
|
|
179
|
+
merged[id].paths = [...new Set([...(merged[id].paths || []), ...(m.paths || [])])]
|
|
180
|
+
merged[id].core_files = [...new Set([...(merged[id].core_files || []), ...(m.core_files || [])])]
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return Object.keys(merged).length > 0 ? merged : null
|
|
185
|
+
} catch { /* 尽力而为,失败归 null */ }
|
|
186
|
+
return null
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** impacts 文本中的路径 token:含 / 或带扩展名的词(`src/stages/archive.js、quicklog.js:486` → 前者 + `quicklog.js`) */
|
|
190
|
+
function extractPathTokens(text) {
|
|
191
|
+
if (!text) return []
|
|
192
|
+
const tokens = String(text).match(/[A-Za-z0-9_\-.\\/]+/g) || []
|
|
193
|
+
return tokens
|
|
194
|
+
.map(t => t.replace(/\\/g, '/').replace(/\/+$/, ''))
|
|
195
|
+
.filter(t => t && (t.includes('/') || /\.[A-Za-z0-9]+$/.test(t)) && t !== '.' && t !== '..')
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** impacts 路径 token × module paths 前缀匹配(全路径优先,基名兜底——卡内裸文件名引用先例) */
|
|
199
|
+
function matchImpactsToModules(impacts, moduleIndex) {
|
|
200
|
+
const flat = normalizeModules(moduleIndex)
|
|
201
|
+
const modPaths = []
|
|
202
|
+
for (const [id, m] of Object.entries(flat)) {
|
|
203
|
+
for (const p of [...(m.paths || []), ...(m.core_files || [])]) {
|
|
204
|
+
const ps = String(p).replace(/\\/g, '/').replace(/\/+$/, '')
|
|
205
|
+
if (ps) modPaths.push({ id, p: ps })
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const tokens = extractPathTokens(impacts)
|
|
209
|
+
const hits = []
|
|
210
|
+
for (const t of tokens) {
|
|
211
|
+
for (const { id, p } of modPaths) {
|
|
212
|
+
if (t === p || t.startsWith(p + '/')) { if (!hits.includes(id)) hits.push(id); break }
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (hits.length === 0) {
|
|
216
|
+
for (const t of tokens) {
|
|
217
|
+
const base = t.slice(t.lastIndexOf('/') + 1)
|
|
218
|
+
for (const { id, p } of modPaths) {
|
|
219
|
+
if (p.slice(p.lastIndexOf('/') + 1) === base && !hits.includes(id)) hits.push(id)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return hits
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** 三级兜底:模块域字段 → impacts × moduleIndex 前缀 → unmapped */
|
|
227
|
+
function resolveDomains(entry, moduleIndex) {
|
|
228
|
+
if (entry.domains && entry.domains.length > 0) return entry.domains.map(sanitizeDomain)
|
|
229
|
+
const matched = moduleIndex ? matchImpactsToModules(entry.impacts, moduleIndex) : []
|
|
230
|
+
if (matched.length > 0) return matched.map(sanitizeDomain)
|
|
231
|
+
return [UNMAPPED_DOMAIN]
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
// knowledge/decisions/<域>.md 条目读写(FR-04 契约条目,幂等)
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
|
|
238
|
+
/** 新域文件头(preamble) */
|
|
239
|
+
function newDomainPreamble(domain) {
|
|
240
|
+
return [
|
|
241
|
+
`# 决策知识 — ${domain}`,
|
|
242
|
+
'',
|
|
243
|
+
'> decision-distill 从变更 decisions.md 幂等提炼(「最近确认」= 归档时 HEAD)。条目字段行为 docs-check 机械解析契约,勿手改。',
|
|
244
|
+
]
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** 单条目段落渲染(design 接口定义的条目格式,逐字对齐:状态/锚点/最近确认/理由 + rejected 专属两行) */
|
|
248
|
+
function renderBlockLines(entry, headHash, supersedesNote) {
|
|
249
|
+
const lines = [`## ${entry.id} ${oneLine(entry.title)}`.replace(/\s+$/, '')]
|
|
250
|
+
lines.push(`状态:${entry.selected}`)
|
|
251
|
+
lines.push(`锚点:${oneLine(entry.anchor) || NOT_RECORDED}`)
|
|
252
|
+
lines.push(`最近确认:${oneLine(headHash) || NOT_RECORDED}`)
|
|
253
|
+
lines.push(`理由:${oneLine(entry.answer || entry.normalizedRequirement || entry.question)}`)
|
|
254
|
+
if (supersedesNote) lines.push(`supersedes:${oneLine(supersedesNote)}`)
|
|
255
|
+
if (entry.selected === 'rejected') {
|
|
256
|
+
lines.push(`否决理由:${oneLine(entry.rejectReason)}`)
|
|
257
|
+
lines.push(`复潮条件:${oneLine(entry.revisitWhen)}`)
|
|
258
|
+
}
|
|
259
|
+
return lines
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** 知识文件切段:preamble 行 + D-xxx@vN 条目段(段边界 = 下一 `## D-` 头) */
|
|
263
|
+
function splitKnowledgeSections(content) {
|
|
264
|
+
const sections = []
|
|
265
|
+
const preamble = []
|
|
266
|
+
let cur = null
|
|
267
|
+
for (const line of content.replace(/\r\n/g, '\n').split('\n')) {
|
|
268
|
+
const m = line.match(/^## (D-\d+)@v(\d+)\s*(.*)$/)
|
|
269
|
+
if (m) {
|
|
270
|
+
if (cur) sections.push(cur)
|
|
271
|
+
cur = { number: m[1], version: parseInt(m[2], 10), title: m[3].trim(), lines: [line] }
|
|
272
|
+
continue
|
|
273
|
+
}
|
|
274
|
+
if (cur) cur.lines.push(line)
|
|
275
|
+
else preamble.push(line)
|
|
276
|
+
}
|
|
277
|
+
if (cur) sections.push(cur)
|
|
278
|
+
for (const s of sections) s.id = `${s.number}@v${s.version}`
|
|
279
|
+
return { preamble, sections }
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** 段落重组落盘文本(幂等归一:段间恰好一个空行 + 文件单一尾换行) */
|
|
283
|
+
function joinKnowledgeFile(preamble, sections) {
|
|
284
|
+
const parts = []
|
|
285
|
+
const pre = preamble.join('\n').replace(/\s+$/, '')
|
|
286
|
+
if (pre) parts.push(pre)
|
|
287
|
+
for (const s of sections) {
|
|
288
|
+
const block = s.lines.join('\n').replace(/\s+$/, '')
|
|
289
|
+
if (block) parts.push(block)
|
|
290
|
+
}
|
|
291
|
+
return parts.join('\n\n') + '\n'
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* 单条目写入一个域文件的段落集(就地修改 sections)。
|
|
296
|
+
* @returns {{ action: 'append'|'update'|'supersede' }|null} null = 高版本已落库,迟到低版本不降级。
|
|
297
|
+
*/
|
|
298
|
+
function applyEntryToSections(sections, entry, headHash) {
|
|
299
|
+
const sameNumber = sections.filter(s => s.number === entry.number)
|
|
300
|
+
if (sameNumber.some(s => s.version > entry.version)) return null
|
|
301
|
+
let action
|
|
302
|
+
let supersedesNote = oneLine(entry.supersedes) || null
|
|
303
|
+
if (sameNumber.length === 0) {
|
|
304
|
+
sections.push({ number: entry.number, version: entry.version, id: entry.id, title: entry.title, lines: renderBlockLines(entry, headHash, supersedesNote) })
|
|
305
|
+
action = 'append'
|
|
306
|
+
} else {
|
|
307
|
+
const same = sameNumber.find(s => s.version === entry.version)
|
|
308
|
+
const slot = same || sameNumber[0]
|
|
309
|
+
if (!same && !supersedesNote) supersedesNote = `${slot.number}@v${slot.version}`
|
|
310
|
+
slot.version = entry.version
|
|
311
|
+
slot.title = entry.title
|
|
312
|
+
slot.lines = renderBlockLines(entry, headHash, supersedesNote)
|
|
313
|
+
// 同号旧版本段(< 新版本)就地清除——同文件内同号只留一个段
|
|
314
|
+
for (let i = sections.length - 1; i >= 0; i--) {
|
|
315
|
+
if (sections[i].number === entry.number && sections[i] !== slot) sections.splice(i, 1)
|
|
316
|
+
}
|
|
317
|
+
action = same ? 'update' : 'supersede'
|
|
318
|
+
}
|
|
319
|
+
return { action }
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ---------------------------------------------------------------------------
|
|
323
|
+
// INDEX.md decisions 路由行幂等维护(FR-05:写入责任在本模块;只动 ## Decisions 段内指向 decisions/ 的行)
|
|
324
|
+
// ---------------------------------------------------------------------------
|
|
325
|
+
|
|
326
|
+
/** INDEX 路由行格式与 knowledge-match 解析口径一致:`- <域>|decision|决策 → [decisions/<域>.md](decisions/<域>.md)` */
|
|
327
|
+
function routingLine(domain) {
|
|
328
|
+
return `- ${domain}|decision|决策 → [decisions/${domain}.md](decisions/${domain}.md)`
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const ROUTING_TARGET_RE = /^-\s+.*→\s*\[[^\]]*\]\(decisions\/([^)#]+?)(?:\.md)?(?:#[^)]*)?\)\s*$/
|
|
332
|
+
|
|
333
|
+
/** 域文件实况 ↔ ## Decisions 段路由行对账:缺行补、失效行删、重复行去重;不动其他类别行 */
|
|
334
|
+
function syncIndexRoutingLines(knowledgeRoot) {
|
|
335
|
+
const decisionsDir = join(knowledgeRoot, 'decisions')
|
|
336
|
+
let domains
|
|
337
|
+
try {
|
|
338
|
+
domains = readdirSync(decisionsDir).filter(f => f.endsWith('.md')).map(f => f.replace(/\.md$/, ''))
|
|
339
|
+
} catch { return false }
|
|
340
|
+
if (domains.length === 0) return false
|
|
341
|
+
|
|
342
|
+
const indexPath = join(knowledgeRoot, 'INDEX.md')
|
|
343
|
+
let original = null
|
|
344
|
+
if (existsSync(indexPath)) {
|
|
345
|
+
try { original = readFileSync(indexPath, 'utf8') } catch { return false }
|
|
346
|
+
}
|
|
347
|
+
const lines = original ? original.replace(/\r\n/g, '\n').split('\n') : ['# Knowledge Index', '']
|
|
348
|
+
|
|
349
|
+
// 定位 ## Decisions 段边界
|
|
350
|
+
let secStart = -1
|
|
351
|
+
let secEnd = lines.length
|
|
352
|
+
for (let i = 0; i < lines.length; i++) {
|
|
353
|
+
const h = lines[i].match(/^##\s+(.+?)\s*$/)
|
|
354
|
+
if (!h) continue
|
|
355
|
+
if (secStart === -1) {
|
|
356
|
+
if (h[1] === 'Decisions') secStart = i + 1
|
|
357
|
+
} else { secEnd = i; break }
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const desired = domains.map(routingLine)
|
|
361
|
+
if (secStart === -1) {
|
|
362
|
+
// 无段则追加(保留原文件其余内容)
|
|
363
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === '') lines.pop()
|
|
364
|
+
lines.push('', '## Decisions', ...desired)
|
|
365
|
+
} else {
|
|
366
|
+
const body = lines.slice(secStart, secEnd)
|
|
367
|
+
const kept = []
|
|
368
|
+
const seen = new Set()
|
|
369
|
+
const domainSet = new Set(domains)
|
|
370
|
+
for (const l of body) {
|
|
371
|
+
const m = l.match(ROUTING_TARGET_RE)
|
|
372
|
+
if (!m) { kept.push(l); continue } // 非路由行(注释/空行)原样保留
|
|
373
|
+
// 目标域文件仍存在的路由行原样保留(关键词可能被人工扩充);失效行(文件已不存在)与重复行删
|
|
374
|
+
if (domainSet.has(m[1]) && !seen.has(m[1])) { seen.add(m[1]); kept.push(l) }
|
|
375
|
+
}
|
|
376
|
+
// 补行插在段尾空行之前(段尾空行是与下一 `## ` 的分隔,新行追加其后会出现行间空洞)
|
|
377
|
+
const tail = []
|
|
378
|
+
while (kept.length > 0 && kept[kept.length - 1].trim() === '') tail.unshift(kept.pop())
|
|
379
|
+
for (const d of desired) {
|
|
380
|
+
const m = d.match(ROUTING_TARGET_RE)
|
|
381
|
+
if (m && !seen.has(m[1])) kept.push(d) // 缺行补
|
|
382
|
+
}
|
|
383
|
+
kept.push(...tail)
|
|
384
|
+
const sepNeeded = secEnd < lines.length && kept.length > 0 && kept[kept.length - 1].trim() !== ''
|
|
385
|
+
lines.splice(secStart, secEnd - secStart, ...kept, ...(sepNeeded ? [''] : []))
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
let next = lines.join('\n').replace(/\n{3,}$/, '\n')
|
|
389
|
+
if (!next.endsWith('\n')) next += '\n'
|
|
390
|
+
if (next === original) return false
|
|
391
|
+
writeFileSync(indexPath, next)
|
|
392
|
+
return true
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// ---------------------------------------------------------------------------
|
|
396
|
+
// IO 入口:提炼(FR-03)
|
|
397
|
+
// ---------------------------------------------------------------------------
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* 纯函数入口:把 changeDir/decisions.md 的入选决策幂等提炼进 knowledgeRoot/decisions/<域>.md,
|
|
401
|
+
* 并幂等维护 knowledgeRoot/INDEX.md 的 decisions 路由行(仅在有实际写入时)。
|
|
402
|
+
* @param {string} changeDir 变更目录(含 decisions.md)
|
|
403
|
+
* @param {string} knowledgeRoot knowledge 目录(如 <specBase>/knowledge)
|
|
404
|
+
* @param {string} headHash 归档时 HEAD hash(落「最近确认」;空值容错为 未记录)
|
|
405
|
+
* @param {object|null} [moduleIndex] 域兜底用模块索引({modules:{id:{paths,core_files}}} 或平铺形;
|
|
406
|
+
* 缺省时按 knowledgeRoot 同级 docs/<项目>/modules/_module-map.yaml 尽力发现)
|
|
407
|
+
* @returns {{ written: Array<{file: string, id: string, action: 'append'|'update'|'supersede'}>,
|
|
408
|
+
* skipped: string|null, needsWait: string|null }}
|
|
409
|
+
* file 为 knowledgeRoot 相对 POSIX 路径(decisions/<域>.md);跨文件旧版本移除记
|
|
410
|
+
* { file: 所在文件, id: 被替换旧版本, action: 'supersede' }。
|
|
411
|
+
* skipped 非空 = 无 decisions.md / 无入选条目(零输出);needsWait 非空 = 有 rejected 条目
|
|
412
|
+
* 缺否决理由/复潮条件被拦下(该条目未写盘,其余条目照常提炼)。
|
|
413
|
+
*/
|
|
414
|
+
export function distillIntoKnowledge(changeDir, knowledgeRoot, headHash, moduleIndex = null) {
|
|
415
|
+
const parsed = parseDecisions(changeDir)
|
|
416
|
+
if (parsed.missing) {
|
|
417
|
+
return { written: [], skipped: `decisions.md 不存在(${join(changeDir, 'decisions.md')}),零输出`, needsWait: null }
|
|
418
|
+
}
|
|
419
|
+
const selected = parsed.entries.filter(e => e.selected)
|
|
420
|
+
if (selected.length === 0) {
|
|
421
|
+
return { written: [], skipped: `无入选条目(解析 ${parsed.entries.length} 条,0 条入选:scope/非五类 type/非 confirmed|accepted|rejected 状态均不入选),零输出`, needsWait: null }
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// needsWait:rejected 缺否决理由/复潮条件 → 该条目不写盘(其余照常)
|
|
425
|
+
const waitParts = []
|
|
426
|
+
const distillable = []
|
|
427
|
+
for (const e of selected) {
|
|
428
|
+
if (e.selected === 'rejected' && (!e.rejectReason || !e.revisitWhen)) {
|
|
429
|
+
const missing = []
|
|
430
|
+
if (!e.rejectReason) missing.push('否决理由')
|
|
431
|
+
if (!e.revisitWhen) missing.push('复潮条件')
|
|
432
|
+
waitParts.push(`${e.id} 缺${missing.join('、')}`)
|
|
433
|
+
} else {
|
|
434
|
+
distillable.push(e)
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
const needsWait = waitParts.length > 0
|
|
438
|
+
? `${waitParts.join(';')}——rejected 条目必填,补齐后重跑提炼`
|
|
439
|
+
: null
|
|
440
|
+
if (distillable.length === 0) return { written: [], skipped: null, needsWait }
|
|
441
|
+
|
|
442
|
+
// 同批同号只提炼最高可写版本(decisions.md 里 v1+v2 并存时 v1 不先落后清——落了也会被 v2
|
|
443
|
+
// 整段替换,跳过避免空转写与日志抖动;v2 被 needsWait 拦下时 v1 仍是最高可写版,照常提炼)
|
|
444
|
+
const highestByNumber = new Map()
|
|
445
|
+
for (const e of distillable) {
|
|
446
|
+
const prev = highestByNumber.get(e.number)
|
|
447
|
+
if (!prev || e.version > prev.version) highestByNumber.set(e.number, e)
|
|
448
|
+
}
|
|
449
|
+
const batch = []
|
|
450
|
+
const taken = new Set()
|
|
451
|
+
for (const e of distillable) {
|
|
452
|
+
const keep = highestByNumber.get(e.number)
|
|
453
|
+
if (keep && !taken.has(e.number)) { taken.add(e.number); batch.push(keep) }
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const decisionsDir = join(knowledgeRoot, 'decisions')
|
|
457
|
+
mkdirSync(decisionsDir, { recursive: true })
|
|
458
|
+
const idx = moduleIndex || discoverModuleIndex(knowledgeRoot)
|
|
459
|
+
|
|
460
|
+
// 域文件 store:域 → { preamble, sections, dirty }(按需加载,批量写盘)
|
|
461
|
+
const store = new Map()
|
|
462
|
+
const loadDomain = (domain) => {
|
|
463
|
+
let st = store.get(domain)
|
|
464
|
+
if (st) return st
|
|
465
|
+
const p = join(decisionsDir, `${domain}.md`)
|
|
466
|
+
if (existsSync(p)) {
|
|
467
|
+
let parsedFile = { preamble: [], sections: [] }
|
|
468
|
+
try { parsedFile = splitKnowledgeSections(readFileSync(p, 'utf8')) } catch { /* 按新文件处理 */ }
|
|
469
|
+
st = { ...parsedFile, dirty: false }
|
|
470
|
+
} else {
|
|
471
|
+
st = { preamble: newDomainPreamble(domain), sections: [], dirty: false }
|
|
472
|
+
}
|
|
473
|
+
store.set(domain, st)
|
|
474
|
+
return st
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const written = []
|
|
478
|
+
const targetSets = new Map() // entry → Set(域)(跨文件清理时排除目标文件)
|
|
479
|
+
for (const entry of batch) {
|
|
480
|
+
const domains = resolveDomains(entry, idx)
|
|
481
|
+
targetSets.set(entry, new Set(domains))
|
|
482
|
+
for (const d of domains) {
|
|
483
|
+
const st = loadDomain(d)
|
|
484
|
+
const res = applyEntryToSections(st.sections, entry, headHash)
|
|
485
|
+
if (res) {
|
|
486
|
+
st.dirty = true
|
|
487
|
+
written.push({ file: `decisions/${d}.md`, id: entry.id, action: res.action })
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// 跨文件清理:同号 ≤ 新版本的旧段散落其他域文件时移除(同号全局只留最高版本/目标文件)
|
|
493
|
+
let onDisk = []
|
|
494
|
+
try { onDisk = readdirSync(decisionsDir).filter(f => f.endsWith('.md')).map(f => f.replace(/\.md$/, '')) } catch { /* 刚创建,走 store */ }
|
|
495
|
+
const allDomains = [...new Set([...onDisk, ...store.keys()])]
|
|
496
|
+
for (const entry of batch) {
|
|
497
|
+
const targets = targetSets.get(entry)
|
|
498
|
+
for (const d of allDomains) {
|
|
499
|
+
if (targets.has(d)) continue
|
|
500
|
+
const st = loadDomain(d)
|
|
501
|
+
const removed = st.sections.filter(s => s.number === entry.number && s.version <= entry.version)
|
|
502
|
+
if (removed.length === 0) continue
|
|
503
|
+
st.sections = st.sections.filter(s => !removed.includes(s))
|
|
504
|
+
st.dirty = true
|
|
505
|
+
for (const r of removed) written.push({ file: `decisions/${d}.md`, id: r.id, action: 'supersede' })
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
for (const [domain, st] of store) {
|
|
510
|
+
if (st.dirty) writeFileSync(join(decisionsDir, `${domain}.md`), joinKnowledgeFile(st.preamble, st.sections))
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// INDEX 路由行对账(仅在实际有条目写入时;零输出场景不动 INDEX)
|
|
514
|
+
if (written.length > 0) syncIndexRoutingLines(knowledgeRoot)
|
|
515
|
+
|
|
516
|
+
return { written, skipped: null, needsWait }
|
|
517
|
+
}
|
|
@@ -31,7 +31,7 @@ SillySpec 主体(你,调度 agent)对 worktree **工作区**做 git diff
|
|
|
31
31
|
|
|
32
32
|
1. 读取该 task 在 worktree 工作区的 git diff(task 开始 → 完成的变更)
|
|
33
33
|
2. 对照 plan.md 与 tasks/task-XX.md 检查实现(不信任 implementer 自报,只看当前 task 的 diff)
|
|
34
|
-
3. 写入 review.json
|
|
34
|
+
3. 写入 review.json:\`{SPEC_ROOT}/.runtime/execute-runs/<runId>/tasks/task-XX/review.json\`({SPEC_ROOT} 由 CLI 渲染时替换为主仓绝对路径——调度者 cwd 无论在哪都写主仓 .runtime,不写 worktree 副本)
|
|
35
35
|
4. review.json 写入成功后,该 task 的派发回收完成
|
|
36
36
|
|
|
37
37
|
review.json 复用既有 task-review 契约(必填字段:schemaVersion / task / base / head /
|
|
@@ -126,8 +126,8 @@ export function renderLocalInstruction(contract) {
|
|
|
126
126
|
主工作区、**破坏 worktree 隔离**,后续 assess / apply / review.json 全部错位,并可能
|
|
127
127
|
撞其他并行 agent 的改动。
|
|
128
128
|
${branchLine}
|
|
129
|
-
- 蓝图文件(tasks.md / design.md / tasks/task-XX.md)在主工作区
|
|
130
|
-
|
|
129
|
+
- 蓝图文件(tasks.md / design.md / tasks/task-XX.md)在主工作区 \`{SPEC_ROOT}/changes/<change>/\`
|
|
130
|
+
下({SPEC_ROOT} 由 CLI 渲染时替换为主仓绝对路径),可能不在 worktree 中——**读蓝图用主工作区绝对路径,不要拼接到 worktree 路径下**;
|
|
131
131
|
写代码必须落在 workdir(worktree)内。
|
|
132
132
|
|
|
133
133
|
### 子代理 prompt 要点
|
|
@@ -136,7 +136,7 @@ ${branchLine}
|
|
|
136
136
|
1. 任务目标(brief):${briefLine}
|
|
137
137
|
2. 蓝图文件路径(让子代理按需读取详情,不要内联整份蓝图污染上下文)
|
|
138
138
|
3. 编码铁律:先读后写、TDD、不编造方法、只做蓝图里写的事、遵守边界处理规则、不超出 allowedPaths
|
|
139
|
-
4.
|
|
139
|
+
4. 如存在模块文档(\`{SPEC_ROOT}/docs/*/modules/\`),按需读取涉及模块的 <module>.md 参考接口约定与数据流(读主仓路径,不读 worktree 副本)
|
|
140
140
|
5. 任务含测试代码时,把下方「测试用例设计」整段复制进子代理 prompt,要求子代理按此设计测试用例
|
|
141
141
|
|
|
142
142
|
{{include: testcase-design}}
|