sillyspec 3.33.1 → 3.33.2
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-flow/SKILL.md +17 -9
- package/AGENTS.md +2 -2
- package/README.md +2 -2
- package/assets/command-cards/flow.md +8 -4
- package/package.json +1 -1
- package/src/cross-repo-reconcile.js +4 -1
- package/src/decision-distill.js +22 -8
- package/src/decision-values.js +34 -0
- package/src/decisions-io.js +5 -2
- package/src/flow-draft.js +15 -9
- package/src/flow-parity.js +93 -5
- package/src/flow-review-pack.js +183 -0
- package/src/flow-review.js +45 -3
- package/src/flow.js +254 -47
- package/src/index.js +21 -14
- package/src/init.js +7 -30
- package/src/run/command.js +1 -1
- package/src/run/gate-snapshot.js +47 -14
- package/src/run/gates.js +1 -1
- package/src/scope-audit.js +6 -1
- package/src/stage-templates.js +3 -2
- package/src/stages/brainstorm.js +5 -4
- package/src/task-tick.js +22 -0
- package/templates/agents-instruction.md +2 -2
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* flow-review-pack.js — 轻量评审任务书预消化材料包(2026-10-19-thin-review-material-pack)。
|
|
3
|
+
*
|
|
4
|
+
* 病根:轻量评审任务书把原始件(requirements/design/change.patch)原样丢给零上下文评审员,
|
|
5
|
+
* 定位类通读 + 大 patch 整吞 prefill 是主要成本(冻结件实测最大 365KB)。完整流程五阶段
|
|
6
|
+
* 2026-09-19 已为同一病根(评审 ~200x 信息放大)建过材料包(review-material-pack.js),
|
|
7
|
+
* 本模块把同款机制移植到轻量道,三条护栏(design 记录):
|
|
8
|
+
* ① 纯机械抽取零模型摘要——全部产出来自确定性行/正则解析,防「自述换皮」破坏「不信自述」;
|
|
9
|
+
* ② 包=基准面非禁读面——段尾印基准面语义行(照抄完整流程 D-004:包外定向查证须列明、
|
|
10
|
+
* 包不足写 cannot_verify);
|
|
11
|
+
* ③ 摘要从冻结件本身算——change.patch 文本直解,不走活 git(review-material-pack 的
|
|
12
|
+
* extractDiffSummary 走活 git 解 base 那条路不能搬:防「包说的」与评审员手里冻结面漂移)。
|
|
13
|
+
*/
|
|
14
|
+
import { readFileSync } from 'node:fs'
|
|
15
|
+
import { join } from 'node:path'
|
|
16
|
+
|
|
17
|
+
/** 材料包整体体积帽(对齐 review-material-pack.js PACK_MAX_CHARS 先例——包不能成为新的全量面)。 */
|
|
18
|
+
export const PACK_MAX_CHARS = 12000
|
|
19
|
+
|
|
20
|
+
/** 段标题与段尾基准面语义行(护栏②:照抄完整流程 D-004 语义——包=基准面非禁读面)。 */
|
|
21
|
+
const PACK_TITLE = '【预消化材料包(CLI 机械抽取——定位索引非证据)】'
|
|
22
|
+
const PACK_BASELINE = ' (基准面:包=定位索引非证据;包外定向查证须列明;包不足以作答写 cannot_verify)'
|
|
23
|
+
|
|
24
|
+
/** FR 标题行(h3):### FR-NN: 标题——锚形态与 requirements 机器起草模板一致。 */
|
|
25
|
+
const FR_HEADING_RE = /^###\s+(FR-\d+)\s*[::]?\s*(.*)$/
|
|
26
|
+
|
|
27
|
+
/** 测试绑定节内行:FR-NN: 绑定内容。 */
|
|
28
|
+
const FR_BINDING_LINE_RE = /^(FR-\d+)\s*[::]\s*(.+)$/
|
|
29
|
+
|
|
30
|
+
/** diff 文件头:diff --git a/<旧> b/<新>——路径取 b/ 侧(rename 时即新名)。 */
|
|
31
|
+
const DIFF_GIT_RE = /^diff --git a\/(.+?) b\/(.+)$/
|
|
32
|
+
|
|
33
|
+
/** hunk 头(计数用,不解析行号——摘要只要 hunk 数)。 */
|
|
34
|
+
const HUNK_RE = /^@@ -\d+(?:,\d+)? \+\d+(?:,\d+)? @@/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* FR↔测试绑定表解析(纯函数):h3 FR 标题按出现序成行,「## 测试绑定」节内 `FR-NN: …`
|
|
38
|
+
* 行按 id 归并。requirements 缺 FR 标题 → 空数组(调用方按该子源零产出处理)。
|
|
39
|
+
* @returns {{ fr: string, title: string, binding: string }[]}
|
|
40
|
+
*/
|
|
41
|
+
export function parseFrBindingRows(requirementsText) {
|
|
42
|
+
const lines = String(requirementsText || '').replace(/\r\n/g, '\n').split('\n')
|
|
43
|
+
const rows = []
|
|
44
|
+
for (const line of lines) {
|
|
45
|
+
const m = line.match(FR_HEADING_RE)
|
|
46
|
+
if (m) rows.push({ fr: m[1], title: String(m[2] || '').trim(), binding: '' })
|
|
47
|
+
}
|
|
48
|
+
if (rows.length === 0) return rows
|
|
49
|
+
let inBinding = false
|
|
50
|
+
for (const line of lines) {
|
|
51
|
+
if (/^##\s/.test(line)) { inBinding = /^##\s*测试绑定/.test(line); continue }
|
|
52
|
+
if (!inBinding) continue
|
|
53
|
+
const m = line.match(FR_BINDING_LINE_RE)
|
|
54
|
+
if (m) {
|
|
55
|
+
const row = rows.find((r) => r.fr === m[1])
|
|
56
|
+
if (row) row.binding = String(m[2] || '').trim()
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return rows
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 冻结 patch 文件级摘要(纯函数):按 `diff --git` 头分文件,统计每文件 +增/−删行数
|
|
64
|
+
* (排除 +++/--- 文件名行)与 hunk 数;rename 取 b/ 侧新路径;Binary 行零计数容错。
|
|
65
|
+
* 无文件头的杂文本 → 空数组(不抛错——垃圾进空出,调用方按零产出处理)。
|
|
66
|
+
* @returns {{ path: string, plus: number, minus: number, hunks: number }[]}
|
|
67
|
+
*/
|
|
68
|
+
export function parseFrozenPatchSummary(patchText) {
|
|
69
|
+
const text = String(patchText || '')
|
|
70
|
+
if (!text.trim()) return []
|
|
71
|
+
const lines = text.replace(/\r\n/g, '\n').split('\n')
|
|
72
|
+
const files = []
|
|
73
|
+
let cur = null
|
|
74
|
+
for (const line of lines) {
|
|
75
|
+
const dg = line.match(DIFF_GIT_RE)
|
|
76
|
+
if (dg) {
|
|
77
|
+
if (cur) files.push(cur)
|
|
78
|
+
cur = { path: dg[2].trim(), plus: 0, minus: 0, hunks: 0 }
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
if (!cur) continue
|
|
82
|
+
if (HUNK_RE.test(line)) { cur.hunks += 1; continue }
|
|
83
|
+
if (line.startsWith('+++') || line.startsWith('---')) continue
|
|
84
|
+
if (line.startsWith('+')) cur.plus += 1
|
|
85
|
+
else if (line.startsWith('-')) cur.minus += 1
|
|
86
|
+
}
|
|
87
|
+
if (cur) files.push(cur)
|
|
88
|
+
return files
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 归属切分归一(fail-soft 入参):非法/缺席 → null(调用方零注入该子段);数组字段缺省按空。
|
|
93
|
+
* 文件路径统一正斜杠归一(Windows/WSL 混用兼容——评审对象路径口径与冻结件一致)。
|
|
94
|
+
*/
|
|
95
|
+
function normalizeAttribution(attribution) {
|
|
96
|
+
if (!attribution || typeof attribution !== 'object' || Array.isArray(attribution)) return null
|
|
97
|
+
const norm = (p) => String(p || '').replace(/\\/g, '/').replace(/^\.\//, '')
|
|
98
|
+
const asList = (v) => (Array.isArray(v) ? v.map(norm).filter(Boolean) : [])
|
|
99
|
+
const foreign = Array.isArray(attribution.foreign)
|
|
100
|
+
? attribution.foreign
|
|
101
|
+
.filter((f) => f && typeof f === 'object' && f.file)
|
|
102
|
+
.map((f) => ({ file: norm(f.file), owners: Array.isArray(f.owners) ? f.owners.map((o) => String(o)).filter(Boolean) : [] }))
|
|
103
|
+
: []
|
|
104
|
+
return { declaredIn: asList(attribution.declaredIn), declaredOut: asList(attribution.declaredOut), foreign }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** 归属切分三段渲染(2026-10-19-thin-review-dispatch-verbatim):零计数照常渲染(不静默隐藏)。 */
|
|
108
|
+
function attributionLines(a) {
|
|
109
|
+
const join = (xs) => (xs.length > 0 ? xs.join('、') : '(无)')
|
|
110
|
+
const foreignLines = a.foreign.map((f) => `${f.file}←${f.owners.length > 0 ? f.owners.join('/') : '未知变更'}`)
|
|
111
|
+
return [
|
|
112
|
+
' 冻结面归属切分(CLI 机械核算——定位索引非证据):',
|
|
113
|
+
` - 声明内(design 变更清单 ∩ 冻结面,${a.declaredIn.length} 文件):${join(a.declaredIn)}`,
|
|
114
|
+
` - 声明外(冻结面−声明清单,${a.declaredOut.length} 文件——共享仓并行混入/生成物候选,抽查即可勿逐文件 git 考古):${join(a.declaredOut)}`,
|
|
115
|
+
` - 他侧归因(提交/声明归属其他变更,${a.foreign.length} 文件):${join(foreignLines)}`,
|
|
116
|
+
]
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 归属切分集合运算(纯函数——flow done review 子步装配用):声明内 = design 变更清单 ∩
|
|
121
|
+
* 冻结面(他侧归因文件除外,归属歧义按他侧披露处理);声明外 = 冻结面 − 声明清单 − 他侧
|
|
122
|
+
* 归因;他侧归因原样归一化(owner 去重保序)。路径统一正斜杠 + 去 `./` 前缀 + 冻结面去重
|
|
123
|
+
* (Windows/WSL 混用与 patch 同文件多段兼容)。纯集合运算零 IO——git/解析失败由调用方
|
|
124
|
+
* fail-soft(源头失败 → 不调用本函数)。
|
|
125
|
+
* @param {{ declaredFiles?: string[], freezeFiles?: string[], foreign?: Array<{file: string, owners?: string[]}> }} args
|
|
126
|
+
* @returns {{ declaredIn: string[], declaredOut: string[], foreign: Array<{file: string, owners: string[]}> }}
|
|
127
|
+
*/
|
|
128
|
+
export function buildFreezeAttribution({ declaredFiles, freezeFiles, foreign }) {
|
|
129
|
+
const norm = (p) => String(p || '').replace(/\\/g, '/').replace(/^\.\//, '')
|
|
130
|
+
const declared = new Set((Array.isArray(declaredFiles) ? declaredFiles : []).map(norm).filter(Boolean))
|
|
131
|
+
const frozen = [...new Set((Array.isArray(freezeFiles) ? freezeFiles : []).map(norm).filter(Boolean))]
|
|
132
|
+
const foreignList = (Array.isArray(foreign) ? foreign : [])
|
|
133
|
+
.filter((f) => f && typeof f === 'object' && f.file)
|
|
134
|
+
.map((f) => ({ file: norm(f.file), owners: [...new Set((Array.isArray(f.owners) ? f.owners : []).map(String).filter(Boolean))] }))
|
|
135
|
+
const foreignSet = new Set(foreignList.map((f) => f.file))
|
|
136
|
+
return {
|
|
137
|
+
declaredIn: frozen.filter((f) => declared.has(f) && !foreignSet.has(f)),
|
|
138
|
+
declaredOut: frozen.filter((f) => !declared.has(f) && !foreignSet.has(f)),
|
|
139
|
+
foreign: foreignList,
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* 装配材料包注入行(唯一组包入口):FR 表、patch 摘要、归属切分三子源各自可得各自产出、
|
|
145
|
+
* 全缺返回 null(调用方零注入)。体积帽行级截断(不切半行),截断注记置于基准面语义行之前——
|
|
146
|
+
* 语义行恒为末行;标题+正文+注记+语义行总长恒 ≤ PACK_MAX_CHARS(截断预留注记预算)。
|
|
147
|
+
* requirements.md 读失败(缺件)→ FR 子段零产出;attribution 缺席/非法 → 归属子段零产出
|
|
148
|
+
* (fail-soft 不抛穿,其余子段不受影响)。
|
|
149
|
+
* @param {{ changeDir: string, patchText: string|null, attribution?: object|null }} args
|
|
150
|
+
* @returns {{ lines: string[], truncated: boolean } | null}
|
|
151
|
+
*/
|
|
152
|
+
export function assembleReviewPack({ changeDir, patchText, attribution = null }) {
|
|
153
|
+
let frRows = []
|
|
154
|
+
try {
|
|
155
|
+
frRows = parseFrBindingRows(readFileSync(join(changeDir, 'requirements.md'), 'utf8'))
|
|
156
|
+
} catch { /* 缺件 → FR 子段零产出 */ }
|
|
157
|
+
const summary = parseFrozenPatchSummary(patchText)
|
|
158
|
+
if (frRows.length === 0 && summary.length === 0) return null
|
|
159
|
+
const body = []
|
|
160
|
+
if (frRows.length > 0) {
|
|
161
|
+
body.push(` FR↔测试绑定表(requirements.md 机械解析,${frRows.length} 条):`)
|
|
162
|
+
for (const r of frRows) body.push(` - ${r.fr}: ${r.title}${r.binding ? ` | 绑定: ${r.binding}` : ' | 绑定: (无绑定行)'}`)
|
|
163
|
+
}
|
|
164
|
+
if (summary.length > 0) {
|
|
165
|
+
body.push(` 冻结 patch 文件级摘要(change.patch 机械解析,${summary.length} 文件):`)
|
|
166
|
+
for (const f of summary) body.push(` - ${f.path}(+${f.plus}/-${f.minus},${f.hunks} hunk)`)
|
|
167
|
+
}
|
|
168
|
+
const attr = normalizeAttribution(attribution)
|
|
169
|
+
if (attr) body.push(...attributionLines(attr))
|
|
170
|
+
const note = ` …(材料包超 ${PACK_MAX_CHARS} 字符截断——需全文时定向查证并列明)`
|
|
171
|
+
let budget = PACK_MAX_CHARS - PACK_TITLE.length - PACK_BASELINE.length - note.length
|
|
172
|
+
const lines = [PACK_TITLE]
|
|
173
|
+
let truncated = false
|
|
174
|
+
for (const l of body) {
|
|
175
|
+
const w = l.length + 1
|
|
176
|
+
if (w > budget) { truncated = true; break }
|
|
177
|
+
lines.push(l)
|
|
178
|
+
budget -= w
|
|
179
|
+
}
|
|
180
|
+
if (truncated) lines.push(note)
|
|
181
|
+
lines.push(PACK_BASELINE)
|
|
182
|
+
return { lines, truncated }
|
|
183
|
+
}
|
package/src/flow-review.js
CHANGED
|
@@ -86,6 +86,22 @@ function readSlotAnswer(text, markerRe) {
|
|
|
86
86
|
return buf.join('\n').trim()
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/**
|
|
90
|
+
* design 四节已答判定(v1 AGENT 槽 / v2 节作答双格式——status 与 task tick 共享单一源,
|
|
91
|
+
* 2026-10-19-approve-gate-tick-nudge FR-03 上移;判据与 flow status 原内联逻辑逐字一致:
|
|
92
|
+
* v1 = 槽标记行后紧跟非空行的槽数 ≥3;v2 = DESIGN_QUESTIONS 四节实质作答(问题行剥离)≥3)。
|
|
93
|
+
*/
|
|
94
|
+
export function designSectionsFilled(dText) {
|
|
95
|
+
const text = String(dText || '').replace(/\r\n/g, '\n')
|
|
96
|
+
if (!text) return false
|
|
97
|
+
if (/<!--AGENT:槽\d+/.test(text)) {
|
|
98
|
+
return Array.from(text.matchAll(/<!--AGENT:槽\d+[^\n]*-->\n(\S)/g)).length >= 3
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
return DESIGN_QUESTIONS_FALLBACK.sections.filter((s) => readV2SectionAnswer(text, s.heading, DESIGN_QUESTIONS_FALLBACK)).length >= 3
|
|
102
|
+
} catch { return false }
|
|
103
|
+
}
|
|
104
|
+
|
|
89
105
|
/**
|
|
90
106
|
* 读 v2 纯 markdown design 节作答(2026-10-04-thin-docs-v2):节标题行到下一节标题之间的
|
|
91
107
|
* 非空行,剥离四问原文行(DESIGN_QUESTIONS 单一源同源引入——独立字符串会被评审实证为
|
|
@@ -204,7 +220,7 @@ export function classifyReviewNeed({ changeDir, input = '', patchText = null, ed
|
|
|
204
220
|
* 印出完整 sha 并要求 review.json 的 reviewedAgainst 照抄;漂移重冻结据此区分「对着旧冻结面的
|
|
205
221
|
* 过期评审」与「对着当前 HEAD 的刚完成评审」(实测竞态:复审 PASS 落盘 26 秒后被误隔离)。
|
|
206
222
|
*/
|
|
207
|
-
export function renderReviewerTaskbook({ change, changeDir, head = null, probeLines = null }) {
|
|
223
|
+
export function renderReviewerTaskbook({ change, changeDir, head = null, probeLines = null, packLines = null }) {
|
|
208
224
|
const headFull = String(head || '').trim()
|
|
209
225
|
// 前轮 findings 注入(2026-10-07-wave-auto-adopt-review-dedup task-02):复审场景(前轮 FAIL/
|
|
210
226
|
// 评审隔离重评)下任务书带上前轮清单——已报项只验修复与回归,重点找新增问题,勿整轮重报
|
|
@@ -234,12 +250,29 @@ export function renderReviewerTaskbook({ change, changeDir, head = null, probeLi
|
|
|
234
250
|
].join('\n')
|
|
235
251
|
}
|
|
236
252
|
} catch { /* 无 review.json / 损坏 → 首评形态,零注入 */ }
|
|
253
|
+
// 冻结面形态(2026-10-19-thin-empty-face-cross-repo-freeze):JSON-only 跨仓冻结没有
|
|
254
|
+
// change.patch,交付 diff 在 change-patch.json 的 scopeAudit.repos[]——任务书材料与
|
|
255
|
+
// 对象表述必须指向真实在場的冻结件,否则评审员按缺失文件判 patch 失败。
|
|
256
|
+
const hasMainPatchFile = existsSync(join(changeDir, 'change.patch'))
|
|
257
|
+
let repoPatchKeys = []
|
|
258
|
+
try {
|
|
259
|
+
const meta = JSON.parse(readFileSync(join(changeDir, 'change-patch.json'), 'utf8'))
|
|
260
|
+
const repos = meta && meta.scopeAudit && Array.isArray(meta.scopeAudit.repos) ? meta.scopeAudit.repos : []
|
|
261
|
+
repoPatchKeys = repos
|
|
262
|
+
.filter((r) => r && typeof r.patch === 'string' && r.patch.length > 0)
|
|
263
|
+
.map((r) => String(r.key || '?'))
|
|
264
|
+
} catch { /* 无/坏 JSON → 单仓形态 */ }
|
|
265
|
+
const freezeLabel = hasMainPatchFile ? 'change.patch 冻结面' : repoPatchKeys.length > 0 ? 'change-patch.json 跨仓冻结面' : '冻结面'
|
|
237
266
|
const taskbook = [
|
|
238
267
|
`⚖️ 独立评审任务书 — ${change}`,
|
|
239
268
|
`══════════════════════════════════════`,
|
|
269
|
+
// 派发纪律(2026-10-19-thin-review-dispatch-verbatim):对齐厚道先例(stage-review.js:803 /
|
|
270
|
+
// index.js:2576)——实测主会话派发时手写缩编 prompt 会把材料包整段丢掉(4 次派发零 pack);
|
|
271
|
+
// 检查单/材料包生产权在 CLI,原样复制是纪律不是建议。无条件在场(非可选注入)。
|
|
272
|
+
`(派发纪律:本任务书原样复制给干净上下文子代理——检查单/材料包生产权在 CLI,勿自行改写、增删或缩编)`,
|
|
240
273
|
`【角色】你是独立评审员(干净上下文,未参与实现)——拿承诺对代码,不信自述,零误报标准。`,
|
|
241
274
|
...(headFull ? [
|
|
242
|
-
`【评审对象】工作区实态 + git HEAD(${headFull}
|
|
275
|
+
`【评审对象】工作区实态 + git HEAD(${headFull})——${freezeLabel}若落后于该 HEAD,以`,
|
|
243
276
|
` 工作区实态与 git log/show 核对为准;review.json 的 reviewedAgainst 字段照抄上面的完整 sha。`,
|
|
244
277
|
] : [
|
|
245
278
|
`【评审对象】工作区实态 + git HEAD——review.json 的 reviewedAgainst 字段填 git rev-parse HEAD`,
|
|
@@ -249,7 +282,16 @@ export function renderReviewerTaskbook({ change, changeDir, head = null, probeLi
|
|
|
249
282
|
`【材料(按需 Read,请求预算硬帽 12 次——超帽即止,包不足以作答写 cannot_verify)】`,
|
|
250
283
|
` - ${join(changeDir, 'requirements.md')}(FR 承诺 + 每条 FR 的测试绑定作答)`,
|
|
251
284
|
` - ${join(changeDir, 'design.md')}(设计承诺:做法/接口契约/盲维四问作答/风险)`,
|
|
252
|
-
|
|
285
|
+
...(hasMainPatchFile
|
|
286
|
+
? [` - ${join(changeDir, 'change.patch')}(交付 diff 冻结件——代码事实的唯一来源)`]
|
|
287
|
+
: repoPatchKeys.length > 0
|
|
288
|
+
? [` - ${join(changeDir, 'change-patch.json')}(跨仓交付 diff 冻结件——scopeAudit.repos[] 的 ${repoPatchKeys.join('、')} 仓 patch 正文;主仓面为空故无 change.patch,diff 在 JSON 内联)`]
|
|
289
|
+
: [` - ${join(changeDir, 'change.patch')}(若缺失按 patch 留档失败处理)`]),
|
|
290
|
+
// 预消化材料包(2026-10-19-thin-review-material-pack):CLI 机械抽取的 FR↔测试绑定表 +
|
|
291
|
+
// 冻结 patch 文件级摘要(flow-review-pack.js 装配,12000 帽行级截断)——砍零上下文评审员
|
|
292
|
+
// 的定位类读与大 patch 整吞 prefill;段自带基准面语义行(包=索引非证据);缺省/空数组
|
|
293
|
+
// 零注入——任务书与现状逐字一致(既有调用方/既有测试零感知)。
|
|
294
|
+
...(Array.isArray(packLines) && packLines.length > 0 ? ['', ...packLines] : []),
|
|
253
295
|
...(priorFindingsMd ? priorFindingsMd.split('\n') : []),
|
|
254
296
|
``,
|
|
255
297
|
`【检查单(逐条作答,P1=承诺被违反或有数据正确性风险)】`,
|