sillyspec 3.29.5 → 3.31.0
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-auto/SKILL.md +1 -1
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +2 -2
- package/.claude/skills/sillyspec-commit/SKILL.md +1 -1
- package/.claude/skills/sillyspec-flow/SKILL.md +57 -0
- package/.claude/skills/sillyspec-quick/SKILL.md +7 -105
- package/AGENTS.md +38 -20
- package/SKILL.md +9 -3
- package/assets/command-cards/flow.md +32 -0
- package/assets/command-cards/run-brainstorm.md +1 -1
- package/assets/command-cards/run-quick.md +8 -25
- package/package.json +2 -1
- package/packages/dashboard/server/executor.js +2 -1
- package/packages/dashboard/server/parser.js +7 -7
- package/r7-test-env/README-R7-TEST.md +93 -0
- package/r7-test-env/demo-project/lib/calc.js +6 -0
- package/r7-test-env/demo-project/package.json +1 -0
- package/r7-test-env/demo-project/test.js +10 -0
- package/r7-test-env/prompt-R7-SMOKE.md +41 -0
- package/r7-test-env/sillyspec-r7.cmd +3 -0
- package/round4/account.mjs +18 -5
- package/round4/b1-analyze-task08.mjs +40 -0
- package/round4/b1-b1-target-validation.md +32 -0
- package/round4/b3a-prompt-audit-result.md +66 -0
- package/round4/b3a-prompt-audit.mjs +89 -0
- package/round4/optimization-plan.md +165 -0
- package/round4/r4-final-report.html +347 -0
- package/round4/r5-scenario-matrix.md +46 -0
- package/src/change-list.js +9 -0
- package/src/classify-change.js +6 -0
- package/src/command-cards.js +2 -1
- package/src/commit-guard.js +2 -0
- package/src/config-schema.js +64 -12
- package/src/db.js +35 -4
- package/src/decision-distill.js +9 -3
- package/src/docs-check.js +1 -1
- package/src/doctor-diagnostics.js +44 -2
- package/src/flow-draft.js +748 -0
- package/src/flow-parity.js +236 -0
- package/src/flow-review.js +184 -0
- package/src/flow.js +1492 -0
- package/src/foreign-declared.js +17 -1
- package/src/fr-index.js +396 -22
- package/src/fs-atomic.js +5 -1
- package/src/git-helper.js +2 -2
- package/src/greenfield-bootstrap.js +98 -0
- package/src/handoff.js +120 -7
- package/src/hunk-attribution.js +191 -0
- package/src/index.js +570 -16
- package/src/knowledge-classify.js +47 -1
- package/src/knowledge-digest.js +162 -0
- package/src/knowledge-quicklog.js +246 -0
- package/src/machine-draft.js +128 -0
- package/src/machine-interface.js +107 -5
- package/src/mcp-server.js +1 -0
- package/src/module-resolve.js +3 -1
- package/src/preview-progress.js +160 -0
- package/src/progress/change-registry.js +11 -3
- package/src/progress/shared.js +3 -2
- package/src/progress.js +56 -13
- package/src/quicklog.js +44 -6
- package/src/redomain.js +147 -0
- package/src/review-material-pack.js +204 -1
- package/src/run/archive-distill.js +4 -2
- package/src/run/bg-sync.js +67 -0
- package/src/run/command.js +123 -10
- package/src/run/complete-handlers.js +327 -72
- package/src/run/complete.js +350 -71
- package/src/run/gate-snapshot-ledger.js +213 -0
- package/src/run/gate-snapshot.js +250 -34
- package/src/run/gates.js +370 -233
- package/src/run/green-cache.js +123 -0
- package/src/run/junction-rm.js +70 -0
- package/src/run/next.js +14 -0
- package/src/run/prompt.js +185 -55
- package/src/run/quick-audit.js +185 -12
- package/src/run/shared.js +120 -3
- package/src/run/stage.js +182 -216
- package/src/run/test-ledger.js +264 -0
- package/src/run/verify-quality-scan.js +84 -7
- package/src/scope-audit.js +6 -1
- package/src/sentinel-assertions.js +114 -0
- package/src/setup.js +3 -3
- package/src/spec-sync.js +41 -1
- package/src/stage-contract.js +26 -8
- package/src/stage-review-checklist.js +2 -0
- package/src/stage-review.js +7 -2
- package/src/stage-templates.js +157 -0
- package/src/stages/archive.js +2 -0
- package/src/stages/brainstorm.js +24 -106
- package/src/stages/execute.js +221 -84
- package/src/stages/knowledge.js +74 -1
- package/src/stages/plan-postcheck.js +352 -1
- package/src/stages/plan.js +23 -1
- package/src/stages/quick.js +10 -2
- package/src/stages/verify.js +4 -2
- package/src/sync.js +81 -23
- package/src/task-done.js +181 -0
- package/src/task-review.js +8 -55
- package/src/test-bindings.js +549 -0
- package/src/timeline.js +218 -0
- package/src/ui-visual.js +175 -0
- package/src/verify-draft.js +214 -0
- package/src/verify-postcheck.js +860 -158
- package/src/verify-probes.js +59 -3
- package/src/watcher.js +1031 -0
- package/src/worktree-apply.js +44 -4
- package/src/worktree-deps.js +22 -8
- package/src/worktree-policy.js +42 -0
- package/src/worktree.js +102 -15
- package/templates/agents-instruction.md +37 -18
|
@@ -0,0 +1,748 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* flow-draft.js — 全件机器起草器(R7 切片三 task-05 / D-004 D-005 / FR-07 FR-08)。
|
|
3
|
+
*
|
|
4
|
+
* 轻量跑道治理工件全部 CLI 机器起草(工件回填轮=0),agent 只裁例外:
|
|
5
|
+
* - proposal:--input 机械转写(动机/关键问题/变更范围从任务原话摘段);
|
|
6
|
+
* - requirements:机械摘「成功标准」条目 → FR 条目;
|
|
7
|
+
* - tasks:成功标准 → checkbox 任务行;任务卡分岔(用户裁定#3):默认 thin+直写零任务卡
|
|
8
|
+
* (轻量跑=quick 的协议兄弟);--thick / --with-tasks 才生成 tasks/task-NN.md 卡;
|
|
9
|
+
* - decisions:只记真实新增(转写任务通常为零——不落文件)。
|
|
10
|
+
*
|
|
11
|
+
* 机器段经 machine-draft.wrapSection 包裹(sha256 指纹标记对,guardNote 指向 flow done 拒收);
|
|
12
|
+
* draft-ledger(.runtime/draft-ledger-<change>.json)每段存 hash+**首版原文 body(首版快照永不
|
|
13
|
+
* 覆盖——切片四 editRatio 的基准依赖;amend 只刷 hash 不动 body)**,命名空间按 文件名+段键。
|
|
14
|
+
* AGENT 槽(<!--AGENT:--> 标记对)=合法书写面(验收侧放行,不参与指纹)。
|
|
15
|
+
*
|
|
16
|
+
* 守卫全在验收侧(护栏#2:零 prompt 劝说):flow done 工件校验子步对 ledger 在案的每文件
|
|
17
|
+
* verifyMarkers 三态拒收(标记缺失/哈希失配/手工重锚未审计);flow amend-draft 是唯一留痕
|
|
18
|
+
* 修改通道(reanchorText 重锚 + ledger amendments 审计)。
|
|
19
|
+
*/
|
|
20
|
+
import { existsSync, readFileSync, mkdirSync, readdirSync } from 'node:fs'
|
|
21
|
+
import { join, dirname } from 'node:path'
|
|
22
|
+
import { writeAtomicSync } from './fs-atomic.js'
|
|
23
|
+
import { wrapSection, verifyMarkers, reanchorText, bodyHash, parseMarkerBlocks } from './machine-draft.js'
|
|
24
|
+
|
|
25
|
+
/** 读文件并归一化 CRLF→LF(2026-09-25-feedback-fixes④:Python/编辑器写盘 CRLF 会破坏
|
|
26
|
+
锚定的槽位识别 regex)。 */
|
|
27
|
+
function readText(path) { return readFileSync(path, 'utf8').replace(/\r\n/g, '\n') }
|
|
28
|
+
|
|
29
|
+
const AMEND_CMD = (change) => `sillyspec flow amend-draft --change ${change}`
|
|
30
|
+
const GUARD_NOTE = '整段改写会被 flow done 拒收'
|
|
31
|
+
|
|
32
|
+
/** sidecar 台账路径(.runtime 下,verify-draft sidecar 同族;命名空间=文件:段键)。 */
|
|
33
|
+
export function draftLedgerPath(runtimeRoot, changeName) {
|
|
34
|
+
return join(runtimeRoot, `draft-ledger-${changeName}.json`)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const AGENT_SLOT = (n, hint) => `<!--AGENT:槽${n} ${hint}——例外裁决书写面(机器段之外合法) -->`
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 复合标准拆分(2026-09-25-fr-compound-split):条目内「A/B」「A;B」的合取标准拆为独立条目
|
|
41
|
+
* (FR 区 agent 书写架构下,摘录一行 A/B 会被直抄成粒度失真的单条 FR)。路径感知:含扩展
|
|
42
|
+
* 名点或超一处斜杠的条目不按斜杠拆(src/flow.js 之类不能劈)。
|
|
43
|
+
*/
|
|
44
|
+
function splitCompoundCriteria(item) {
|
|
45
|
+
const parts = String(item).split(/[;;]/).map((s) => s.trim()).filter(Boolean)
|
|
46
|
+
const out = []
|
|
47
|
+
for (const part of parts) {
|
|
48
|
+
const pathLike = /\.[A-Za-z]{1,5}\b/.test(part) || (part.match(/\//g) || []).length > 1
|
|
49
|
+
if (!pathLike && /[//]/.test(part)) {
|
|
50
|
+
for (const seg of part.split(/[//]/).map((s) => s.trim()).filter(Boolean)) out.push(seg)
|
|
51
|
+
} else out.push(part)
|
|
52
|
+
}
|
|
53
|
+
return out
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 从任务原话摘「成功标准」条目(行级机械提取:「成功标准/验收」节下的条目行;无节则回退列表行)。
|
|
58
|
+
* 编号条目通道(2026-09-25-thin-fr-quality,R16 实证驱动):输入为完整任务书时行为需求以
|
|
59
|
+
* 「1. …n.」编号列在正文、成功标准节只有总括句(「上述 1-9 全部实现…」入库为 FR 是口号不是
|
|
60
|
+
* 行为语义)——正文编号条目数 ≥3 且多于节条目时取而代之(与节条目去重)。adopt/proposal
|
|
61
|
+
* 回提路径经 opts.numberedChannel=false 关闭(proposal 其他节的编号列表会误劫持)。
|
|
62
|
+
*/
|
|
63
|
+
/**
|
|
64
|
+
* 续行合并判定(cli-protocol-trust,R17 实证):括号/引号未闭合,或行尾悬空连接符
|
|
65
|
+
* (冒号/顿号/逗号/开括号)→ 该行与下一行本是一条标准(--input 手写换行拆散)。
|
|
66
|
+
* 节标题行(成功标准:/动机:等)不参与合并——它们必须独立成行才能被节检测正则命中。
|
|
67
|
+
*/
|
|
68
|
+
const SECTION_HEAD_RE = /^(?:#+\s*)?(成功标准|验收标准|验收|acceptance|动机|背景|关键问题|变更范围|需求|非目标)[::]?$/i
|
|
69
|
+
function needsContinuationMerge(line) {
|
|
70
|
+
if (SECTION_HEAD_RE.test(line)) return false
|
|
71
|
+
if (/[::、,,((【\[「『]$/u.test(line)) return true // 行尾悬空连接符/开括号收尾
|
|
72
|
+
const pairs = [['(', ')'], ['(', ')'], ['【', '】'], ['[', ']'], ['「', '」'], ['『', '』']]
|
|
73
|
+
for (const [o, c] of pairs) {
|
|
74
|
+
const open = (line.match(new RegExp('\\' + o, 'g')) || []).length
|
|
75
|
+
const close = (line.match(new RegExp('\\' + c, 'g')) || []).length
|
|
76
|
+
if (open > close) return true
|
|
77
|
+
}
|
|
78
|
+
return false
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** 碎片特征检测(cli-protocol-trust):括号不平衡/开括号收尾/闭括号开头——只警告不阻断
|
|
82
|
+
* (句子级强切与「完整标点收尾」自检经方案评审否决:误伤复合条目/无标点短条目)。 */
|
|
83
|
+
function detectFragmentedCriteria(criteria, context) {
|
|
84
|
+
for (const c of criteria || []) {
|
|
85
|
+
const open = (c.match(/[((【\[「『]/g) || []).length
|
|
86
|
+
const close = (c.match(/[))】\]」』]/g) || []).length
|
|
87
|
+
if (open !== close || /[((【\[「『]$/u.test(c) || /^[))】\]」』]/.test(c)) {
|
|
88
|
+
console.warn(`⚠️ 摘录碎片特征:条目「${String(c).slice(0, 50)}${String(c).length > 50 ? '…' : ''}」括号不平衡(${open} 开/${close} 闭)——疑为切分残留,请核对 ${context || 'requirements/tasks'} 机器段`)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** tasks 行截断(cli-protocol-trust):60 字硬切改句界感知(窗内取末个句读,无则硬切),
|
|
94
|
+
* 带省略号收尾。下游消费按 `- [ ] task-NN` 前缀锚(complete.js 勾选正则/--step 断言),
|
|
95
|
+
* 不依赖截断长度——放宽安全(方案评审确认)。导出供 test/draft-continuation 直测。 */
|
|
96
|
+
export function clipTaskText(s) {
|
|
97
|
+
const str = String(s || '')
|
|
98
|
+
if (str.length <= 80) return str
|
|
99
|
+
const win = str.slice(0, 80)
|
|
100
|
+
const m = win.match(/[。;;!!??,,][^。;;!!??,,]*$/)
|
|
101
|
+
const cut = m ? win.slice(0, win.length - m[0].length + 1) : win
|
|
102
|
+
return cut.replace(/[,,、;;\s]+$/, '') + '…'
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function extractSuccessCriteria(input, opts = {}) {
|
|
106
|
+
if (!input) return []
|
|
107
|
+
const NL = /\r\n|\r|\n/
|
|
108
|
+
const rawLines = String(input).split(NL).map((l) => l.trim()).filter(Boolean)
|
|
109
|
+
// 续行合并(R17 实证:括号换行的单条标准被行级切分拆成两条碎片)
|
|
110
|
+
const lines = []
|
|
111
|
+
for (const l of rawLines) {
|
|
112
|
+
const prev = lines[lines.length - 1]
|
|
113
|
+
if (prev != null && needsContinuationMerge(prev)) lines[lines.length - 1] = prev + l
|
|
114
|
+
else lines.push(l)
|
|
115
|
+
}
|
|
116
|
+
const criteria = []
|
|
117
|
+
let inSection = false
|
|
118
|
+
let sawSection = false
|
|
119
|
+
for (const raw of lines) {
|
|
120
|
+
const line = raw.replace(/^#+\s*/, '')
|
|
121
|
+
if (/^(成功标准|验收标准|验收|acceptance)\s*[::]?$/i.test(line)) { inSection = true; sawSection = true; continue }
|
|
122
|
+
if (/^(动机|背景|关键问题|变更范围|需求|非目标)/i.test(line)) { inSection = false; continue }
|
|
123
|
+
if (!sawSection && /^[-*•]\s+\S/.test(raw)) { criteria.push(raw.replace(/^[-*•]\s+/, '')); continue }
|
|
124
|
+
if (inSection) {
|
|
125
|
+
const item = raw.replace(/^[-*•\d.)、]+\s*/, '')
|
|
126
|
+
if (item && !/^#/.test(item)) criteria.push(...splitCompoundCriteria(item))
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// 编号条目通道:正文「1. …」行为条目(与节条目去重),数 ≥3 且多于节条目时取代
|
|
130
|
+
if (opts.numberedChannel !== false) {
|
|
131
|
+
const numbered = []
|
|
132
|
+
for (const raw of lines) {
|
|
133
|
+
const m = raw.match(/^\d{1,2}[.、))]\s*(\S.*)$/)
|
|
134
|
+
if (!m) continue
|
|
135
|
+
const t = m[1].trim()
|
|
136
|
+
if (t && !numbered.includes(t) && !criteria.includes(t)) numbered.push(t)
|
|
137
|
+
}
|
|
138
|
+
if (numbered.length >= 3 && numbered.length > criteria.length) return numbered
|
|
139
|
+
}
|
|
140
|
+
return criteria
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** proposal 机器稿(--input 转写:动机/关键问题/变更范围/成功标准四机器段+AGENT 槽)。 */
|
|
144
|
+
function draftProposal({ change, input, criteria }) {
|
|
145
|
+
const crit = criteria || extractSuccessCriteria(input)
|
|
146
|
+
const wrapped = (key, body) => wrapSection({ key, body, amendCmd: AMEND_CMD(change), guardNote: GUARD_NOTE })
|
|
147
|
+
const text = [
|
|
148
|
+
'---',
|
|
149
|
+
`author: flow-machine-draft`,
|
|
150
|
+
`created_at: ${new Date().toISOString()}`,
|
|
151
|
+
'---',
|
|
152
|
+
`# 提案书(Proposal)— ${change}`,
|
|
153
|
+
'',
|
|
154
|
+
'## 动机',
|
|
155
|
+
wrapped('proposal-motivation', `任务原话转写:${input || '(未提供 --input)'}`),
|
|
156
|
+
AGENT_SLOT(1, '动机例外裁决'),
|
|
157
|
+
'',
|
|
158
|
+
'## 变更范围',
|
|
159
|
+
wrapped('proposal-scope', crit.length > 0
|
|
160
|
+
? ['按成功标准机械推导,共 ' + crit.length + ' 条验收面:', ...crit.map((c, i) => `${i + 1}. ${c}`)].join('\n')
|
|
161
|
+
: '(--input 未含成功标准条目——flow done 测试门与工件校验为默认验收面)'),
|
|
162
|
+
'',
|
|
163
|
+
'## 成功标准(可验证)',
|
|
164
|
+
wrapped('proposal-criteria', crit.length > 0
|
|
165
|
+
? crit.map((c, i) => `${i + 1}. ${c}`).join('\n')
|
|
166
|
+
: '1. flow done 六子步全绿(测试门实测通过+工件指纹校验通过)'),
|
|
167
|
+
AGENT_SLOT(2, '成功标准例外裁决(增删条目在此书写)'),
|
|
168
|
+
'',
|
|
169
|
+
].join('\n')
|
|
170
|
+
return { text, sections: collectSections(text) }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** requirements 机器稿(骨架 + agent 填写 FR + 绑定槽——2026-09-25-fr-agent-writable,
|
|
174
|
+
* 平台狗粮驱动架构修正:FR 内容从机器指纹段改为 agent 书写面(同 design 槽模式)。
|
|
175
|
+
* 机器只搭骨架:节标题 + FR 空区(含参考摘录注释)+ 绑定槽。agent 干活时直接填 FR,
|
|
176
|
+
* 不走 amend、不触发 edit_ratio——机器摘录 FR 太薄(「flow done 全绿」级)是实证痛点,
|
|
177
|
+
* amend 改写被 route_hint:thick 误报打击改善积极性,且 FR 质量仍受限。 */
|
|
178
|
+
/**
|
|
179
|
+
* 成功标准 → GWT 骨架预填(governance-autopilot,R20 实证:agent 手写 FR +12 轮 Edit)——
|
|
180
|
+
* 从标准文本机械推导三段。骨架进场 agent 可覆盖(消灭空槽冷启动,agent 只需改错的不需从零写)。
|
|
181
|
+
*/
|
|
182
|
+
function draftGwtSkeleton(criterion, index) {
|
|
183
|
+
const c = String(criterion || '').trim()
|
|
184
|
+
const id = `FR-${String(index + 1).padStart(2, '0')}`
|
|
185
|
+
if (!c) return `### ${id}: (待描述)\nGiven 系统就绪\nWhen 执行目标行为\nThen 达成预期`
|
|
186
|
+
const kw = c.match(/(api|端点|接口|存储|迁移|鉴权|幂等|上限|正序|增量|append-only|前端|组件|折叠|高亮|徽标|轮询|测试|E2E|curl)/gi)
|
|
187
|
+
const given = kw && kw.length > 0 ? `Given ${[...new Set(kw.map(k => k.toLowerCase()))].slice(0, 3).join(' / ')} 相关模块就绪` : 'Given 系统就绪'
|
|
188
|
+
const arrowSplit = c.split(/[→➜]|则|使得/)
|
|
189
|
+
const when = (arrowSplit[0] || c).trim().slice(0, 80)
|
|
190
|
+
const then = (arrowSplit[1] || '行为符合本条标准描述').trim().slice(0, 80)
|
|
191
|
+
return `### ${id}: ${c.slice(0, 50)}\n${given}\nWhen ${when}\nThen ${then}`
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function draftRequirements({ change, criteria, input }) {
|
|
195
|
+
const crit = criteria || []
|
|
196
|
+
// GWT 骨架预填(governance-autopilot):FR 区直接生成完整 Given/When/Then 块——agent 可覆盖
|
|
197
|
+
const frBlocks = crit.length > 0
|
|
198
|
+
? crit.map((c, i) => draftGwtSkeleton(c, i)).join('\n\n')
|
|
199
|
+
: '### FR-01: (按任务语义填写)\nGiven 系统就绪\nWhen 执行目标行为\nThen 达成预期'
|
|
200
|
+
const n = crit.length > 0 ? crit.length : 1
|
|
201
|
+
const bindingSlots = Array.from({ length: n }, (_, i) => {
|
|
202
|
+
const id = `FR-${String(i + 1).padStart(2, '0')}`
|
|
203
|
+
return `<!--AGENT:测试绑定${id} 哪个测试文件/用例覆盖这条 FR(项目相对全路径+用例名;空槽将在 flow done 自动从测试结果补全——预填可加速)——例外裁决书写面(机器段之外合法) -->`
|
|
204
|
+
}).join('\n\n')
|
|
205
|
+
const text = [
|
|
206
|
+
'---',
|
|
207
|
+
`author: flow-machine-draft`,
|
|
208
|
+
`created_at: ${new Date().toISOString()}`,
|
|
209
|
+
'---',
|
|
210
|
+
`# 需求规格(Requirements)— ${change}`,
|
|
211
|
+
'',
|
|
212
|
+
'## 功能需求(GWT 骨架已机器预填——可编辑覆盖;FR 进知识索引)',
|
|
213
|
+
'',
|
|
214
|
+
`<!--AGENT:FR区 agent 填写功能需求(GWT 骨架已预填——覆盖/修改/保留均可) -->`,
|
|
215
|
+
frBlocks,
|
|
216
|
+
'',
|
|
217
|
+
'## 测试绑定(每条 FR 至少一行——空槽将在 flow done 时自动从测试结果补全)',
|
|
218
|
+
'',
|
|
219
|
+
bindingSlots,
|
|
220
|
+
'',
|
|
221
|
+
].join('\n')
|
|
222
|
+
return { text, sections: collectSections(text) }
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* requirements「测试绑定」槽位门(flow done artifacts 子步消费面):每条 FR 的绑定槽非空
|
|
227
|
+
* (「不适用:理由」=已答);FR 机器段在场但零绑定槽 = 骨架早于本机制的旧版——修复路径是
|
|
228
|
+
* 删 requirements.md 重入 flow start(redraft 按 proposal 机器段回提 criteria 重新起草)。
|
|
229
|
+
* 纯读盘面;无 requirements.md → not-applicable(redraft 补生成)。
|
|
230
|
+
*/
|
|
231
|
+
export function verifyRequirementBindings({ changeDir }) {
|
|
232
|
+
const path = join(changeDir, 'requirements.md')
|
|
233
|
+
if (!existsSync(path)) return { applicable: false, emptySlots: [] }
|
|
234
|
+
const text = readFileSync(path, 'utf8').replace(/\r\n/g, '\n')
|
|
235
|
+
const lines = text.split('\n')
|
|
236
|
+
const emptySlots = []
|
|
237
|
+
let current = null
|
|
238
|
+
let hasContent = false
|
|
239
|
+
let slotCount = 0
|
|
240
|
+
let frAreaFilled = false
|
|
241
|
+
let inFrArea = false
|
|
242
|
+
let inHtmlComment = false
|
|
243
|
+
const flush = () => { if (current && !hasContent) emptySlots.push(current) }
|
|
244
|
+
for (const line of lines) {
|
|
245
|
+
if (/^<!--\s*AGENT:FR区/.test(line)) { inFrArea = true; continue }
|
|
246
|
+
// HTML 注释跳过(参考摘录里的 FR 行不算 agent 填写):<!-- 开非 AGENT 行入注释态,--> 出
|
|
247
|
+
if (/^<!--(?!.*AGENT)/.test(line.trim())) { inHtmlComment = !/-->/.test(line); continue }
|
|
248
|
+
if (inHtmlComment) { if (/-->/.test(line)) inHtmlComment = false; continue }
|
|
249
|
+
if (inFrArea && (/^<!--\s*AGENT:测试绑定/.test(line) || /^##\s/.test(line))) {
|
|
250
|
+
inFrArea = false
|
|
251
|
+
// FR 区内容检查:至少一行非注释非空的实质内容(### FR- 开头的行为需求)
|
|
252
|
+
if (!frAreaFilled) emptySlots.push('FR区(agent 未填写功能需求——每条 FR 格式 ### FR-NN: 标题 + Given/When/Then)')
|
|
253
|
+
}
|
|
254
|
+
if (inFrArea && /^###\s+FR-/.test(line)) frAreaFilled = true
|
|
255
|
+
if (/^<!--\s*AGENT:测试绑定/.test(line)) {
|
|
256
|
+
flush()
|
|
257
|
+
current = (line.match(/AGENT:(测试绑定\S+)/) || [])[1] || '测试绑定'
|
|
258
|
+
hasContent = false
|
|
259
|
+
slotCount++
|
|
260
|
+
continue
|
|
261
|
+
}
|
|
262
|
+
if (/^<!--/.test(line) || /^#{1,6}\s/.test(line)) { flush(); current = null; continue }
|
|
263
|
+
if (current && line.trim()) hasContent = true
|
|
264
|
+
}
|
|
265
|
+
flush()
|
|
266
|
+
// FR 区在文件尾的情况(无后续节)
|
|
267
|
+
if (inFrArea && !frAreaFilled) emptySlots.push('FR区(agent 未填写功能需求)')
|
|
268
|
+
if (slotCount === 0 && !/AGENT:FR区/.test(text) && !/MACHINE-DRAFT:requirements-frs/.test(text)) {
|
|
269
|
+
// 无 FR 区标记也无绑定槽 = 骨架过旧或异常
|
|
270
|
+
return { applicable: true, emptySlots: ['(requirements 结构异常——无 FR 区标记也无绑定槽;修复:删除 requirements.md 后重入 flow start 补生成)'] }
|
|
271
|
+
}
|
|
272
|
+
return { applicable: true, emptySlots }
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* 从「测试绑定」槽提取绑定行(flow done distill 子步消费面 → writeChangeTrace →
|
|
277
|
+
* indexRequirements 归档提升铸全局)。锚=FR 局部编号(FR-01…,与机器 FR 序一致);
|
|
278
|
+
* 「不适用」/无路径 token 的作答不产行;tests=作答文本里的测试文件路径(去重)。
|
|
279
|
+
*
|
|
280
|
+
* 2026-09-26-binding-anchor-fidelity:tests 条目从纯路径扩为「路径+可选用例锚」——
|
|
281
|
+
* 用例锚只在路径 token 后的紧邻窗口(到下一路径 token 或文本尾)捕获,四形态任一:
|
|
282
|
+
* 「X」+可选 组/用例 后缀、#id、::id、> name(书写原形只摘不译,「:」后是描述不捕);
|
|
283
|
+
* 裸文件名/残缺路径段解析为项目相对全路径(直取存在优先,仓内唯一后缀命中,歧义原样保留)。
|
|
284
|
+
*/
|
|
285
|
+
const TEST_PATH_TOKEN_RE = /[A-Za-z0-9_/.-]+\.(?:mjs|cjs|js|ts|tsx|py)/g
|
|
286
|
+
// 用例锚四形态(「X」+可选 组/用例 后缀、#id、::id、> name)——字符类排除竖线:锚条目
|
|
287
|
+
// 在 FR 机器子块按 `tests: a | b` 序列化、split('|') 回读(评审 P2),带竖线的锚在竖线处
|
|
288
|
+
// 截断(诚实降级),不产出裂格式幻影条目。
|
|
289
|
+
const CASE_ANCHOR_RE = /^\s*(?:(#[^\s:;;,。\n|]+)|(::[^\s:;;,。\n|]+)|(>\s*[^:;;,。\n|]+)|(「[^」\n|]+」(?:组|用例)?))/
|
|
290
|
+
const isTestPathToken = (p) => /(^|\/)(test|tests)\//.test(p) || /\.(test|spec)\./.test(p) || /_test\b/.test(p) || /_spec\b/.test(p)
|
|
291
|
+
|
|
292
|
+
/** 仓内文件索引(懒建:首个直取未命中的 token 才扫描;单次提取调用内缓存)。
|
|
293
|
+
* 排除依赖/生成物/治理面目录(点目录全跳);条目上限熔断防病态大仓。 */
|
|
294
|
+
function buildRepoFileIndex(root) {
|
|
295
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', 'coverage', 'out', 'target', 'vendor', 'venv', '.venv', '__pycache__'])
|
|
296
|
+
const files = []
|
|
297
|
+
const walk = (dir) => {
|
|
298
|
+
if (files.length >= 100000) return
|
|
299
|
+
let ents
|
|
300
|
+
try { ents = readdirSync(dir, { withFileTypes: true }) } catch { return }
|
|
301
|
+
ents.sort((a, b) => (a.name < b.name ? -1 : 1))
|
|
302
|
+
for (const e of ents) {
|
|
303
|
+
if (e.isDirectory()) { if (!SKIP_DIRS.has(e.name) && !e.name.startsWith('.')) walk(join(dir, e.name)) }
|
|
304
|
+
else files.push(join(dir, e.name))
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
walk(root)
|
|
308
|
+
return files
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** 裸文件名/路径段 → 项目相对全路径:直取命中原样返回;否则仓内唯一命中(rel===p /
|
|
312
|
+
* rel 以 /p 结尾 / 基名相等)解析;零/多命中原样保留(诚实——dangling 校验自然暴露)。 */
|
|
313
|
+
function resolveTestPathRel(p, root, indexRef) {
|
|
314
|
+
if (!root || existsSync(join(root, p))) return p
|
|
315
|
+
if (!indexRef.files) indexRef.files = buildRepoFileIndex(root)
|
|
316
|
+
const want = p.replace(/\\/g, '/')
|
|
317
|
+
const hits = []
|
|
318
|
+
for (const abs of indexRef.files) {
|
|
319
|
+
const rel = abs.slice(root.length + 1).replace(/\\/g, '/')
|
|
320
|
+
if (rel === want || rel.endsWith('/' + want) || rel.split('/').pop() === want) hits.push(rel)
|
|
321
|
+
if (hits.length > 1) break
|
|
322
|
+
}
|
|
323
|
+
return hits.length === 1 ? hits[0] : p
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function extractTestAnchors(content, root, indexRef) {
|
|
327
|
+
const matches = [...content.matchAll(TEST_PATH_TOKEN_RE)]
|
|
328
|
+
const out = []
|
|
329
|
+
for (let i = 0; i < matches.length; i++) {
|
|
330
|
+
const m = matches[i]
|
|
331
|
+
const raw = m[0].replace(/^[./\\]+/, '').replace(/\\/g, '/')
|
|
332
|
+
if (!isTestPathToken(raw)) continue
|
|
333
|
+
const winEnd = i + 1 < matches.length ? matches[i + 1].index : content.length
|
|
334
|
+
const cm = content.slice(m.index + m[0].length, winEnd).match(CASE_ANCHOR_RE)
|
|
335
|
+
const file = resolveTestPathRel(raw, root, indexRef)
|
|
336
|
+
const anchor = cm ? cm[0].trim().replace(/^>\s*/, ' > ') : ''
|
|
337
|
+
out.push(file + anchor)
|
|
338
|
+
}
|
|
339
|
+
return [...new Set(out)]
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function extractRequirementBindings({ changeDir, change }) {
|
|
343
|
+
const path = join(changeDir, 'requirements.md')
|
|
344
|
+
if (!existsSync(path)) return []
|
|
345
|
+
const lines = readFileSync(path, 'utf8').replace(/\r\n/g, '\n').split('\n')
|
|
346
|
+
// 仓根推导:changeDir 形如 <root>/.sillyspec/changes/<名>(主仓/worktree 同构,三层上溯);
|
|
347
|
+
// 推导失效(目录形态异构)回退 cwd
|
|
348
|
+
let root = null
|
|
349
|
+
try {
|
|
350
|
+
const r = dirname(dirname(dirname(changeDir)))
|
|
351
|
+
root = existsSync(join(r, '.sillyspec')) ? r : process.cwd()
|
|
352
|
+
} catch { root = process.cwd() }
|
|
353
|
+
const indexRef = { files: null }
|
|
354
|
+
const rows = []
|
|
355
|
+
let current = null
|
|
356
|
+
let buf = []
|
|
357
|
+
const flush = () => {
|
|
358
|
+
if (!current) return
|
|
359
|
+
const content = buf.join('\n').trim()
|
|
360
|
+
buf = []
|
|
361
|
+
if (!content || /^不适用/.test(content)) return
|
|
362
|
+
const tests = extractTestAnchors(content, root, indexRef)
|
|
363
|
+
if (tests.length === 0) return
|
|
364
|
+
rows.push({ anchor: current.replace(/^测试绑定/, ''), row_id: `${change}:flow:${current.replace(/^测试绑定/, '')}`, tests, reason: 'spec', state: 'candidate', discovery: 'machine', confirmed_by: null, source_change: change })
|
|
365
|
+
}
|
|
366
|
+
for (const line of lines) {
|
|
367
|
+
if (/^<!--\s*AGENT:测试绑定\S/.test(line)) {
|
|
368
|
+
flush()
|
|
369
|
+
current = (line.match(/AGENT:(测试绑定\S+)/) || [])[1] || null
|
|
370
|
+
buf = []
|
|
371
|
+
continue
|
|
372
|
+
}
|
|
373
|
+
if (/^<!--/.test(line) || /^#{1,6}\s/.test(line)) { flush(); current = null; continue }
|
|
374
|
+
if (current) buf.push(line)
|
|
375
|
+
}
|
|
376
|
+
flush()
|
|
377
|
+
return rows
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* design.md 机器骨架(2026-09-24 v3 设计记录全档化第一片):机器段=固定问题模板(指纹保护,
|
|
382
|
+
* 问题不可被删改),AGENT 槽=作答面。盲维四问钉死在「边界与并发」节——对撞实验 5 个 P1 全落在
|
|
383
|
+
* 乱序/并发/切换/作用域四维且全是承诺未落盘形态,问题模板化让危险问题每跑必被问;小改动槽里
|
|
384
|
+
* 一行「不适用:<理由>」即合规(档位伸缩后续片)。
|
|
385
|
+
*/
|
|
386
|
+
export function draftDesignRecord({ change }) {
|
|
387
|
+
const wrapped = (key, body) => wrapSection({ key, body, amendCmd: AMEND_CMD(change), guardNote: GUARD_NOTE })
|
|
388
|
+
const text = [
|
|
389
|
+
'---',
|
|
390
|
+
`author: flow-machine-draft`,
|
|
391
|
+
`created_at: ${new Date().toISOString()}`,
|
|
392
|
+
'---',
|
|
393
|
+
`# 设计记录(Design Record)— ${change}`,
|
|
394
|
+
'',
|
|
395
|
+
'> 四节的「问题」是机器段(指纹保护,勿改);你的回答写在每节问题下方的 AGENT 槽里。',
|
|
396
|
+
'> 每节至少一行——小改动可写「不适用:<理由>」;flow done 空槽拒收。',
|
|
397
|
+
'',
|
|
398
|
+
'## 做法概述',
|
|
399
|
+
wrapped('design-approach', '本变更怎么解决问题?改哪里、为什么选这个方案(一两段)。'),
|
|
400
|
+
AGENT_SLOT(1, '做法概述作答'),
|
|
401
|
+
'',
|
|
402
|
+
'## 接口契约',
|
|
403
|
+
wrapped('design-contract', '动了哪些函数/端点/命令/文件格式?对外可见的签名或行为变化是什么(含「无」的说明)?'),
|
|
404
|
+
AGENT_SLOT(2, '接口契约作答'),
|
|
405
|
+
'',
|
|
406
|
+
'## 边界与并发(盲维四问——每问必答,答不了即设计缺口)',
|
|
407
|
+
wrapped('design-boundaries', [
|
|
408
|
+
'1. 乱序/迟到到达:输入或事件乱序时,本设计的假设还成立吗?',
|
|
409
|
+
'2. 并发写:两个执行体同时操作同一数据/文件会发生什么?',
|
|
410
|
+
'3. 切换/生命周期:会话、请求或变更中途切换/中断时状态是否安全?',
|
|
411
|
+
'4. 作用域:跨工作区/跨仓/多实例时数据会不会串台?',
|
|
412
|
+
].join('\n')),
|
|
413
|
+
AGENT_SLOT(3, '盲维四问作答'),
|
|
414
|
+
'',
|
|
415
|
+
'## 风险与死路',
|
|
416
|
+
wrapped('design-risks', '本方案最大的风险是什么?试过但放弃的方案及放弃理由?'),
|
|
417
|
+
AGENT_SLOT(4, '风险与死路作答'),
|
|
418
|
+
'',
|
|
419
|
+
].join('\n')
|
|
420
|
+
return { text, sections: collectSections(text) }
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** tasks 机器稿(成功标准 → 逐条 checkbox 预填草稿)。计划面归 agent(2026-09-26-thin-agent-tasks,
|
|
424
|
+
* 用户否决 thin-workunits 的域关键词聚类——开放世界任务形态不可穷举,枚举分类表是错误方向):
|
|
425
|
+
* 预填只是零冷启动兜底,简报明示可按实际实现路径覆写(保持 `- [ ] task-NN:` 行形态)——
|
|
426
|
+
* 验收面锚定在 requirements FR(指纹保护),任务面不做机器分类。任务卡分岔:withTasks 才生成 tasks/task-NN.md。 */
|
|
427
|
+
function draftTasks({ change, criteria, withTasks }) {
|
|
428
|
+
const crit = criteria || []
|
|
429
|
+
const wrapped = (key, body) => wrapSection({ key, body, amendCmd: AMEND_CMD(change), guardNote: GUARD_NOTE })
|
|
430
|
+
const rows = crit.length > 0
|
|
431
|
+
? crit.map((c, i) => `- [ ] task-${String(i + 1).padStart(2, '0')}: ${clipTaskText(c)}`)
|
|
432
|
+
: ['- [ ] task-01: 完成实现并使 flow done 六子步全绿']
|
|
433
|
+
const text = [
|
|
434
|
+
'---',
|
|
435
|
+
`author: flow-machine-draft`,
|
|
436
|
+
`created_at: ${new Date().toISOString()}`,
|
|
437
|
+
'---',
|
|
438
|
+
`# 任务注册表(Tasks)— ${change}`,
|
|
439
|
+
'',
|
|
440
|
+
'> 机器预填草稿(成功标准逐条镜像)——任务面归 agent:按实际实现路径覆写本文件(保持 checkbox 行形态),验收锚在 requirements;',
|
|
441
|
+
`> ${withTasks ? '任务卡模式(--with-tasks/--thick):tasks/task-NN.md 卡已生成,中间自愿 task done,收尾仍 flow done' : '默认 thin:无任务卡文件,收口=flow done 唯一裁决'}。`,
|
|
442
|
+
'> ✅ 边干边勾(2026-09-26-tick-loop-nudge,OS Guardrails 同款纪律):完成一条 = 实现到位 + 相关测试跑绿 → 立即勾 `[x]`,勿攒到收口一把勾(勾选是进度锚与哨兵证据面)。本文件收口前随交付显式 pathspec 提交。',
|
|
443
|
+
'',
|
|
444
|
+
// tasks-rows 去指纹化(2026-09-25-feedback-fixes,平台狗粮反馈①):勾选行在指纹段内导致
|
|
445
|
+
// 勾一条就失配 → 必走 amend → editRatio=1 被判该走厚档——勾选纪律与指纹门自相矛盾。
|
|
446
|
+
// 改为裸 markdown,agent 直接勾;哨兵的防假勾逻辑独立于指纹(按提交 token 判据)。
|
|
447
|
+
rows.join('\n'),
|
|
448
|
+
'',
|
|
449
|
+
].join('\n')
|
|
450
|
+
return { text, sections: collectSections(text) }
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** 任务卡(--with-tasks/--thick 分岔产物,最小合规 frontmatter)。 */
|
|
454
|
+
function draftTaskCards({ change, criteria }) {
|
|
455
|
+
const crit = (criteria && criteria.length > 0) ? criteria : ['完成实现并使 flow done 六子步全绿']
|
|
456
|
+
return crit.map((c, i) => {
|
|
457
|
+
const id = `task-${String(i + 1).padStart(2, '0')}`
|
|
458
|
+
return {
|
|
459
|
+
id,
|
|
460
|
+
text: [
|
|
461
|
+
'---',
|
|
462
|
+
`id: ${id}`,
|
|
463
|
+
`title: 'machine-drafted task ${i + 1}'`,
|
|
464
|
+
`title_zh: '${c.slice(0, 30).replace(/'/g, '')}'`,
|
|
465
|
+
`author: flow-machine-draft`,
|
|
466
|
+
`created_at: ${new Date().toISOString()}`,
|
|
467
|
+
'priority: P1',
|
|
468
|
+
'depends_on: []',
|
|
469
|
+
'blocks: []',
|
|
470
|
+
'requirement_ids: []',
|
|
471
|
+
'decision_ids: []',
|
|
472
|
+
`goal: >`,
|
|
473
|
+
` ${c}`,
|
|
474
|
+
'---',
|
|
475
|
+
'',
|
|
476
|
+
].join('\n'),
|
|
477
|
+
}
|
|
478
|
+
})
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** decisions:只记真实新增(无真实决策=不落文件,转写任务通常为零)。 */
|
|
482
|
+
export function draftDecisions({ change, decisions }) {
|
|
483
|
+
if (!decisions || decisions.length === 0) return null
|
|
484
|
+
const wrapped = (key, body) => wrapSection({ key, body, amendCmd: AMEND_CMD(change), guardNote: GUARD_NOTE })
|
|
485
|
+
const text = [
|
|
486
|
+
'---',
|
|
487
|
+
`author: flow-machine-draft`,
|
|
488
|
+
`created_at: ${new Date().toISOString()}`,
|
|
489
|
+
'---',
|
|
490
|
+
`# 决策记录(Decisions)— ${change}`,
|
|
491
|
+
'',
|
|
492
|
+
wrapped('decisions-entries', decisions.map((d, i) => `## D-${String(i + 1).padStart(3, '0')}@v1: ${d.title}\n- 决策:${d.body}`).join('\n\n')),
|
|
493
|
+
'',
|
|
494
|
+
].join('\n')
|
|
495
|
+
return { text, sections: collectSections(text) }
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/** 从稿文本收集段哈希+首版原文(ledger 记账面)。 */
|
|
499
|
+
function collectSections(text) {
|
|
500
|
+
const blocks = []
|
|
501
|
+
const re = /MACHINE-DRAFT:([\w.-]+):([0-9a-f]{64}):begin/g
|
|
502
|
+
const lines = text.split('\n')
|
|
503
|
+
let m
|
|
504
|
+
// 按 wrapSection 结构回溯:begin 标记行下一段直到 end 标记
|
|
505
|
+
let current = null
|
|
506
|
+
for (const line of lines) {
|
|
507
|
+
const b = line.match(/<!--\s*MACHINE-DRAFT:([\w.-]+):([0-9a-f]{64}):begin/)
|
|
508
|
+
if (b) { current = { key: b[1], hash: b[2], body: [] }; blocks.push(current); continue }
|
|
509
|
+
if (/<!--\s*MACHINE-DRAFT:[\w.-]+:end/.test(line)) { current = null; continue }
|
|
510
|
+
if (current) current.body.push(line)
|
|
511
|
+
}
|
|
512
|
+
const sections = {}
|
|
513
|
+
for (const b of blocks) {
|
|
514
|
+
sections[b.key] = { hash: b.hash, body: b.body.join('\n') }
|
|
515
|
+
}
|
|
516
|
+
return sections
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* 起草全件落盘 + draft-ledger 记账(flow start 消费)。
|
|
521
|
+
* @returns {{written: string[], ledgerPath: string}}
|
|
522
|
+
*/
|
|
523
|
+
export function draftAll({ changeDir, change, input, withTasks = false, runtimeRoot }) {
|
|
524
|
+
const criteria = extractSuccessCriteria(input)
|
|
525
|
+
detectFragmentedCriteria(criteria, `flow start --input 摘录(change=${change})`)
|
|
526
|
+
const outputs = [
|
|
527
|
+
{ file: 'proposal.md', draft: draftProposal({ change, input, criteria }) },
|
|
528
|
+
{ file: 'requirements.md', draft: draftRequirements({ change, criteria }) },
|
|
529
|
+
{ file: 'design.md', draft: draftDesignRecord({ change }) },
|
|
530
|
+
{ file: 'tasks.md', draft: draftTasks({ change, criteria, withTasks }) },
|
|
531
|
+
]
|
|
532
|
+
const written = []
|
|
533
|
+
const ledger = { schemaVersion: 1, change, generatedAt: new Date().toISOString(), files: {}, amendments: [] }
|
|
534
|
+
for (const { file, draft } of outputs) {
|
|
535
|
+
writeAtomicSync(join(changeDir, file), draft.text)
|
|
536
|
+
ledger.files[file] = draft.sections
|
|
537
|
+
written.push(file)
|
|
538
|
+
}
|
|
539
|
+
if (withTasks) {
|
|
540
|
+
const cards = draftTaskCards({ change, criteria })
|
|
541
|
+
mkdirSync(join(changeDir, 'tasks'), { recursive: true })
|
|
542
|
+
for (const c of cards) writeAtomicSync(join(changeDir, 'tasks', `${c.id}.md`), c.text)
|
|
543
|
+
written.push(`tasks/(${cards.length} 卡)`)
|
|
544
|
+
}
|
|
545
|
+
const ledgerPath = draftLedgerPath(runtimeRoot, change)
|
|
546
|
+
writeAtomicSync(ledgerPath, JSON.stringify(ledger, null, 2) + '\n')
|
|
547
|
+
return { written, ledgerPath, criteria }
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* computeEditRatio——机器稿改写比例(R7 切片四 / D-005 / FR-09):决策覆盖度机械代理。
|
|
552
|
+
* LCS 行 diff:改写行数 / 首版总行数(首版为 ledger body 永存基准;AGENT 槽不属机器段
|
|
553
|
+
* 天然不计入)。纯函数。
|
|
554
|
+
*/
|
|
555
|
+
export function computeEditRatio(originalBody, currentBody) {
|
|
556
|
+
const a = String(originalBody || '').split('\n')
|
|
557
|
+
const b = String(currentBody || '').split('\n')
|
|
558
|
+
if (a.length === 0 || (a.length === 1 && !a[0])) return 0
|
|
559
|
+
// LCS 长度 DP(机器段行数小,O(n·m) 足够)
|
|
560
|
+
const dp = Array.from({ length: a.length + 1 }, () => new Uint32Array(b.length + 1))
|
|
561
|
+
for (let i = a.length - 1; i >= 0; i--) {
|
|
562
|
+
for (let j = b.length - 1; j >= 0; j--) {
|
|
563
|
+
dp[i][j] = a[i] === b[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1])
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
const lcs = dp[0][0]
|
|
567
|
+
// 改写比例口径=原文被改行占比(a.length - lcs)/a.length——del+ins 双计会把「替换」算两倍
|
|
568
|
+
// (2 行体改 1 行得 1.0 失真);纯增行不改写原文不计入。
|
|
569
|
+
return Math.min(1, Math.max(0, (a.length - lcs) / a.length))
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* flow amend-draft:机器稿留痕重锚(唯一合法修改通道)。
|
|
574
|
+
* 对 ledger 在案的每文件 reanchorText 重锚 + ledger 追加 amendment 审计(首版 body 不动)。
|
|
575
|
+
* 切片四:amend 时对首版原文计算 editRatio(改写比例=决策覆盖度机械代理,AGENT 槽不计入)。
|
|
576
|
+
* @returns {{reanchored: string[], files: string[], editRatio: number, sectionRatios: Object}}
|
|
577
|
+
*/
|
|
578
|
+
export function amendFlowDraft({ changeDir, change, runtimeRoot }) {
|
|
579
|
+
const ledgerPath = draftLedgerPath(runtimeRoot, change)
|
|
580
|
+
if (!existsSync(ledgerPath)) return { reanchored: [], files: [], editRatio: 0, sectionRatios: {} }
|
|
581
|
+
let ledger
|
|
582
|
+
try { ledger = JSON.parse(readFileSync(ledgerPath, 'utf8')) } catch { return { reanchored: [], files: [], editRatio: 0, sectionRatios: {} } }
|
|
583
|
+
const reanchored = []
|
|
584
|
+
const sectionRatios = {}
|
|
585
|
+
let totalOrig = 0
|
|
586
|
+
let totalChanged = 0
|
|
587
|
+
for (const file of Object.keys(ledger.files || {})) {
|
|
588
|
+
const mdPath = join(changeDir, file)
|
|
589
|
+
let text
|
|
590
|
+
try { text = readText(mdPath) } catch { continue }
|
|
591
|
+
const r = reanchorText({ text, amendCmd: AMEND_CMD(change), guardNote: GUARD_NOTE })
|
|
592
|
+
if (r.keys.length === 0) continue
|
|
593
|
+
writeAtomicSync(mdPath, r.text)
|
|
594
|
+
for (const k of r.keys) {
|
|
595
|
+
const firstBody = ledger.files[file][k] && ledger.files[file][k].body
|
|
596
|
+
const ratio = firstBody != null ? computeEditRatio(firstBody, r.contentByKey[k]) : 0
|
|
597
|
+
sectionRatios[`${file}:${k}`] = Math.round(ratio * 1000) / 1000
|
|
598
|
+
// FR 段不进决策密度(2026-09-25-fr-quality-fix,平台狗粮驱动):agent 丰富 FR 文本=需求
|
|
599
|
+
// 澄清非设计决策,计入 edit_ratio 会触发 route_hint:thick 误报,打击 FR 改善积极性。
|
|
600
|
+
// sectionRatios 照记(审计可见),只是不计入 totalOrig/totalChanged 的汇总。
|
|
601
|
+
const isFrSection = k === 'requirements-frs'
|
|
602
|
+
if (!isFrSection) {
|
|
603
|
+
const aLines = String(firstBody || '').split('\n').length
|
|
604
|
+
totalOrig += aLines
|
|
605
|
+
totalChanged += Math.round(ratio * aLines)
|
|
606
|
+
}
|
|
607
|
+
ledger.files[file][k] = { ...ledger.files[file][k], hash: bodyHash(r.contentByKey[k]) }
|
|
608
|
+
}
|
|
609
|
+
reanchored.push(`${file}(${r.keys.join('/')})`)
|
|
610
|
+
}
|
|
611
|
+
if (reanchored.length > 0) {
|
|
612
|
+
ledger.amendments = [...(ledger.amendments || []), { at: new Date().toISOString(), files: reanchored, sectionRatios }]
|
|
613
|
+
writeAtomicSync(ledgerPath, JSON.stringify(ledger, null, 2) + '\n')
|
|
614
|
+
}
|
|
615
|
+
const editRatio = totalOrig > 0 ? Math.round((totalChanged / totalOrig) * 1000) / 1000 : 0
|
|
616
|
+
return { reanchored, files: Object.keys(ledger.files || {}), editRatio, sectionRatios }
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/** 三态拒收校验(flow done 工件子步消费面;无 ledger=not-applicable)。 */
|
|
620
|
+
export function verifyFlowDrafts({ changeDir, change, runtimeRoot }) {
|
|
621
|
+
const ledgerPath = draftLedgerPath(runtimeRoot, change)
|
|
622
|
+
if (!existsSync(ledgerPath)) return { applicable: false, violations: [] }
|
|
623
|
+
let ledger
|
|
624
|
+
try { ledger = JSON.parse(readFileSync(ledgerPath, 'utf8')) } catch { return { applicable: false, violations: [] } }
|
|
625
|
+
const violations = []
|
|
626
|
+
for (const [file, sections] of Object.entries(ledger.files || {})) {
|
|
627
|
+
const mdPath = join(changeDir, file)
|
|
628
|
+
let text
|
|
629
|
+
try { text = readText(mdPath) } catch {
|
|
630
|
+
violations.push(`机器稿「${file}」不可读(draft ledger 在案)`)
|
|
631
|
+
continue
|
|
632
|
+
}
|
|
633
|
+
violations.push(...verifyMarkers({ text, sections, amendCmd: AMEND_CMD(change) }))
|
|
634
|
+
}
|
|
635
|
+
return { applicable: true, violations }
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* design.md AGENT 槽空槽判定(flow done 工件子步消费面)。槽内容 = 槽标记行之后到下一个
|
|
640
|
+
* 标记/标题之前的非空行;「不适用:<理由>」是非空行,天然视作已答(档位伸缩的 S0 出口)。
|
|
641
|
+
* 纯读盘面;无 design.md → not-applicable(存量变更/混跑回退面)。
|
|
642
|
+
*/
|
|
643
|
+
export function verifyDesignRecordFilled({ changeDir }) {
|
|
644
|
+
const path = join(changeDir, 'design.md')
|
|
645
|
+
if (!existsSync(path)) return { applicable: false, emptySlots: [] }
|
|
646
|
+
const lines = readFileSync(path, 'utf8').replace(/\r\n/g, '\n').split('\n')
|
|
647
|
+
const emptySlots = []
|
|
648
|
+
let current = null
|
|
649
|
+
let hasContent = false
|
|
650
|
+
let slotCount = 0
|
|
651
|
+
const flush = () => {
|
|
652
|
+
if (current && !hasContent) emptySlots.push(current)
|
|
653
|
+
}
|
|
654
|
+
for (const line of lines) {
|
|
655
|
+
if (/^<!--\s*AGENT:/.test(line)) {
|
|
656
|
+
flush()
|
|
657
|
+
current = (line.match(/AGENT:(\S+)/) || [])[1] || '槽'
|
|
658
|
+
hasContent = false
|
|
659
|
+
slotCount++
|
|
660
|
+
continue
|
|
661
|
+
}
|
|
662
|
+
if (/^<!--\s*MACHINE-DRAFT:/.test(line) || /^#{1,6}\s/.test(line)) {
|
|
663
|
+
flush()
|
|
664
|
+
current = null
|
|
665
|
+
continue
|
|
666
|
+
}
|
|
667
|
+
if (current && line.trim()) hasContent = true
|
|
668
|
+
}
|
|
669
|
+
flush()
|
|
670
|
+
// 防绕过:design.md 在场但零 AGENT 槽(骨架被整删/手写替代)——机器段指纹只护 ledger 在案
|
|
671
|
+
// 变更,非在案面(如工具升级前的在途变更)唯一的守卫就是这里
|
|
672
|
+
if (slotCount === 0) return { applicable: true, emptySlots: ['(骨架缺失——design.md 无任何 AGENT 作答槽,被整删或手写替代;恢复机器骨架后作答)'] }
|
|
673
|
+
return { applicable: true, emptySlots }
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* 幂等补起草(2026-09-25-thin-dogfood-fixes 修复①):draft 谱系是 flow start 时点快照——
|
|
678
|
+
* 工具升级新增工件(如 design.md)后,在途变更重入 start 拿不到新稿。缺哪补哪、已存在不碰
|
|
679
|
+
* (agent 已填槽/已 amend 段零影响)、ledger 合并(新文件段落入账,既有段与 amendments 原样)。
|
|
680
|
+
* criteria 来源:input 显式 > 既有 proposal 的 proposal-criteria 机器段回提(input 缺省不退化
|
|
681
|
+
* 为兜底单行)> 空(draft 各自兜底)。
|
|
682
|
+
*/
|
|
683
|
+
export function redraftMissingArtifacts({ changeDir, change, input, runtimeRoot }) {
|
|
684
|
+
// criteria 回提:input 显式 > 既有 proposal 机器段(轻量变更自产,指纹保证原文可信)> proposal
|
|
685
|
+
// 手写「成功标准」节(brainstorm 预段产物,adopt 路径)> 空(draft 各自兜底)
|
|
686
|
+
let criteria = input ? extractSuccessCriteria(input) : null
|
|
687
|
+
if (criteria === null || (Array.isArray(criteria) && criteria.length === 0)) {
|
|
688
|
+
try {
|
|
689
|
+
const pText = readText(join(changeDir, 'proposal.md'))
|
|
690
|
+
const block = parseMarkerBlocks(pText).find((b) => b.key === 'proposal-criteria')
|
|
691
|
+
if (block) {
|
|
692
|
+
criteria = block.contentLines
|
|
693
|
+
.map((l) => l.trim())
|
|
694
|
+
.filter((l) => /^\d+[.、]\s+/.test(l))
|
|
695
|
+
.map((l) => l.replace(/^\d+[.、]\s+/, ''))
|
|
696
|
+
} else {
|
|
697
|
+
// adopt 回提关闭编号通道:proposal 其他节(变更范围/非目标)的编号列表会误劫持
|
|
698
|
+
criteria = extractSuccessCriteria(pText, { numberedChannel: false })
|
|
699
|
+
}
|
|
700
|
+
} catch { /* 无 proposal 可回提 → 空,draft 各自兜底 */ }
|
|
701
|
+
}
|
|
702
|
+
const crit = Array.isArray(criteria) && criteria.length > 0 ? criteria : null
|
|
703
|
+
const withTasks = existsSync(join(changeDir, 'tasks'))
|
|
704
|
+
const drafts = [
|
|
705
|
+
{ file: 'proposal.md', make: () => draftProposal({ change, input, criteria: crit }) },
|
|
706
|
+
{ file: 'requirements.md', make: () => draftRequirements({ change, criteria: crit, input }) },
|
|
707
|
+
{ file: 'design.md', make: () => draftDesignRecord({ change }) },
|
|
708
|
+
{ file: 'tasks.md', make: () => draftTasks({ change, criteria: crit, withTasks }) },
|
|
709
|
+
]
|
|
710
|
+
const ledgerPath = draftLedgerPath(runtimeRoot, change)
|
|
711
|
+
let ledger = null
|
|
712
|
+
try { ledger = JSON.parse(readFileSync(ledgerPath, 'utf8')) } catch { ledger = null }
|
|
713
|
+
const drafted = []
|
|
714
|
+
for (const { file, make } of drafts) {
|
|
715
|
+
if (existsSync(join(changeDir, file))) continue
|
|
716
|
+
const d = make()
|
|
717
|
+
writeAtomicSync(join(changeDir, file), d.text)
|
|
718
|
+
drafted.push(file)
|
|
719
|
+
if (!ledger) ledger = { schemaVersion: 1, change, generatedAt: new Date().toISOString(), files: {}, amendments: [] }
|
|
720
|
+
ledger.files[file] = d.sections
|
|
721
|
+
}
|
|
722
|
+
if (drafted.length > 0) writeAtomicSync(ledgerPath, JSON.stringify(ledger, null, 2) + '\n')
|
|
723
|
+
return { drafted }
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* 收编追加测试绑定槽(2026-09-25-thin-brainstorm-prestage adopt 路径消费面):头脑风暴产物
|
|
728
|
+
* requirements 是 agent 手写、无绑定面——按文中实际 FR 编号(###/## FR-NN 标题)追加机器
|
|
729
|
+
* 绑定节(纯 AGENT 槽面,不指纹);已有绑定槽/无 requirements → no-op(redraft 先产新稿自带槽)。
|
|
730
|
+
*/
|
|
731
|
+
export function ensureBindingSlots({ changeDir }) {
|
|
732
|
+
const path = join(changeDir, 'requirements.md')
|
|
733
|
+
if (!existsSync(path)) return { appended: false, slots: 0 }
|
|
734
|
+
let text = readText(path)
|
|
735
|
+
if (/<!--\s*AGENT:测试绑定/.test(text)) return { appended: false, slots: 0 }
|
|
736
|
+
const ids = []
|
|
737
|
+
for (const m of text.matchAll(/(?:^|\n)#{2,4}\s*(FR-\d+)[^\n]*/g)) {
|
|
738
|
+
const id = m[1]
|
|
739
|
+
if (!ids.includes(id)) ids.push(id)
|
|
740
|
+
}
|
|
741
|
+
const list = ids.length > 0 ? ids : ['FR-01']
|
|
742
|
+
const slots = list.map((id) => `<!--AGENT:测试绑定${id} 哪个测试文件/用例覆盖这条 FR(项目相对全路径+用例名,如 test/foo.test.mjs「用例组」或 test/foo.test.mjs#用例;无测试面写「不适用:理由」)——例外裁决书写面(机器段之外合法) -->`)
|
|
743
|
+
const section = `\n## 测试绑定(收编追加——每条 FR 至少一行:test 文件路径或用例名;不适用要写理由;flow done 空槽拒收)\n\n${slots.join('\n\n')}\n`
|
|
744
|
+
writeAtomicSync(path, text.endsWith('\n') ? text + section : text + '\n' + section)
|
|
745
|
+
return { appended: true, slots: list.length }
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
export default { draftAll, amendFlowDraft, verifyFlowDrafts, verifyDesignRecordFilled, verifyRequirementBindings, extractRequirementBindings, ensureBindingSlots, draftDesignRecord, redraftMissingArtifacts, draftDecisions, extractSuccessCriteria, draftLedgerPath }
|