sillyspec 3.31.0 → 3.32.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 +10 -6
- package/.claude/skills/sillyspec-quick/SKILL.md +105 -7
- package/.zcodeignore +66 -0
- package/AGENTS.md +16 -16
- package/assets/command-cards/flow.md +9 -3
- package/assets/command-cards/run-quick.md +4 -1
- package/package.json +2 -2
- package/src/agent-session-log.js +27 -5
- package/src/config-schema.js +21 -0
- package/src/datetime.js +51 -0
- package/src/db.js +10 -3
- package/src/decision-distill.js +21 -12
- package/src/design-facts.js +46 -6
- package/src/flow-draft.js +468 -95
- package/src/flow-parity.js +62 -4
- package/src/flow-review.js +136 -6
- package/src/flow.js +729 -125
- package/src/foreign-declared.js +71 -3
- package/src/fr-index.js +148 -162
- package/src/git-helper.js +16 -3
- package/src/hooks/worktree-guard.js +16 -5
- package/src/hunk-attribution.js +28 -5
- package/src/index.js +186 -19
- package/src/knowledge-classify.js +3 -1
- package/src/knowledge-digest.js +55 -26
- package/src/knowledge-match.js +135 -7
- package/src/knowledge-stats.js +66 -4
- package/src/knowledge-vector.js +160 -0
- package/src/machine-interface.js +90 -2
- package/src/module-impact.js +22 -1
- package/src/modules.js +9 -0
- package/src/progress/change-registry.js +20 -0
- package/src/progress/stage-machine.js +4 -7
- package/src/progress.js +8 -1
- package/src/quicklog.js +20 -0
- package/src/redomain.js +23 -7
- package/src/route-hindsight.js +276 -0
- package/src/run/archive-distill.js +3 -1
- package/src/run/command.js +65 -12
- package/src/run/complete-handlers.js +219 -5
- package/src/run/complete.js +77 -11
- package/src/run/gate-snapshot.js +15 -0
- package/src/run/gates.js +60 -4
- package/src/run/prompt.js +159 -31
- package/src/run/quick-audit.js +66 -7
- package/src/run/shared.js +6 -11
- package/src/run/stage.js +5 -1
- package/src/run/verify-quality-scan.js +84 -0
- package/src/scan-facts.js +2 -1
- package/src/scope-audit.js +9 -2
- package/src/sentinel-assertions.js +67 -16
- package/src/stage-contract-engine.js +4 -1
- package/src/stage-contract-spec.js +103 -4
- package/src/stage-contract.js +55 -7
- package/src/stages/brainstorm.js +27 -14
- package/src/stages/knowledge.js +4 -3
- package/src/stages/plan-postcheck.js +98 -3
- package/src/stages/plan.js +22 -2
- package/src/stages/quick.js +1 -1
- package/src/stages/verify.js +2 -1
- package/src/task-review.js +1 -1
- package/src/task-tick.js +121 -0
- package/src/taskcard-frontmatter.js +31 -0
- package/src/ui-visual.js +43 -8
- package/src/verify-postcheck.js +509 -37
- package/src/verify-probes.js +216 -5
- package/src/watcher.js +1067 -1031
- package/src/worktree-apply.js +9 -1
- package/src/worktree.js +21 -6
- package/templates/agents-instruction.md +15 -15
- package/templates/prompts/taskcard-rules.md +1 -0
package/src/flow-draft.js
CHANGED
|
@@ -1,26 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* flow-draft.js —
|
|
2
|
+
* flow-draft.js — 轻量道工件起草器(双代并存:指纹 v1 / 纯 markdown v2)。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* v2(2026-10-04-thin-docs-v2,OpenSpec 对照 + 厚道结构门禁经验):文档=纯 markdown 人类
|
|
5
|
+
* 可读正文——零 MACHINE-DRAFT 指纹标记、零 AGENT 槽注释;FR 骨架只到标题锚(### FR-NN:
|
|
6
|
+
* <成功标准原文>),SHALL 正文与 Scenario 场景块由 agent 撰写(空答/缺强度词在 flow done
|
|
7
|
+
* 拒收);防篡改锚点(成功标准原文 + design 四问文本)搬进 draft ledger 机器态
|
|
8
|
+
* (schemaVersion:2 的 anchor 字段),flow done 做文档↔锚对比——问题被删/节被清空拒收,
|
|
9
|
+
* 成功标准在 requirements 面消失出漂移 advisory。四问文本单一源=DESIGN_QUESTIONS
|
|
10
|
+
* 常量(起草端与验收端同源,防镜像漂移)。tasks.md:成功标准逐条转写的工作分解种子
|
|
11
|
+
* (2026-10-07-thin-tasks-v3——镜像锚契约退役,agent 自由增删改;文件内零指令)。
|
|
10
12
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* 守卫全在验收侧(护栏#2:零 prompt 劝说):flow done 工件校验子步对 ledger 在案的每文件
|
|
17
|
-
* verifyMarkers 三态拒收(标记缺失/哈希失配/手工重锚未审计);flow amend-draft 是唯一留痕
|
|
18
|
-
* 修改通道(reanchorText 重锚 + ledger amendments 审计)。
|
|
13
|
+
* v1(存量在途变更,冻结不改):全件机器起草 + MACHINE-DRAFT 指纹标记 + AGENT 槽书写面;
|
|
14
|
+
* - proposal:--input 机械转写;requirements:GWT 骨架预填;tasks:成功标准→checkbox 镜像;
|
|
15
|
+
* - 机器段经 machine-draft.wrapSection 包裹(sha256 指纹),draft-ledger 记段哈希+首版原文;
|
|
16
|
+
* - 守卫在验收侧:flow done verifyMarkers 三态拒收;flow amend-draft 唯一留痕修改通道。
|
|
17
|
+
* v1 判据=ledger.schemaVersion 缺省(1);redraft/verify 按 ledger 代别自动选轨(双轨)。
|
|
19
18
|
*/
|
|
20
19
|
import { existsSync, readFileSync, mkdirSync, readdirSync } from 'node:fs'
|
|
21
20
|
import { join, dirname } from 'node:path'
|
|
22
21
|
import { writeAtomicSync } from './fs-atomic.js'
|
|
23
22
|
import { wrapSection, verifyMarkers, reanchorText, bodyHash, parseMarkerBlocks } from './machine-draft.js'
|
|
23
|
+
// 占位句常量真同源(2026-10-03-fr-skeleton-gate 评审 P3 清偿):起草端 Then 兜底与骨架判据
|
|
24
|
+
// (fr-index.isThinSkeletonBodies)共用同一字面量——两处独立字符串会被评审实证为「镜像非同源」。
|
|
25
|
+
import { SKELETON_THEN_PLACEHOLDER } from './fr-index.js'
|
|
24
26
|
|
|
25
27
|
/** 读文件并归一化 CRLF→LF(2026-09-25-feedback-fixes④:Python/编辑器写盘 CRLF 会破坏
|
|
26
28
|
锚定的槽位识别 regex)。 */
|
|
@@ -29,6 +31,40 @@ function readText(path) { return readFileSync(path, 'utf8').replace(/\r\n/g, '\n
|
|
|
29
31
|
const AMEND_CMD = (change) => `sillyspec flow amend-draft --change ${change}`
|
|
30
32
|
const GUARD_NOTE = '整段改写会被 flow done 拒收'
|
|
31
33
|
|
|
34
|
+
/** v2 ledger 代别标记(draft ledger schemaVersion === 2 → 纯 markdown 轨;缺省=1 指纹轨)。 */
|
|
35
|
+
export const DRAFT_SCHEMA_V2 = 2
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* design 四问单一源常量(v2):起草端逐字写入 design.md,验收端(verifyThinDocsV2)对
|
|
39
|
+
* ledger.anchor.designQuestions 逐字比对——两处独立字符串会被评审实证为「镜像非同源」。
|
|
40
|
+
* v1 指纹稿的问题文本冻结在其 wrapped 段内(存量在途变更零影响),不回读本常量。
|
|
41
|
+
*/
|
|
42
|
+
export const DESIGN_QUESTIONS = {
|
|
43
|
+
sections: [
|
|
44
|
+
{ key: 'approach', heading: '做法概述', lines: ['本变更怎么解决问题?改哪里、为什么选这个方案(一两段)。'] },
|
|
45
|
+
{ key: 'contract', heading: '接口契约', lines: ['动了哪些函数/端点/命令/文件格式?对外可见的签名或行为变化是什么(含「无」的说明)?'] },
|
|
46
|
+
{
|
|
47
|
+
key: 'boundaries',
|
|
48
|
+
heading: '边界与并发(盲维四问——每问必答,答不了即设计缺口)',
|
|
49
|
+
lines: [
|
|
50
|
+
'1. 乱序/迟到到达:输入或事件乱序时,本设计的假设还成立吗?',
|
|
51
|
+
'2. 并发写:两个执行体同时操作同一数据/文件会发生什么?',
|
|
52
|
+
'3. 切换/生命周期:会话、请求或变更中途切换/中断时状态是否安全?',
|
|
53
|
+
'4. 作用域:跨工作区/跨仓/多实例时数据会不会串台?',
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
{ key: 'risks', heading: '风险与死路', lines: ['本方案最大的风险是什么?试过但放弃的方案及放弃理由?'] },
|
|
57
|
+
],
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** FR 行为句强度词判据(v2 验收端与模板指引共用):正文行含任一 → 视为已撰写行为句。
|
|
61
|
+
* 词表=SHALL/MUST/SHOULD(2026-10-04-strength-should P2 清偿:模板指引列 SHOULD 为合法
|
|
62
|
+
* 强度,SHOULD NOT 由 SHOULD\b 前缀覆盖)+中文 必须/禁止。
|
|
63
|
+
* 位置中英同构(2026-10-05-wordpos:anchor-triggerpull 收口实证——旧版英文被行首锚定
|
|
64
|
+
* (须「- 系统 MUST」形态)而中文任意命中,句中写英文强度词的合法行为句被误拒收;
|
|
65
|
+
* 占位句自带词表字样的误放行由 pending 检查((待撰写 前缀)独立拦截,不依赖本判据)。 */
|
|
66
|
+
const FR_STRENGTH_RE = /\b(SHALL|MUST|SHOULD)\b|必须|禁止/
|
|
67
|
+
|
|
32
68
|
/** sidecar 台账路径(.runtime 下,verify-draft sidecar 同族;命名空间=文件:段键)。 */
|
|
33
69
|
export function draftLedgerPath(runtimeRoot, changeName) {
|
|
34
70
|
return join(runtimeRoot, `draft-ledger-${changeName}.json`)
|
|
@@ -37,28 +73,17 @@ export function draftLedgerPath(runtimeRoot, changeName) {
|
|
|
37
73
|
const AGENT_SLOT = (n, hint) => `<!--AGENT:槽${n} ${hint}——例外裁决书写面(机器段之外合法) -->`
|
|
38
74
|
|
|
39
75
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
76
|
+
* 复合标准拆分 v2 起退役(2026-10-04-thin-docs-v2 FR-03):「A/B」「A;B」保持整条——
|
|
77
|
+
* 拆分是静默变形(谓词词表误判成对短名词/路径形态的历史坑全在同链),一条标准一行是
|
|
78
|
+
* --input 书写者的责任,机器不做语义猜测。v1 在途变更的 redraft 同享本口径(摘录端
|
|
79
|
+
* 单一实现,双轨差异只在文档形态不在摘录语义)。
|
|
43
80
|
*/
|
|
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
81
|
|
|
56
82
|
/**
|
|
57
83
|
* 从任务原话摘「成功标准」条目(行级机械提取:「成功标准/验收」节下的条目行;无节则回退列表行)。
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* 回提路径经 opts.numberedChannel=false 关闭(proposal 其他节的编号列表会误劫持)。
|
|
84
|
+
* 编号劫持通道 v2 起退役(2026-10-04-thin-docs-v2 FR-03):正文「1. …」编号条目不再取代成功
|
|
85
|
+
* 标准节(动机/背景节的编号列点被误劫持为 FR 是实证变形)——条目只认「成功标准」节与无节时的
|
|
86
|
+
* 列表行。opts.numberedChannel 参数保留兼容(redraft 调用面)但恒无效。
|
|
62
87
|
*/
|
|
63
88
|
/**
|
|
64
89
|
* 续行合并判定(cli-protocol-trust,R17 实证):括号/引号未闭合,或行尾悬空连接符
|
|
@@ -123,20 +148,9 @@ export function extractSuccessCriteria(input, opts = {}) {
|
|
|
123
148
|
if (!sawSection && /^[-*•]\s+\S/.test(raw)) { criteria.push(raw.replace(/^[-*•]\s+/, '')); continue }
|
|
124
149
|
if (inSection) {
|
|
125
150
|
const item = raw.replace(/^[-*•\d.)、]+\s*/, '')
|
|
126
|
-
if (item && !/^#/.test(item)) criteria.push(
|
|
151
|
+
if (item && !/^#/.test(item)) criteria.push(item)
|
|
127
152
|
}
|
|
128
153
|
}
|
|
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
154
|
return criteria
|
|
141
155
|
}
|
|
142
156
|
|
|
@@ -170,27 +184,265 @@ function draftProposal({ change, input, criteria }) {
|
|
|
170
184
|
return { text, sections: collectSections(text) }
|
|
171
185
|
}
|
|
172
186
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
187
|
+
// ════════════════════════════ v2 纯 markdown 起草族(2026-10-04-thin-docs-v2)════════════════════════════
|
|
188
|
+
// 文档=人类可读正文(零指纹标记零 AGENT 槽注释);FR 只给标题锚(成功标准原文逐字),
|
|
189
|
+
// SHALL 正文/Scenario 场景块/绑定行由 agent 撰写,空答在验收端拒收(护栏#2:守卫在验收侧)。
|
|
190
|
+
|
|
191
|
+
/** proposal v2:动机原话转写 + 范围/成功标准纯文本清单。 */
|
|
192
|
+
function draftProposalV2({ change, input, criteria }) {
|
|
193
|
+
const crit = criteria || []
|
|
194
|
+
const lines = [
|
|
195
|
+
'---',
|
|
196
|
+
`author: flow-machine-draft`,
|
|
197
|
+
`created_at: ${new Date().toISOString()}`,
|
|
198
|
+
'---',
|
|
199
|
+
`# 提案书(Proposal)— ${change}`,
|
|
200
|
+
'',
|
|
201
|
+
'## 动机',
|
|
202
|
+
'',
|
|
203
|
+
`任务原话转写:${input || '(未提供 --input)'}`,
|
|
204
|
+
'',
|
|
205
|
+
'## 变更范围',
|
|
206
|
+
'',
|
|
207
|
+
...(crit.length > 0
|
|
208
|
+
? [`按成功标准机械推导,共 ${crit.length} 条验收面:`, ...crit.map((c, i) => `${i + 1}. ${c}`)]
|
|
209
|
+
: ['(--input 未含成功标准条目——flow done 测试门与工件校验为默认验收面)']),
|
|
210
|
+
'',
|
|
211
|
+
'## 成功标准(可验证)',
|
|
212
|
+
'',
|
|
213
|
+
...(crit.length > 0
|
|
214
|
+
? crit.map((c, i) => `${i + 1}. ${c}`)
|
|
215
|
+
: ['1. flow done 六子步全绿(测试门实测通过+工件校验通过)']),
|
|
216
|
+
'',
|
|
217
|
+
]
|
|
218
|
+
return lines.join('\n')
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** requirements v2:FR 标题锚(成功标准原文逐字、不截断)+ SHALL/Scenario 撰写指引 + 纯文本绑定行。 */
|
|
222
|
+
function draftRequirementsV2({ change, criteria }) {
|
|
223
|
+
const crit = criteria && criteria.length > 0 ? criteria : null
|
|
224
|
+
const n = crit ? crit.length : 1
|
|
225
|
+
const frBlocks = (crit || ['(按任务语义撰写)']).map((c, i) => {
|
|
226
|
+
const id = `FR-${String(i + 1).padStart(2, '0')}`
|
|
227
|
+
return [
|
|
228
|
+
`### ${id}: ${c}`,
|
|
229
|
+
'',
|
|
230
|
+
`- (待撰写:把本条成功标准改写为一句可判定的行为规定并标约束强度——必须/禁止/SHOULD/可以)`,
|
|
231
|
+
'',
|
|
232
|
+
'#### 场景:主路径',
|
|
233
|
+
'',
|
|
234
|
+
'(按需保留或改写:Given 前提 / When 触发 / Then 可判定预期——每边界情形一个场景块,归档索引用场景名作摘要)',
|
|
235
|
+
].join('\n')
|
|
236
|
+
})
|
|
237
|
+
const bindingRows = Array.from({ length: n }, (_, i) =>
|
|
238
|
+
`FR-${String(i + 1).padStart(2, '0')}: (待填——哪个测试文件/用例覆盖这条 FR;无测试面写「不适用:理由」)`)
|
|
239
|
+
return [
|
|
240
|
+
'---',
|
|
241
|
+
`author: flow-machine-draft`,
|
|
242
|
+
`created_at: ${new Date().toISOString()}`,
|
|
243
|
+
'---',
|
|
244
|
+
`# 需求规格(Requirements)— ${change}`,
|
|
245
|
+
'',
|
|
246
|
+
'## 功能需求',
|
|
247
|
+
'',
|
|
248
|
+
...frBlocks.flatMap((b) => [b, '']),
|
|
249
|
+
'## 测试绑定(每条 FR 至少一行——`FR-NN: test/路径「用例名」`;空行/待填在 flow done 拒收)',
|
|
250
|
+
'',
|
|
251
|
+
...bindingRows.join('\n').split('\n'),
|
|
252
|
+
'',
|
|
253
|
+
].join('\n')
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** design v2:四节问题文本(DESIGN_QUESTIONS 单一源逐字)+ 作答区(答案写在问题下方)。 */
|
|
257
|
+
function draftDesignRecordV2({ change }) {
|
|
258
|
+
const secs = DESIGN_QUESTIONS.sections.map((s) => [
|
|
259
|
+
`## ${s.heading}`,
|
|
260
|
+
'',
|
|
261
|
+
...s.lines,
|
|
262
|
+
'',
|
|
263
|
+
])
|
|
264
|
+
return [
|
|
265
|
+
'---',
|
|
266
|
+
`author: flow-machine-draft`,
|
|
267
|
+
`created_at: ${new Date().toISOString()}`,
|
|
268
|
+
'---',
|
|
269
|
+
`# 设计记录(Design Record)— ${change}`,
|
|
270
|
+
'',
|
|
271
|
+
...secs.flat(),
|
|
272
|
+
].join('\n')
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** tasks v2:工作分解种子(2026-10-07-thin-tasks-v3,镜像锚契约退役)——成功标准逐条转写为
|
|
276
|
+
* `- [ ] task-NN:` 工作行(多数成功标准本就是祈使工作语句),agent 按真实实现路径自由增删改;
|
|
277
|
+
* 零文件内指令(书写规则由 flow start 横幅与命令卡承载——文件里只剩任务本体)。 */
|
|
278
|
+
function draftTasksV2({ change, criteria, withTasks }) {
|
|
279
|
+
const crit = criteria || []
|
|
280
|
+
const rows = crit.length > 0
|
|
281
|
+
? crit.map((c, i) => `- [ ] task-${String(i + 1).padStart(2, '0')}: ${c}`)
|
|
282
|
+
: ['- [ ] task-01: 完成实现并使 flow done 六子步全绿']
|
|
283
|
+
return [
|
|
284
|
+
'---',
|
|
285
|
+
`author: flow-machine-draft`,
|
|
286
|
+
`created_at: ${new Date().toISOString()}`,
|
|
287
|
+
'---',
|
|
288
|
+
`# 任务注册表(Tasks)— ${change}`,
|
|
289
|
+
'',
|
|
290
|
+
...rows,
|
|
291
|
+
'',
|
|
292
|
+
].join('\n')
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* v2 工件校验(flow done artifacts 子步消费面;ledger.schemaVersion===2 时取代指纹三态+槽位门)。
|
|
297
|
+
* 拒收项(violations):四问被删/节空答、FR 未撰写行为句(缺强度词)、绑定行空/待填、
|
|
298
|
+
* 四件缺失/结构缺失、tasks 工作队列空。漂移 advisory:锚内成功标准在 requirements FR 标题中
|
|
299
|
+
* 失踪(门柱漂移嫌疑——agent 改写是合法例外,advisory 提示审核面核对,不阻断;tasks 侧
|
|
300
|
+
* 零 advisory——工作分解归 agent,2026-10-07-thin-tasks-v3)。
|
|
301
|
+
* adopted brainstorm 变更(opts.skipDesign)豁免 design 四节门(设计承诺以 brainstorm design 为准)。
|
|
302
|
+
*/
|
|
303
|
+
export function verifyThinDocsV2({ changeDir, ledger, skipDesign = false }) {
|
|
304
|
+
const violations = []
|
|
305
|
+
const advisories = []
|
|
306
|
+
const anchorCriteria = Array.isArray(ledger?.anchor?.criteria) ? ledger.anchor.criteria : []
|
|
307
|
+
const read = (f) => {
|
|
308
|
+
try { return readFileSync(join(changeDir, f), 'utf8').replace(/\r\n/g, '\n') } catch { return null }
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// design 四节:问题在场(对锚逐字)+ 实质作答
|
|
312
|
+
if (!skipDesign) {
|
|
313
|
+
const dText = read('design.md')
|
|
314
|
+
if (dText === null) violations.push('design.md 缺失(v2 起草件在案)——删除后重入 flow start 补生成')
|
|
315
|
+
else {
|
|
316
|
+
const anchorSections = ledger?.anchor?.designQuestions?.sections || DESIGN_QUESTIONS.sections
|
|
317
|
+
const dLines = dText.split('\n')
|
|
318
|
+
for (const sec of anchorSections) {
|
|
319
|
+
if (!dLines.some((l) => l.trim() === `## ${sec.heading}`)) {
|
|
320
|
+
violations.push(`design「${sec.heading}」节被删(标题缺失)——恢复小节与四问原文后作答`)
|
|
321
|
+
continue
|
|
322
|
+
}
|
|
323
|
+
for (const q of sec.lines) {
|
|
324
|
+
if (!dLines.some((l) => l.trim() === q)) violations.push(`design「${sec.heading}」问题文本被改写:「${q.slice(0, 30)}…」——恢复原文(问题钉死,答案写在下方)`)
|
|
325
|
+
}
|
|
326
|
+
// 实质作答:节内非空行 - 标题行 - 问题行 - 引导行(> 开头)≥1
|
|
327
|
+
const start = dLines.findIndex((l) => l.trim() === `## ${sec.heading}`)
|
|
328
|
+
let end = dLines.findIndex((l, i) => i > start && /^##\s/.test(l))
|
|
329
|
+
if (end === -1) end = dLines.length
|
|
330
|
+
const qSet = new Set(sec.lines)
|
|
331
|
+
const answered = dLines.slice(start + 1, end).some((l) => {
|
|
332
|
+
const t = l.trim()
|
|
333
|
+
return t && !qSet.has(t) && !t.startsWith('>') && !t.startsWith('##')
|
|
334
|
+
})
|
|
335
|
+
if (!answered) violations.push(`design「${sec.heading}」未作答——每节至少一行(小改动可写「不适用:<理由>」)`)
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// requirements:FR 行为句 + 绑定行
|
|
341
|
+
const rText = read('requirements.md')
|
|
342
|
+
const frIds = []
|
|
343
|
+
if (rText === null) violations.push('requirements.md 缺失——删除后重入 flow start 补生成')
|
|
344
|
+
else {
|
|
345
|
+
const rLines = rText.split('\n')
|
|
346
|
+
const frHeadRe = /^### (FR-\d{2}):\s*(.*)$/
|
|
347
|
+
for (let i = 0; i < rLines.length; i++) {
|
|
348
|
+
const m = rLines[i].match(frHeadRe)
|
|
349
|
+
if (!m) continue
|
|
350
|
+
frIds.push(m[1])
|
|
351
|
+
// 块体:到下一 ### / ## 或文尾
|
|
352
|
+
let j = i + 1
|
|
353
|
+
const body = []
|
|
354
|
+
while (j < rLines.length && !/^(###|##)\s/.test(rLines[j])) { body.push(rLines[j]); j++ }
|
|
355
|
+
const hasStrength = body.some((l) => FR_STRENGTH_RE.test(l))
|
|
356
|
+
const pending = body.some((l) => /^\s*-?\s*(待撰写/.test(l.trim()))
|
|
357
|
+
if (!hasStrength || pending) violations.push(`${m[1]} 行为句未撰写——正文须含强度词(必须/禁止/SHOULD/SHOULD NOT/MUST)的一句可判定行为规定`)
|
|
358
|
+
i = j - 1
|
|
359
|
+
}
|
|
360
|
+
if (frIds.length === 0) violations.push('requirements 功能需求区为空——至少一条 `### FR-NN: 标题` + 行为句')
|
|
361
|
+
// 绑定行:## 测试绑定 节内 `FR-NN: 内容`
|
|
362
|
+
const bindStart = rLines.findIndex((l) => /^##\s*测试绑定/.test(l))
|
|
363
|
+
if (bindStart === -1) violations.push('requirements「## 测试绑定」节缺失——每条 FR 至少一行绑定')
|
|
364
|
+
else {
|
|
365
|
+
const bindRows = new Map()
|
|
366
|
+
for (let i = bindStart + 1; i < rLines.length && !/^##\s/.test(rLines[i]); i++) {
|
|
367
|
+
const bm = rLines[i].match(/^(FR-\d{2})[::]\s*(.*)$/)
|
|
368
|
+
if (bm) bindRows.set(bm[1], bm[2].trim())
|
|
369
|
+
}
|
|
370
|
+
for (const id of frIds) {
|
|
371
|
+
const v = bindRows.get(id)
|
|
372
|
+
if (v === undefined) violations.push(`${id} 测试绑定行缺失——格式 \`FR-NN: test/路径「用例名」\`(无测试面写「不适用:理由」)`)
|
|
373
|
+
else if (!v || v.startsWith('(待填')) violations.push(`${id} 测试绑定未作答——空行/待填在收口拒收`)
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// tasks:工作队列非空(零任务行 → 拒收)。镜像锚契约退役(2026-10-07-thin-tasks-v3):
|
|
379
|
+
// 任务面归 agent 自由改写,成功标准对账面只在 requirements FR 标题锚——tasks 侧零 advisory。
|
|
380
|
+
const tText = read('tasks.md')
|
|
381
|
+
if (tText === null) violations.push('tasks.md 缺失——删除后重入 flow start 补生成')
|
|
382
|
+
else {
|
|
383
|
+
const taskRows = [...tText.matchAll(/^- \[([ xX])\] (task-\d+):/gm)]
|
|
384
|
+
if (taskRows.length === 0) violations.push('tasks.md 无任何 task-NN 行——工作队列不可为空(至少一条 `- [ ] task-NN:` 实现步骤)')
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// proposal:在场 + 成功标准节非空
|
|
388
|
+
const pText = read('proposal.md')
|
|
389
|
+
if (pText === null) violations.push('proposal.md 缺失——删除后重入 flow start 补生成')
|
|
390
|
+
else if (!/^##\s*成功标准/m.test(pText)) violations.push('proposal「## 成功标准」节缺失——门柱锚(删除会致漂移判定失真)')
|
|
391
|
+
|
|
392
|
+
// 漂移 advisory:requirements 的 FR 标题锚是对账面(归档索引用它)——锚文本从 requirements
|
|
393
|
+
// 消失即 advisory(子字包含语义:轻改写保留原文不误报;agent 合法改写是书写面权利,advisory
|
|
394
|
+
// 交审核面核对语义未失真,不阻断)。tasks 不参与(2026-10-07-thin-tasks-v3:工作分解归 agent,
|
|
395
|
+
// 成功标准对账只看 FR 标题锚)。proposal 不参与(成功标准节=--input 转写件,非对账面)。
|
|
396
|
+
for (let i = 0; i < anchorCriteria.length; i++) {
|
|
397
|
+
const c = String(anchorCriteria[i] || '').trim()
|
|
398
|
+
if (!c) continue
|
|
399
|
+
const inFr = rText !== null && rText.includes(c)
|
|
400
|
+
if (!inFr) advisories.push(`成功标准第 ${i + 1} 条原文未在 requirements 见到(FR 标题锚漂移嫌疑——若属有意改写请核对语义未失真):${c.slice(0, 40)}${c.length > 40 ? '…' : ''}`)
|
|
401
|
+
}
|
|
402
|
+
return { violations, advisories }
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
|
|
178
406
|
/**
|
|
179
407
|
* 成功标准 → GWT 骨架预填(governance-autopilot,R20 实证:agent 手写 FR +12 轮 Edit)——
|
|
180
408
|
* 从标准文本机械推导三段。骨架进场 agent 可覆盖(消灭空槽冷启动,agent 只需改错的不需从零写)。
|
|
181
409
|
*/
|
|
410
|
+
/** When/Then 分隔符扫描(2026-10-03-fr-inject-relevance-rank):只在括号深度 0 处生效——
|
|
411
|
+
* 括号内箭头/「则/使得」不切(实证:平台仓 2026-09-30-breadcrumb-dedupe-zh FR-03 的
|
|
412
|
+
* 「(changes→变更中心 等)」被当分隔符,When=半截括号、Then=后半截)。返回 [when, then] 或
|
|
413
|
+
* null(无深度 0 切分点)。导出供 test 直测。 */
|
|
414
|
+
export function splitGwtSeparator(criterion) {
|
|
415
|
+
const OPEN = new Set(['(', '(', '【', '[', '「', '『'])
|
|
416
|
+
const CLOSE = new Set([')', ')', '】', ']', '」', '』'])
|
|
417
|
+
const s = String(criterion || '')
|
|
418
|
+
let depth = 0
|
|
419
|
+
for (let i = 0; i < s.length; i++) {
|
|
420
|
+
const ch = s[i]
|
|
421
|
+
if (OPEN.has(ch)) { depth++; continue }
|
|
422
|
+
if (CLOSE.has(ch)) { if (depth > 0) depth--; continue }
|
|
423
|
+
if (depth > 0) continue
|
|
424
|
+
if (ch === '→' || ch === '➜') return [s.slice(0, i), s.slice(i + 1)]
|
|
425
|
+
if (ch === '则') return [s.slice(0, i), s.slice(i + 1)]
|
|
426
|
+
if (ch === '使' && s.startsWith('使得', i)) return [s.slice(0, i), s.slice(i + 2)]
|
|
427
|
+
}
|
|
428
|
+
return null
|
|
429
|
+
}
|
|
430
|
+
|
|
182
431
|
function draftGwtSkeleton(criterion, index) {
|
|
183
432
|
const c = String(criterion || '').trim()
|
|
184
433
|
const id = `FR-${String(index + 1).padStart(2, '0')}`
|
|
185
434
|
if (!c) return `### ${id}: (待描述)\nGiven 系统就绪\nWhen 执行目标行为\nThen 达成预期`
|
|
186
435
|
const kw = c.match(/(api|端点|接口|存储|迁移|鉴权|幂等|上限|正序|增量|append-only|前端|组件|折叠|高亮|徽标|轮询|测试|E2E|curl)/gi)
|
|
187
436
|
const given = kw && kw.length > 0 ? `Given ${[...new Set(kw.map(k => k.toLowerCase()))].slice(0, 3).join(' / ')} 相关模块就绪` : 'Given 系统就绪'
|
|
188
|
-
const
|
|
189
|
-
const when = (
|
|
190
|
-
const then = (
|
|
191
|
-
|
|
437
|
+
const parts = splitGwtSeparator(c)
|
|
438
|
+
const when = (parts && parts[0] ? parts[0] : c).trim().slice(0, 80)
|
|
439
|
+
const then = (parts && parts[1] ? parts[1] : SKELETON_THEN_PLACEHOLDER).trim().slice(0, 80)
|
|
440
|
+
// 标题不截断(2026-10-03-fr-inject-relevance-rank):旧 slice(0,50) 硬切留永久残句(平台仓
|
|
441
|
+
// FR-components-shared-038「…专业术语除」实证)且随归档固化进知识索引——标题全量保留。
|
|
442
|
+
return `### ${id}: ${c}\n${given}\nWhen ${when}\nThen ${then}`
|
|
192
443
|
}
|
|
193
444
|
|
|
445
|
+
/** requirements 机器稿 v1(指纹代,存量在途变更 redraft 专用;新生成走 draftRequirementsV2)。 */
|
|
194
446
|
function draftRequirements({ change, criteria, input }) {
|
|
195
447
|
const crit = criteria || []
|
|
196
448
|
// GWT 骨架预填(governance-autopilot):FR 区直接生成完整 Given/When/Then 块——agent 可覆盖
|
|
@@ -223,15 +475,35 @@ function draftRequirements({ change, criteria, input }) {
|
|
|
223
475
|
}
|
|
224
476
|
|
|
225
477
|
/**
|
|
226
|
-
* requirements「测试绑定」槽位门(flow done artifacts 子步消费面):每条 FR
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
478
|
+
* requirements「测试绑定」槽位门(flow done artifacts 子步消费面):每条 FR 的绑定行非空
|
|
479
|
+
* (「不适用:理由」=已答)。双格式:v2 纯文本行(`## 测试绑定` 节内 `FR-NN: 内容`——
|
|
480
|
+
* 空行/(待填=未答);v1 AGENT 槽标记(<!--AGENT:测试绑定FR-NN → 下一段非空=已答)。
|
|
481
|
+
* FR 机器段在场但零绑定面 = 骨架早于本机制的旧版——修复路径是删 requirements.md 重入
|
|
482
|
+
* flow start。纯读盘面;无 requirements.md → not-applicable(redraft 补生成)。
|
|
230
483
|
*/
|
|
231
484
|
export function verifyRequirementBindings({ changeDir }) {
|
|
232
485
|
const path = join(changeDir, 'requirements.md')
|
|
233
486
|
if (!existsSync(path)) return { applicable: false, emptySlots: [] }
|
|
234
487
|
const text = readFileSync(path, 'utf8').replace(/\r\n/g, '\n')
|
|
488
|
+
// v2 纯文本绑定:文档含 ## 测试绑定 节且无 AGENT 绑定槽 → 走行级校验
|
|
489
|
+
if (/^##\s*测试绑定/m.test(text) && !/<!--\s*AGENT:测试绑定/.test(text)) {
|
|
490
|
+
const lines = text.split('\n')
|
|
491
|
+
const frIds = [...text.matchAll(/^### (FR-\d{2}):/gm)].map((m) => m[1])
|
|
492
|
+
const bindStart = lines.findIndex((l) => /^##\s*测试绑定/.test(l))
|
|
493
|
+
const rows = new Map()
|
|
494
|
+
for (let i = bindStart + 1; i < lines.length && !/^##\s/.test(lines[i]); i++) {
|
|
495
|
+
const bm = lines[i].match(/^(FR-\d{2})[::]\s*(.*)$/)
|
|
496
|
+
if (bm) rows.set(bm[1], bm[2].trim())
|
|
497
|
+
}
|
|
498
|
+
const emptySlots = []
|
|
499
|
+
for (const id of frIds.length > 0 ? frIds : ['FR-01']) {
|
|
500
|
+
const v = rows.get(id)
|
|
501
|
+
if (v === undefined) emptySlots.push(`测试绑定${id}(行缺失)`)
|
|
502
|
+
else if (!v || v.startsWith('(待填')) emptySlots.push(`测试绑定${id}`)
|
|
503
|
+
}
|
|
504
|
+
if (frIds.length === 0) emptySlots.push('FR区(agent 未填写功能需求——每条 FR 格式 ### FR-NN: 标题 + 强度词行为句)')
|
|
505
|
+
return { applicable: true, emptySlots }
|
|
506
|
+
}
|
|
235
507
|
const lines = text.split('\n')
|
|
236
508
|
const emptySlots = []
|
|
237
509
|
let current = null
|
|
@@ -282,7 +554,10 @@ export function verifyRequirementBindings({ changeDir }) {
|
|
|
282
554
|
* 「X」+可选 组/用例 后缀、#id、::id、> name(书写原形只摘不译,「:」后是描述不捕);
|
|
283
555
|
* 裸文件名/残缺路径段解析为项目相对全路径(直取存在优先,仓内唯一后缀命中,歧义原样保留)。
|
|
284
556
|
*/
|
|
285
|
-
|
|
557
|
+
// 扩展名交替按长度降序(tsx 先于 ts——坑 fr-domain-suggest 同链实证:2026-09-28 归档绑定行
|
|
558
|
+
// governance-cards.test.tsx 被截成 .test.ts,test-bindings 的 .ts→.tsx 变体兜底正是本序的
|
|
559
|
+
// 历史代偿;正则交替贪心取首个命中,短扩展名在前吃掉长扩展名的尾字符)
|
|
560
|
+
const TEST_PATH_TOKEN_RE = /[A-Za-z0-9_/.-]+\.(?:mjs|cjs|tsx|ts|js|py)/g
|
|
286
561
|
// 用例锚四形态(「X」+可选 组/用例 后缀、#id、::id、> name)——字符类排除竖线:锚条目
|
|
287
562
|
// 在 FR 机器子块按 `tests: a | b` 序列化、split('|') 回读(评审 P2),带竖线的锚在竖线处
|
|
288
563
|
// 截断(诚实降级),不产出裂格式幻影条目。
|
|
@@ -331,10 +606,20 @@ function extractTestAnchors(content, root, indexRef) {
|
|
|
331
606
|
const raw = m[0].replace(/^[./\\]+/, '').replace(/\\/g, '/')
|
|
332
607
|
if (!isTestPathToken(raw)) continue
|
|
333
608
|
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
609
|
const file = resolveTestPathRel(raw, root, indexRef)
|
|
336
|
-
|
|
337
|
-
|
|
610
|
+
// 连续锚段全收(坑 test-trace-tests-glue-bracket-note 边角,2026-09-27 实证:`path「a」「b」`
|
|
611
|
+
// 只粘首段、「b」静默丢弃):路径后紧跟的每一段用例锚(四形态任意序列)各产一条
|
|
612
|
+
// 书写原形条目;无锚产纯路径条目(原行为零变化)。
|
|
613
|
+
let rest = content.slice(m.index + m[0].length, winEnd)
|
|
614
|
+
let anchored = false
|
|
615
|
+
for (;;) {
|
|
616
|
+
const cm = rest.match(CASE_ANCHOR_RE)
|
|
617
|
+
if (!cm) break
|
|
618
|
+
out.push(file + cm[0].trim().replace(/^>\s*/, ' > '))
|
|
619
|
+
rest = rest.slice(cm[0].length)
|
|
620
|
+
anchored = true
|
|
621
|
+
}
|
|
622
|
+
if (!anchored) out.push(file)
|
|
338
623
|
}
|
|
339
624
|
return [...new Set(out)]
|
|
340
625
|
}
|
|
@@ -342,7 +627,31 @@ function extractTestAnchors(content, root, indexRef) {
|
|
|
342
627
|
export function extractRequirementBindings({ changeDir, change }) {
|
|
343
628
|
const path = join(changeDir, 'requirements.md')
|
|
344
629
|
if (!existsSync(path)) return []
|
|
345
|
-
const
|
|
630
|
+
const text = readFileSync(path, 'utf8').replace(/\r\n/g, '\n')
|
|
631
|
+
// v2 纯文本绑定行(`## 测试绑定` 节内 `FR-NN: 内容`)——tests 锚与 v1 同规则
|
|
632
|
+
// (extractTestAnchors:路径 token+可选用例锚;「不适用」/「(待填」不产行)。
|
|
633
|
+
if (/^##\s*测试绑定/m.test(text) && !/<!--\s*AGENT:测试绑定/.test(text)) {
|
|
634
|
+
let root = null
|
|
635
|
+
try {
|
|
636
|
+
const r = dirname(dirname(dirname(changeDir)))
|
|
637
|
+
root = existsSync(join(r, '.sillyspec')) ? r : process.cwd()
|
|
638
|
+
} catch { root = process.cwd() }
|
|
639
|
+
const indexRef = { files: null }
|
|
640
|
+
const lines = text.split('\n')
|
|
641
|
+
const bindStart = lines.findIndex((l) => /^##\s*测试绑定/.test(l))
|
|
642
|
+
const rows = []
|
|
643
|
+
for (let i = bindStart + 1; i >= 0 && i < lines.length && !/^##\s/.test(lines[i]); i++) {
|
|
644
|
+
const bm = lines[i].match(/^(FR-\d{2})[::]\s*(.*)$/)
|
|
645
|
+
if (!bm) continue
|
|
646
|
+
const content = bm[2].trim()
|
|
647
|
+
if (!content || content.startsWith('(待填') || /^不适用/.test(content)) continue
|
|
648
|
+
const tests = extractTestAnchors(content, root, indexRef)
|
|
649
|
+
if (tests.length === 0) continue
|
|
650
|
+
rows.push({ anchor: bm[1], row_id: `${change}:flow:测试绑定${bm[1]}`, tests, reason: 'spec', state: 'candidate', discovery: 'machine', confirmed_by: null, source_change: change })
|
|
651
|
+
}
|
|
652
|
+
return rows
|
|
653
|
+
}
|
|
654
|
+
const lines = text.split('\n')
|
|
346
655
|
// 仓根推导:changeDir 形如 <root>/.sillyspec/changes/<名>(主仓/worktree 同构,三层上溯);
|
|
347
656
|
// 推导失效(目录形态异构)回退 cwd
|
|
348
657
|
let root = null
|
|
@@ -437,13 +746,10 @@ function draftTasks({ change, criteria, withTasks }) {
|
|
|
437
746
|
'---',
|
|
438
747
|
`# 任务注册表(Tasks)— ${change}`,
|
|
439
748
|
'',
|
|
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
749
|
// tasks-rows 去指纹化(2026-09-25-feedback-fixes,平台狗粮反馈①):勾选行在指纹段内导致
|
|
445
750
|
// 勾一条就失配 → 必走 amend → editRatio=1 被判该走厚档——勾选纪律与指纹门自相矛盾。
|
|
446
751
|
// 改为裸 markdown,agent 直接勾;哨兵的防假勾逻辑独立于指纹(按提交 token 判据)。
|
|
752
|
+
// 2026-10-07-thin-tasks-v3:文件内指令块移除(v1 backfill 轨同 v2 契约——规则在横幅/命令卡)。
|
|
447
753
|
rows.join('\n'),
|
|
448
754
|
'',
|
|
449
755
|
].join('\n')
|
|
@@ -517,23 +823,32 @@ function collectSections(text) {
|
|
|
517
823
|
}
|
|
518
824
|
|
|
519
825
|
/**
|
|
520
|
-
* 起草全件落盘 + draft-ledger 记账(flow start
|
|
521
|
-
*
|
|
826
|
+
* 起草全件落盘 + draft-ledger 记账(flow start 消费面)。
|
|
827
|
+
* v2(新生成):纯 markdown 文档 + 锚入 ledger(criteria 原文 + 四问文本 + 首版全文——
|
|
828
|
+
* 漂移对比与 editRatio 的基准);文档正文零机器标记。
|
|
829
|
+
* @returns {{written: string[], ledgerPath: string, criteria: string[], schema: number}}
|
|
522
830
|
*/
|
|
523
831
|
export function draftAll({ changeDir, change, input, withTasks = false, runtimeRoot }) {
|
|
524
832
|
const criteria = extractSuccessCriteria(input)
|
|
525
833
|
detectFragmentedCriteria(criteria, `flow start --input 摘录(change=${change})`)
|
|
526
|
-
const
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
834
|
+
const texts = {
|
|
835
|
+
'proposal.md': draftProposalV2({ change, input, criteria }),
|
|
836
|
+
'requirements.md': draftRequirementsV2({ change, criteria }),
|
|
837
|
+
'design.md': draftDesignRecordV2({ change }),
|
|
838
|
+
'tasks.md': draftTasksV2({ change, criteria, withTasks }),
|
|
839
|
+
}
|
|
532
840
|
const written = []
|
|
533
|
-
const ledger = {
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
841
|
+
const ledger = {
|
|
842
|
+
schemaVersion: DRAFT_SCHEMA_V2,
|
|
843
|
+
change,
|
|
844
|
+
generatedAt: new Date().toISOString(),
|
|
845
|
+
anchor: { criteria, designQuestions: DESIGN_QUESTIONS },
|
|
846
|
+
files: {},
|
|
847
|
+
amendments: [],
|
|
848
|
+
}
|
|
849
|
+
for (const [file, text] of Object.entries(texts)) {
|
|
850
|
+
writeAtomicSync(join(changeDir, file), text)
|
|
851
|
+
ledger.files[file] = { text }
|
|
537
852
|
written.push(file)
|
|
538
853
|
}
|
|
539
854
|
if (withTasks) {
|
|
@@ -544,7 +859,7 @@ export function draftAll({ changeDir, change, input, withTasks = false, runtimeR
|
|
|
544
859
|
}
|
|
545
860
|
const ledgerPath = draftLedgerPath(runtimeRoot, change)
|
|
546
861
|
writeAtomicSync(ledgerPath, JSON.stringify(ledger, null, 2) + '\n')
|
|
547
|
-
return { written, ledgerPath, criteria }
|
|
862
|
+
return { written, ledgerPath, criteria, schema: DRAFT_SCHEMA_V2 }
|
|
548
863
|
}
|
|
549
864
|
|
|
550
865
|
/**
|
|
@@ -580,6 +895,43 @@ export function amendFlowDraft({ changeDir, change, runtimeRoot }) {
|
|
|
580
895
|
if (!existsSync(ledgerPath)) return { reanchored: [], files: [], editRatio: 0, sectionRatios: {} }
|
|
581
896
|
let ledger
|
|
582
897
|
try { ledger = JSON.parse(readFileSync(ledgerPath, 'utf8')) } catch { return { reanchored: [], files: [], editRatio: 0, sectionRatios: {} } }
|
|
898
|
+
// v2(2026-10-04-thin-docs-v2):无指纹段可重锚——editRatio 改对「首版全文↔当前全文」算
|
|
899
|
+
// (决策密度代理延续,基准=ledger.files[].text;口径=内容行——剥空行/frontmatter/标题/引导行,
|
|
900
|
+
// 结构行会把比例稀释到阈值下失真)。路由信号(返回的 sectionRatios→CLI 取段级最大)只含
|
|
901
|
+
// 决策载体 proposal/design:requirements 的 FR 撰写=需求澄清、tasks 的勾选/细化=合法日常动作,
|
|
902
|
+
// 计入会把每个 v2 变更都误报 route_hint:thick(与 v1「FR 段不进决策密度/tasks 去指纹」同裁)
|
|
903
|
+
// ——审计面在 ledger amendment 全量记录。
|
|
904
|
+
if (ledger.schemaVersion === DRAFT_SCHEMA_V2) {
|
|
905
|
+
const contentLines = (t) => String(t || '').split('\n').filter((l) => {
|
|
906
|
+
const s = l.trim()
|
|
907
|
+
return s && s !== '---' && !s.startsWith('#') && !s.startsWith('>')
|
|
908
|
+
})
|
|
909
|
+
const reanchored = []
|
|
910
|
+
const sectionRatios = {}
|
|
911
|
+
const auditRatios = {}
|
|
912
|
+
let totalOrig = 0
|
|
913
|
+
let totalChanged = 0
|
|
914
|
+
for (const [file, rec] of Object.entries(ledger.files || {})) {
|
|
915
|
+
if (!rec || typeof rec.text !== 'string') continue
|
|
916
|
+
let cur
|
|
917
|
+
try { cur = readText(join(changeDir, file)) } catch { continue }
|
|
918
|
+
const ratio = Math.round(computeEditRatio(contentLines(rec.text).join('\n'), contentLines(cur).join('\n')) * 1000) / 1000
|
|
919
|
+
auditRatios[file] = ratio
|
|
920
|
+
if (cur !== rec.text) reanchored.push(file)
|
|
921
|
+
if (file === 'proposal.md' || file === 'design.md') {
|
|
922
|
+
sectionRatios[file] = ratio
|
|
923
|
+
const aLines = contentLines(rec.text).length
|
|
924
|
+
totalOrig += aLines
|
|
925
|
+
totalChanged += Math.round(ratio * aLines)
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
if (reanchored.length > 0) {
|
|
929
|
+
ledger.amendments = [...(ledger.amendments || []), { at: new Date().toISOString(), files: reanchored, schema: DRAFT_SCHEMA_V2, sectionRatios: auditRatios }]
|
|
930
|
+
writeAtomicSync(ledgerPath, JSON.stringify(ledger, null, 2) + '\n')
|
|
931
|
+
}
|
|
932
|
+
const editRatio = totalOrig > 0 ? Math.round((totalChanged / totalOrig) * 1000) / 1000 : 0
|
|
933
|
+
return { reanchored, files: Object.keys(ledger.files || {}), editRatio, sectionRatios }
|
|
934
|
+
}
|
|
583
935
|
const reanchored = []
|
|
584
936
|
const sectionRatios = {}
|
|
585
937
|
let totalOrig = 0
|
|
@@ -616,12 +968,19 @@ export function amendFlowDraft({ changeDir, change, runtimeRoot }) {
|
|
|
616
968
|
return { reanchored, files: Object.keys(ledger.files || {}), editRatio, sectionRatios }
|
|
617
969
|
}
|
|
618
970
|
|
|
619
|
-
/** 三态拒收校验(flow done 工件子步消费面;无 ledger=not-applicable
|
|
620
|
-
|
|
971
|
+
/** 三态拒收校验(flow done 工件子步消费面;无 ledger=not-applicable)。双轨:
|
|
972
|
+
* schemaVersion===2 → verifyThinDocsV2(文档↔锚对比:问题/节/FR/绑定拒收 + 漂移 advisory);
|
|
973
|
+
* v1(缺省)→ verifyMarkers 指纹三态(存量在途变更零影响)。
|
|
974
|
+
* opts.skipDesign:adopted brainstorm 变更豁免 design 四节门(flow.js 判定后透传)。 */
|
|
975
|
+
export function verifyFlowDrafts({ changeDir, change, runtimeRoot, skipDesign = false }) {
|
|
621
976
|
const ledgerPath = draftLedgerPath(runtimeRoot, change)
|
|
622
977
|
if (!existsSync(ledgerPath)) return { applicable: false, violations: [] }
|
|
623
978
|
let ledger
|
|
624
979
|
try { ledger = JSON.parse(readFileSync(ledgerPath, 'utf8')) } catch { return { applicable: false, violations: [] } }
|
|
980
|
+
if (ledger.schemaVersion === DRAFT_SCHEMA_V2) {
|
|
981
|
+
const r = verifyThinDocsV2({ changeDir, ledger, skipDesign })
|
|
982
|
+
return { applicable: true, schema: DRAFT_SCHEMA_V2, ...r }
|
|
983
|
+
}
|
|
625
984
|
const violations = []
|
|
626
985
|
for (const [file, sections] of Object.entries(ledger.files || {})) {
|
|
627
986
|
const mdPath = join(changeDir, file)
|
|
@@ -632,7 +991,7 @@ export function verifyFlowDrafts({ changeDir, change, runtimeRoot }) {
|
|
|
632
991
|
}
|
|
633
992
|
violations.push(...verifyMarkers({ text, sections, amendCmd: AMEND_CMD(change) }))
|
|
634
993
|
}
|
|
635
|
-
return { applicable: true, violations }
|
|
994
|
+
return { applicable: true, schema: 1, violations, advisories: [] }
|
|
636
995
|
}
|
|
637
996
|
|
|
638
997
|
/**
|
|
@@ -701,23 +1060,34 @@ export function redraftMissingArtifacts({ changeDir, change, input, runtimeRoot
|
|
|
701
1060
|
}
|
|
702
1061
|
const crit = Array.isArray(criteria) && criteria.length > 0 ? criteria : null
|
|
703
1062
|
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
1063
|
const ledgerPath = draftLedgerPath(runtimeRoot, change)
|
|
711
1064
|
let ledger = null
|
|
712
1065
|
try { ledger = JSON.parse(readFileSync(ledgerPath, 'utf8')) } catch { ledger = null }
|
|
1066
|
+
// 双轨(2026-10-04-thin-docs-v2 FR-09):v1 在途变更(ledger.schemaVersion 缺省=1)补件
|
|
1067
|
+
// 沿用指纹稿(否则补件与存量件混代,verifyMarkers 拒收);无 ledger / v2 → 纯 markdown v2。
|
|
1068
|
+
const legacy = ledger != null && ledger.schemaVersion !== DRAFT_SCHEMA_V2
|
|
1069
|
+
const drafts = legacy
|
|
1070
|
+
? [
|
|
1071
|
+
{ file: 'proposal.md', make: () => draftProposal({ change, input, criteria: crit }) },
|
|
1072
|
+
{ file: 'requirements.md', make: () => draftRequirements({ change, criteria: crit, input }) },
|
|
1073
|
+
{ file: 'design.md', make: () => draftDesignRecord({ change }) },
|
|
1074
|
+
{ file: 'tasks.md', make: () => draftTasks({ change, criteria: crit, withTasks }) },
|
|
1075
|
+
]
|
|
1076
|
+
: [
|
|
1077
|
+
{ file: 'proposal.md', make: () => ({ text: draftProposalV2({ change, input, criteria: crit }) }) },
|
|
1078
|
+
{ file: 'requirements.md', make: () => ({ text: draftRequirementsV2({ change, criteria: crit }) }) },
|
|
1079
|
+
{ file: 'design.md', make: () => ({ text: draftDesignRecordV2({ change }) }) },
|
|
1080
|
+
{ file: 'tasks.md', make: () => ({ text: draftTasksV2({ change, criteria: crit, withTasks }) }) },
|
|
1081
|
+
]
|
|
713
1082
|
const drafted = []
|
|
1083
|
+
const criteriaForAnchor = crit || []
|
|
714
1084
|
for (const { file, make } of drafts) {
|
|
715
1085
|
if (existsSync(join(changeDir, file))) continue
|
|
716
1086
|
const d = make()
|
|
717
1087
|
writeAtomicSync(join(changeDir, file), d.text)
|
|
718
1088
|
drafted.push(file)
|
|
719
|
-
if (!ledger) ledger = { schemaVersion:
|
|
720
|
-
ledger.files[file] = d.sections
|
|
1089
|
+
if (!ledger) ledger = { schemaVersion: DRAFT_SCHEMA_V2, change, generatedAt: new Date().toISOString(), anchor: { criteria: criteriaForAnchor, designQuestions: DESIGN_QUESTIONS }, files: {}, amendments: [] }
|
|
1090
|
+
ledger.files[file] = legacy ? d.sections : { text: d.text }
|
|
721
1091
|
}
|
|
722
1092
|
if (drafted.length > 0) writeAtomicSync(ledgerPath, JSON.stringify(ledger, null, 2) + '\n')
|
|
723
1093
|
return { drafted }
|
|
@@ -732,17 +1102,20 @@ export function ensureBindingSlots({ changeDir }) {
|
|
|
732
1102
|
const path = join(changeDir, 'requirements.md')
|
|
733
1103
|
if (!existsSync(path)) return { appended: false, slots: 0 }
|
|
734
1104
|
let text = readText(path)
|
|
735
|
-
|
|
1105
|
+
// 双格式在场即 no-op:v1 AGENT 槽或 v2 纯文本节任一在场(redraft 先产新稿自带绑定面)
|
|
1106
|
+
if (/<!--\s*AGENT:测试绑定/.test(text) || /^##\s*测试绑定/m.test(text)) return { appended: false, slots: 0 }
|
|
736
1107
|
const ids = []
|
|
737
1108
|
for (const m of text.matchAll(/(?:^|\n)#{2,4}\s*(FR-\d+)[^\n]*/g)) {
|
|
738
1109
|
const id = m[1]
|
|
739
1110
|
if (!ids.includes(id)) ids.push(id)
|
|
740
1111
|
}
|
|
741
1112
|
const list = ids.length > 0 ? ids : ['FR-01']
|
|
742
|
-
|
|
743
|
-
|
|
1113
|
+
// v2 纯文本绑定节(2026-10-04-thin-docs-v2):行式 `FR-NN: (待填…)`——与 draftRequirementsV2
|
|
1114
|
+
// 及 verifyRequirementBindings/extractRequirementBindings 的 v2 分支同格式
|
|
1115
|
+
const slots = list.map((id) => `${id}: (待填——哪个测试文件/用例覆盖这条 FR;无测试面写「不适用:理由」)`)
|
|
1116
|
+
const section = `\n## 测试绑定(每条 FR 至少一行:\`FR-NN: test/路径「用例名」\`;不适用要写理由;flow done 空行拒收)\n\n${slots.join('\n')}\n`
|
|
744
1117
|
writeAtomicSync(path, text.endsWith('\n') ? text + section : text + '\n' + section)
|
|
745
1118
|
return { appended: true, slots: list.length }
|
|
746
1119
|
}
|
|
747
1120
|
|
|
748
|
-
export default { draftAll, amendFlowDraft, verifyFlowDrafts, verifyDesignRecordFilled, verifyRequirementBindings, extractRequirementBindings, ensureBindingSlots, draftDesignRecord, redraftMissingArtifacts, draftDecisions, extractSuccessCriteria, draftLedgerPath }
|
|
1121
|
+
export default { draftAll, amendFlowDraft, verifyFlowDrafts, verifyDesignRecordFilled, verifyRequirementBindings, verifyThinDocsV2, extractRequirementBindings, ensureBindingSlots, draftDesignRecord, redraftMissingArtifacts, draftDecisions, extractSuccessCriteria, draftLedgerPath, DESIGN_QUESTIONS, DRAFT_SCHEMA_V2 }
|