sillyspec 3.32.7 → 3.32.8
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/package.json +1 -1
- package/src/change-list.js +30 -7
- package/src/flow-draft.js +68 -6
- package/src/flow-state.js +21 -0
- package/src/flow.js +150 -28
- package/src/fr-index.js +10 -1
- package/src/index.js +47 -0
- package/src/progress.js +16 -0
- package/src/sentinel-assertions.js +13 -7
- package/src/stage-review.js +3 -0
- package/src/stages/knowledge.js +22 -0
- package/src/stages/plan-postcheck.js +42 -7
package/package.json
CHANGED
package/src/change-list.js
CHANGED
|
@@ -168,6 +168,17 @@ function looksLikePath(p) {
|
|
|
168
168
|
*/
|
|
169
169
|
const INCIDENTAL_RE = /顺带修复|附带修复|顺带|drive-?by|incidental/i
|
|
170
170
|
|
|
171
|
+
/**
|
|
172
|
+
* 「<repo-key> 仓变更」段头识别(单一源——2026-10-13-h2-repo-heading-parse 起定义在
|
|
173
|
+
* change-list.js 并导出;原 stages/plan-postcheck.js 模块内常量上移,plan-postcheck 改
|
|
174
|
+
* import 同一符号)。段头为 h2/h3:h3 子段形态由本文件消费(currentRepo 设置器),
|
|
175
|
+
* h2 形态在生态中是既定约定(brainstorm 厚道模板教学、plan-postcheck 覆盖对账
|
|
176
|
+
* parseDesignCoverageByRepo、design-facts/docs-check 存在性核验、评审清单四处按此解析)。
|
|
177
|
+
* 容忍可选编号前缀、(…)备注后缀(全/半角括号)与尾随空白/冒号;捕获组 1 = repo-key
|
|
178
|
+
* (限字母数字._-);key 是否在注册表由调用方 fail-closed 校验。
|
|
179
|
+
*/
|
|
180
|
+
export const REPO_SECTION_HEADER_RE = /^#{2,3}\s+(?:\d+[.)]\s*)?([A-Za-z0-9_.\-]+)\s*仓变更(?:\s*[((][^()()]*[))])?\s*[::]?\s*$/
|
|
181
|
+
|
|
171
182
|
/**
|
|
172
183
|
* 从清单子段标题 / 路径前缀解析跨仓 repoKey(scope-audit-cross-repo-blindness 改进点 2,
|
|
173
184
|
* 2026-09-15):多仓变更 design 清单按「main 仓 / sub-xxx / spdemo」分段书写,主仓跑
|
|
@@ -222,14 +233,16 @@ function _parseFileListDetailed(designMdPath, { keepSillyspecDocs = false, repoK
|
|
|
222
233
|
const sectionMatch = content.match(FILE_LIST_SECTION_RE)
|
|
223
234
|
if (!sectionMatch) return []
|
|
224
235
|
|
|
225
|
-
// 从标题后开始,截取到下一个 ##
|
|
236
|
+
// 从标题后开始,截取到下一个 ## 标题或文件末尾——「<注册key> 仓变更」段头(h2)是本节
|
|
237
|
+
// 子分段不截断(2026-10-13-h2-repo-heading-parse:段头是生态既定约定——brainstorm 模板/
|
|
238
|
+
// plan-postcheck 覆盖对账/design-facts 核验四处消费;此前 h2 段头在此终结「文件变更清单」
|
|
239
|
+
// 节,其后条目整段静默丢失,agent 照告警①书写即丢对账与冻结)。
|
|
226
240
|
const afterSection = content.slice(sectionMatch.index + sectionMatch[0].length)
|
|
227
|
-
const
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const lines = relevantContent.split('\n')
|
|
241
|
+
const lines = []
|
|
242
|
+
for (const line of afterSection.split('\n')) {
|
|
243
|
+
if (/^##\s/.test(line) && !REPO_SECTION_HEADER_RE.test(line)) break
|
|
244
|
+
lines.push(line)
|
|
245
|
+
}
|
|
233
246
|
let headerSkipped = false
|
|
234
247
|
let pathColIdx = 1 // 默认第 2 列;解析表头后可重定位
|
|
235
248
|
let opColIdx = -1 // 操作列下标(表头扫描后定位;-1 = 无操作列 → operation=null)
|
|
@@ -239,6 +252,16 @@ function _parseFileListDetailed(designMdPath, { keepSillyspecDocs = false, repoK
|
|
|
239
252
|
const entries = new Map() // path -> { path, operation, incidental, repo }(exclude 子段 delete)
|
|
240
253
|
|
|
241
254
|
for (const line of lines) {
|
|
255
|
+
// 「<key> 仓变更」段头(h2/h3,REPO_SECTION_HEADER_RE):仓分段切换——key 须在注册表内
|
|
256
|
+
// (fail-closed:未注册含 main → 主仓 repo=null,与 detectRepoFromSubsection 同口径);
|
|
257
|
+
// 段头重置分类子段态(新分段从 include 起、操作类别清空)。
|
|
258
|
+
const repoHdr = line.match(REPO_SECTION_HEADER_RE)
|
|
259
|
+
if (repoHdr) {
|
|
260
|
+
listMode = 'include'
|
|
261
|
+
currentOp = null
|
|
262
|
+
currentRepo = (Array.isArray(repoKeys) && repoKeys.includes(repoHdr[1])) ? repoHdr[1] : null
|
|
263
|
+
continue
|
|
264
|
+
}
|
|
242
265
|
// 分类列表子标题:### 新增文件 / ### 修改文件 / ### 不修改文件 / ### sub-grid-security(跨仓段)
|
|
243
266
|
const subHeader = line.match(/^###\s+(.+?)\s*$/)
|
|
244
267
|
if (subHeader) {
|
package/src/flow-draft.js
CHANGED
|
@@ -65,6 +65,27 @@ export const DESIGN_QUESTIONS = {
|
|
|
65
65
|
* 占位句自带词表字样的误放行由 pending 检查((待撰写 前缀)独立拦截,不依赖本判据)。 */
|
|
66
66
|
const FR_STRENGTH_RE = /\b(SHALL|MUST|SHOULD)\b|必须|禁止/
|
|
67
67
|
|
|
68
|
+
/** 去 whitespace 字符 bigram Jaccard 相似度(design 问题行回填定位——2026-10-13-design-question-autorepair)。
|
|
69
|
+
* 改述行由锚问题衍生、共享绝大多数字符对,天然高于任何答案行;≥0.5 视为可原位替换。 */
|
|
70
|
+
function charBigramSimilarity(a, b) {
|
|
71
|
+
const grams = (s) => {
|
|
72
|
+
const t = String(s || '').replace(/\s+/g, '')
|
|
73
|
+
const set = new Set()
|
|
74
|
+
for (let i = 0; i < t.length - 1; i++) set.add(t.slice(i, i + 2))
|
|
75
|
+
return set
|
|
76
|
+
}
|
|
77
|
+
const A = grams(a), B = grams(b)
|
|
78
|
+
let inter = 0
|
|
79
|
+
for (const g of A) if (B.has(g)) inter++
|
|
80
|
+
const union = A.size + B.size - inter
|
|
81
|
+
return union === 0 ? 0 : inter / union
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** design 问题行回填阈值:bigram 相似度 ≥ 此值定位到改述行(插入点=其上方),低于则节首插入。
|
|
85
|
+
* 回填只插入永不删除(评审 P1 清偿:引用问题原文的作答行相似度可胜过真改述行,任何替换式
|
|
86
|
+
* 回填都有静默销毁 agent 内容的面——插入式把「绝不删除」做成结构不变量,不再依赖阈值甄别)。 */
|
|
87
|
+
const QUESTION_REPAIR_SIM_THRESHOLD = 0.5
|
|
88
|
+
|
|
68
89
|
/** sidecar 台账路径(.runtime 下,verify-draft sidecar 同族;命名空间=文件:段键)。 */
|
|
69
90
|
export function draftLedgerPath(runtimeRoot, changeName) {
|
|
70
91
|
return join(runtimeRoot, `draft-ledger-${changeName}.json`)
|
|
@@ -303,25 +324,66 @@ function draftTasksV2({ change, criteria, withTasks }) {
|
|
|
303
324
|
export function verifyThinDocsV2({ changeDir, ledger, skipDesign = false }) {
|
|
304
325
|
const violations = []
|
|
305
326
|
const advisories = []
|
|
327
|
+
const repairs = []
|
|
306
328
|
const anchorCriteria = Array.isArray(ledger?.anchor?.criteria) ? ledger.anchor.criteria : []
|
|
307
329
|
const read = (f) => {
|
|
308
330
|
try { return readFileSync(join(changeDir, f), 'utf8').replace(/\r\n/g, '\n') } catch { return null }
|
|
309
331
|
}
|
|
310
332
|
|
|
311
|
-
// design 四节:问题在场(对锚逐字)+
|
|
333
|
+
// design 四节:问题在场(对锚逐字)+ 实质作答。问题行漂移(节标题在场)→ 机器按锚回填
|
|
334
|
+
// (2026-10-13-design-question-autorepair:sess_dcb57f25 实证 Write 整文件重写滑误——原文
|
|
335
|
+
// 3 秒前刚 Read 过仍被整段改述,事前提示是概率性防护;确定防线=锚对比,修被拦后的恢复面:
|
|
336
|
+
// 高相似改述原位替换、低相似/删除节首插入,agent 作答零丢失,repairs 留痕交 flow.js 渲染)。
|
|
312
337
|
if (!skipDesign) {
|
|
313
338
|
const dText = read('design.md')
|
|
314
339
|
if (dText === null) violations.push('design.md 缺失(v2 起草件在案)——删除后重入 flow start 补生成')
|
|
315
340
|
else {
|
|
316
341
|
const anchorSections = ledger?.anchor?.designQuestions?.sections || DESIGN_QUESTIONS.sections
|
|
317
|
-
|
|
342
|
+
let dLines = dText.split('\n')
|
|
343
|
+
// 回填通道:改写先到不了归档(回填后进归档的就是锚文本,与人工恢复等价),写盘失败降级
|
|
344
|
+
// 回带全文的拒收文案(不抛异常阻断 CLI)。
|
|
345
|
+
const allQuestionLines = new Set(anchorSections.flatMap((s) => s.lines))
|
|
346
|
+
const repairedLines = [...dLines]
|
|
347
|
+
const repairEntries = []
|
|
348
|
+
for (const sec of anchorSections) {
|
|
349
|
+
for (const q of sec.lines) {
|
|
350
|
+
if (repairedLines.some((l) => l.trim() === q)) continue
|
|
351
|
+
const hIdx = repairedLines.findIndex((l) => l.trim() === `## ${sec.heading}`)
|
|
352
|
+
if (hIdx === -1) continue // 节整删 → 下方校验环拒收(文案带全文)
|
|
353
|
+
let end = repairedLines.findIndex((l, i) => i > hIdx && /^##\s/.test(l))
|
|
354
|
+
if (end === -1) end = repairedLines.length
|
|
355
|
+
let bestIdx = -1, bestSim = 0
|
|
356
|
+
for (let i = hIdx + 1; i < end; i++) {
|
|
357
|
+
const t = repairedLines[i].trim()
|
|
358
|
+
if (!t || allQuestionLines.has(t)) continue
|
|
359
|
+
const sim = charBigramSimilarity(t, q)
|
|
360
|
+
if (sim > bestSim) { bestSim = sim; bestIdx = i }
|
|
361
|
+
}
|
|
362
|
+
// 只插入不替换(评审 P1 清偿):高相似定位 → 插在改述行上方(原文与改述相邻、可读性
|
|
363
|
+
// 最好);未定位 → 节首。任何路径都不改动既有行——误定位的最坏结果是插入位置欠佳。
|
|
364
|
+
const insertAt = bestIdx >= 0 && bestSim >= QUESTION_REPAIR_SIM_THRESHOLD
|
|
365
|
+
? bestIdx
|
|
366
|
+
: (repairedLines[hIdx + 1] === '' ? hIdx + 2 : hIdx + 1)
|
|
367
|
+
repairedLines.splice(insertAt, 0, q, '')
|
|
368
|
+
repairEntries.push({ heading: sec.heading, q })
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
if (repairEntries.length > 0) {
|
|
372
|
+
try {
|
|
373
|
+
writeAtomicSync(join(changeDir, 'design.md'), repairedLines.join('\n'))
|
|
374
|
+
dLines = repairedLines
|
|
375
|
+
for (const e of repairEntries) repairs.push(`design「${e.heading}」问题行曾被改写——已按锚回填原文(agent 作答保留):${e.q}`)
|
|
376
|
+
} catch {
|
|
377
|
+
for (const e of repairEntries) violations.push(`design「${e.heading}」问题文本被改写——原文:${e.q}(机器回填写盘失败,请手工恢复此行,答案写在下方)`)
|
|
378
|
+
}
|
|
379
|
+
}
|
|
318
380
|
for (const sec of anchorSections) {
|
|
319
381
|
if (!dLines.some((l) => l.trim() === `## ${sec.heading}`)) {
|
|
320
|
-
violations.push(`design「${sec.heading}
|
|
382
|
+
violations.push(`design「${sec.heading}」节被删(标题缺失)——恢复小节与四问原文后作答;问题原文:${sec.lines.join('/')}`)
|
|
321
383
|
continue
|
|
322
384
|
}
|
|
323
385
|
for (const q of sec.lines) {
|
|
324
|
-
if (!dLines.some((l) => l.trim() === q)) violations.push(`design「${sec.heading}
|
|
386
|
+
if (!dLines.some((l) => l.trim() === q)) violations.push(`design「${sec.heading}」问题文本被改写——原文:${q}(问题钉死,答案写在下方)`)
|
|
325
387
|
}
|
|
326
388
|
// 实质作答:节内非空行 - 标题行 - 问题行 - 引导行(> 开头)≥1
|
|
327
389
|
const start = dLines.findIndex((l) => l.trim() === `## ${sec.heading}`)
|
|
@@ -332,7 +394,7 @@ export function verifyThinDocsV2({ changeDir, ledger, skipDesign = false }) {
|
|
|
332
394
|
const t = l.trim()
|
|
333
395
|
return t && !qSet.has(t) && !t.startsWith('>') && !t.startsWith('##')
|
|
334
396
|
})
|
|
335
|
-
if (!answered) violations.push(`design「${sec.heading}
|
|
397
|
+
if (!answered) violations.push(`design「${sec.heading}」未作答——每节至少一行(小改动可写「不适用:<理由>」);问题原文:${sec.lines.join('/')}`)
|
|
336
398
|
}
|
|
337
399
|
}
|
|
338
400
|
}
|
|
@@ -399,7 +461,7 @@ export function verifyThinDocsV2({ changeDir, ledger, skipDesign = false }) {
|
|
|
399
461
|
const inFr = rText !== null && rText.includes(c)
|
|
400
462
|
if (!inFr) advisories.push(`成功标准第 ${i + 1} 条原文未在 requirements 见到(FR 标题锚漂移嫌疑——若属有意改写请核对语义未失真):${c.slice(0, 40)}${c.length > 40 ? '…' : ''}`)
|
|
401
463
|
}
|
|
402
|
-
return { violations, advisories }
|
|
464
|
+
return { violations, advisories, repairs }
|
|
403
465
|
}
|
|
404
466
|
|
|
405
467
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// flow-state.yaml 单一源读取(2026-10-13-thin-sync-stage:自 flow.js 上移的叶子模块)。
|
|
2
|
+
// 依赖仅 node:fs / node:path / js-yaml——供 progress.js(同步投影)与 flow.js(协议主
|
|
3
|
+
// 实现,re-export 保持 `import('../flow.js')` 既有引用面)共享;叶子化让 progress.js
|
|
4
|
+
// 不必为读一个状态文件拉入 flow 协议整条依赖链(评审 P3:非循环依赖问题,flow.js 静态
|
|
5
|
+
// import 闭包实测不达 progress.js)。
|
|
6
|
+
import { readFileSync } from 'node:fs'
|
|
7
|
+
import { join } from 'node:path'
|
|
8
|
+
import yaml from 'js-yaml'
|
|
9
|
+
|
|
10
|
+
export const FLOW_STATE_FILE = 'flow-state.yaml'
|
|
11
|
+
|
|
12
|
+
/** 读 flow-state(缺文件/损坏 → null = 未参与 thin)。 */
|
|
13
|
+
export function readFlowState(changeDir) {
|
|
14
|
+
try {
|
|
15
|
+
const raw = readFileSync(join(changeDir, FLOW_STATE_FILE), 'utf8')
|
|
16
|
+
const obj = yaml.load(raw)
|
|
17
|
+
return obj && typeof obj === 'object' ? obj : null
|
|
18
|
+
} catch {
|
|
19
|
+
return null
|
|
20
|
+
}
|
|
21
|
+
}
|
package/src/flow.js
CHANGED
|
@@ -33,21 +33,15 @@ import { writeAtomicSync } from './fs-atomic.js'
|
|
|
33
33
|
import { resolveRuntimeRoot, triggerSync, assertDatedChangeName } from './run/shared.js'
|
|
34
34
|
import { detectUiTouch, buildUiGuidanceLines, runUiVisualProbe, readUiVisualGate, UI_EVIDENCE_FILENAME } from './ui-visual.js'
|
|
35
35
|
import { runHunkAttributionGate, readHunkGate, renderHunkAttributionLines } from './hunk-attribution.js'
|
|
36
|
+
// 2026-10-13-thin-sync-stage:FLOW_STATE_FILE/readFlowState 单一源上移叶子模块 flow-state.js
|
|
37
|
+
// (progress.js 同步投影也要读;叶子化避免 progress.js 为读一个状态文件拉入本协议整条
|
|
38
|
+
// 依赖链——评审 P3 勘误:非静态循环依赖,本文件 import 闭包实测不达 progress.js);
|
|
39
|
+
// re-export 保持 `import('../flow.js')`(run/command.js 既有动态引用)导出面不变。
|
|
40
|
+
import { FLOW_STATE_FILE, readFlowState } from './flow-state.js'
|
|
41
|
+
export { FLOW_STATE_FILE, readFlowState }
|
|
36
42
|
|
|
37
|
-
const FLOW_STATE_FILE = 'flow-state.yaml'
|
|
38
43
|
const SUBSTEPS = ['artifacts', 'ledger', 'patch', 'review', 'probes', 'distill', 'archive', 'events']
|
|
39
44
|
|
|
40
|
-
/** 读 flow-state(缺文件/损坏 → null = 未参与 thin)。 */
|
|
41
|
-
export function readFlowState(changeDir) {
|
|
42
|
-
try {
|
|
43
|
-
const raw = readFileSync(join(changeDir, FLOW_STATE_FILE), 'utf8')
|
|
44
|
-
const obj = yaml.load(raw)
|
|
45
|
-
return obj && typeof obj === 'object' ? obj : null
|
|
46
|
-
} catch {
|
|
47
|
-
return null
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
45
|
/** 原子写 flow-state(patch 合并,幂等)。 */
|
|
52
46
|
export function writeFlowState(changeDir, patch) {
|
|
53
47
|
const cur = readFlowState(changeDir) || {}
|
|
@@ -536,6 +530,7 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
|
|
|
536
530
|
`requirements 测试绑定槽(收编追加)每条 FR 至少一行作答;写码前后顺手填。`,
|
|
537
531
|
`✅ 任务面归你(工作分解契约):tasks.md 是你的工作队列(机器种子可增删改,保持 \`- [ ] task-NN:\` 行形态)——CLI 以 checkbox 为进度状态机(task tick/flow status 实时读它),TodoWrite 类工具不替代;`,
|
|
538
532
|
` 勾选是收口证据面(逐格核提交 token/review.json):做一件 → 勾一格(task tick 即时回显进度与下一任务)→ 继续下一条,勿攒一把勾;`,
|
|
533
|
+
` 任务拟法:收口/推送/发布类动作不单列 task(协议自身承担,成功标准照写 requirements);确需单列的,勾选前先有消息预埋 task-NN token 的交付提交(回执类证据写进提交消息)。`,
|
|
539
534
|
` flow status --change ${change} 为自愿查看/恢复面(非协议必需,D-007 中间零必需交互;恢复时给下一任务指针与进度)。`,
|
|
540
535
|
`⚠️ 交付纪律:收口前交付代码显式 pathspec 提交——冻结件范围=baseline..HEAD,未提交不进审计件。`,
|
|
541
536
|
``,
|
|
@@ -773,6 +768,53 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
|
|
|
773
768
|
try {
|
|
774
769
|
digest = await flowKnowledgeDigest({ specBase, change, changeDir, input, filesOverride: await extractRoutingInputPaths(cwd, specBase, input) })
|
|
775
770
|
} catch { /* 注入 best-effort 不阻断 start */ }
|
|
771
|
+
// 跨仓清单写法提示(2026-10-13-thin-empty-face-crosscheck):注册仓非空时提示段头契约——
|
|
772
|
+
// thin 通道此前零跨仓指引(fire-ledger-form-strict 实证:契约只活在厚流程模板里,裸写仓目录
|
|
773
|
+
// 前缀撞空面即静默丢对账)。注册为空 → 零新增输出(横幅逐字节不变)。
|
|
774
|
+
let crossRepoHintLines = []
|
|
775
|
+
try {
|
|
776
|
+
const { loadRegisteredRepoKeys } = await import('./scope-audit.js')
|
|
777
|
+
const _repoKeys = loadRegisteredRepoKeys(specBase)
|
|
778
|
+
if (_repoKeys && _repoKeys.length > 0) crossRepoHintLines = [
|
|
779
|
+
`🌐 跨仓交付:清单跨仓文件另用段头「## <注册key> 仓变更」段内写仓根相对路径(或子段「### <注册key>」/条目前缀 cross-repo:<注册key>:)——key 以 local.yaml repos 注册为准(本仓注册 ${_repoKeys.length} 个);`,
|
|
780
|
+
` 裸写「仓目录名/路径」前缀不触发跨仓对账与冻结(收口空面门将点名阻断)。`,
|
|
781
|
+
``,
|
|
782
|
+
]
|
|
783
|
+
} catch { /* 注册仓读取失败 → 零提示(与无注册仓同形态) */ }
|
|
784
|
+
// FR 域预判(2026-10-13-batchtick-evidence-domain-hint):--input 提及路径 × 模块图(discoverModuleIndex
|
|
785
|
+
// 读 docs/<project>/modules/_module-map.yaml——与 distill 域路由同一匹配面)。第一次知道「路径未登记」
|
|
786
|
+
// 不该是收口模块门(最贵时点)。纯提示不设门:input 路径可能不全,路由真相以实际交付文件为准;
|
|
787
|
+
// 绿地无图/零路径 → 零新增输出(与模块门「绿地无图不拦」语义对齐)。fr-index.js 零编辑——
|
|
788
|
+
// resolveTouchedDomains(内部即 pseudoDomainFromPaths+词典守卫)复用 distill 同一伪域回退链。
|
|
789
|
+
let domainHintLines = []
|
|
790
|
+
try {
|
|
791
|
+
const _routingFace = await extractRoutingInputPaths(cwd, specBase, input)
|
|
792
|
+
if (_routingFace.length > 0) {
|
|
793
|
+
const { discoverModuleIndex } = await import('./decision-distill.js')
|
|
794
|
+
const { matchedModuleIds, resolveTouchedDomains } = await import('./fr-index.js')
|
|
795
|
+
const _knowledgeRoot = join(specBase, 'knowledge')
|
|
796
|
+
const _moduleIndex = discoverModuleIndex(_knowledgeRoot)
|
|
797
|
+
if (_moduleIndex && Object.keys(_moduleIndex).length > 0) {
|
|
798
|
+
const _hitRows = []
|
|
799
|
+
const _missFiles = []
|
|
800
|
+
for (const f of _routingFace) {
|
|
801
|
+
const ids = matchedModuleIds(_moduleIndex, f)
|
|
802
|
+
if (ids.length > 0) _hitRows.push({ f, ids })
|
|
803
|
+
else _missFiles.push(f)
|
|
804
|
+
}
|
|
805
|
+
if (_hitRows.length > 0 || _missFiles.length > 0) {
|
|
806
|
+
const _lines = ['🗺️ FR 域预判(按 --input 提及路径;最终以收口实际交付文件路由为准):']
|
|
807
|
+
for (const r of _hitRows) _lines.push(` ✓ ${r.f} → fr/${r.ids.join('、')}`)
|
|
808
|
+
if (_missFiles.length > 0) {
|
|
809
|
+
const _pseudo = (resolveTouchedDomains(changeDir, _moduleIndex, _missFiles, _knowledgeRoot)[0]) || 'unmapped'
|
|
810
|
+
_lines.push(` ⚠️ ${_missFiles.slice(0, 5).join('、')}${_missFiles.length > 5 ? ' 等' : ''} → 未命中知识模块图,FR 蒸馏将落域 ${_pseudo}(按 distill 同一伪域回退链预判——未吸附为 auto-<段>,词典吸附命中既有真域)`)
|
|
811
|
+
_lines.push(' 建议现在登记模块条目(docs/<project>/modules/_module-map.yaml 的 modules.<id>.paths 覆盖该目录)——登记后蒸馏自动落真域(收口模块门仍会兜底拦未登记目录)')
|
|
812
|
+
}
|
|
813
|
+
domainHintLines = _lines
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
} catch { /* 域预判 best-effort:图/路径读取失败零输出 */ }
|
|
776
818
|
const lines = [
|
|
777
819
|
`🏃 flow start(${thick ? 'thick 厚档(--thick 显式声明,人声明不做启发式)' : 'thin 轻量跑道'}): ${change}`,
|
|
778
820
|
`══════════════════════════════════════`,
|
|
@@ -795,6 +837,7 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
|
|
|
795
837
|
` 文档内零指纹标记、零槽注释、零书写指导(规则以本横幅与命令卡为唯一源);成功标准漂移只出 advisory 不拒收。`,
|
|
796
838
|
`⚠️ design.md 四节动码前后顺手作答;列改动文件用独立「## 文件变更清单」节+表格(| 操作 | 路径 | 说明 |)`,
|
|
797
839
|
` ——勿写在「接口契约」节内(收口按节名解析声明面)。`,
|
|
840
|
+
...crossRepoHintLines,
|
|
798
841
|
`📜 requirements 每条 FR = 标题锚 + 行为句 + 按需场景块(#### 场景:名 + Given/When/Then);`,
|
|
799
842
|
` 测试绑定节每条 FR 一行 \`FR-NN: test/路径「用例」\`(无测试面写「不适用:理由」)。`,
|
|
800
843
|
`📦 冻结面在 flow done 时点采集(baseline..HEAD 提交面):未提交交付文件——会话专属 worktree 自动并入;`,
|
|
@@ -812,6 +855,9 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
|
|
|
812
855
|
` (sillyspec task tick --change ${change} --task task-NN 即时回显进度 N/M 与下一任务指针,flow status 同源;或直接 Edit 翻格 \`- [ ]\`→\`- [x]\`),watcher 实时上平台。`,
|
|
813
856
|
` ⚠️ harness 的 TodoWrite 类工具是会话内便利面,不替代 tasks.md——平台进度/收口证据只读它(0/12 事故实证:todo 全 completed 而 tasks.md 0 勾);`,
|
|
814
857
|
` 勾选是收口证据面:每格勾选要有对应提交(消息带 task-NN——逐个列出或 /、,连写如 task-01/02/03,「task-01..task-05」区间写法不认)或 review.json,全勾但零证据会被拒收;勿攒一把勾(收口硬门拒单拍多格);`,
|
|
858
|
+
// 任务拟法指引(2026-10-12-closeout-task-guidance,三次「哨兵抓收口型任务零证据」复盘):
|
|
859
|
+
// 成功标准≠工作分解——收口/推送/发布是协议自身承担的动作;单列则证据(token 提交)必须先于勾选存在。
|
|
860
|
+
` 任务拟法:收口/推送/发布类动作不单列 task(提交、归档、推送是 flow start→done 协议自身承担的动作——成功标准照写 requirements,勿逐字搬进任务清单);确需单列的收口/操作型任务,勾选前先做消息预埋 task-NN token 的交付提交——回执类证据(push/publish 回执、构建日志路径)写进提交消息,勿依赖勾选后补;`,
|
|
815
861
|
` flow status --change ${change} 为自愿查看/恢复面(恢复时给下一任务指针与进度)。`,
|
|
816
862
|
``,
|
|
817
863
|
`🛑 三断点纪律(可控性要求——用户没说「全跑完」就必须在每个断点向用户汇报并等确认):`,
|
|
@@ -838,6 +884,7 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
|
|
|
838
884
|
...materials.map((p) => ` - ${p}`),
|
|
839
885
|
``,
|
|
840
886
|
...digest.lines,
|
|
887
|
+
...(domainHintLines.length > 0 ? ['', ...domainHintLines] : []),
|
|
841
888
|
]
|
|
842
889
|
if (json) {
|
|
843
890
|
console.log(JSON.stringify({ change, tier: thick ? 'thick' : 'thin', baseline, materials, knowledgeDigest: digest.summary }))
|
|
@@ -900,7 +947,7 @@ export function remainingSubstepsAtFail({ snapshotSubsteps = {}, doneList = [],
|
|
|
900
947
|
return SUBSTEPS.filter((k) => k !== failed && snapshotSubsteps?.[k] !== 'done' && !doneNow.has(k))
|
|
901
948
|
}
|
|
902
949
|
|
|
903
|
-
export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null, confirmArchive = true, freezeDirty = false, acceptDirtyGap = false, acceptOrphanFace = false, allowBatchTick = false, acceptUnmapped = false }) {
|
|
950
|
+
export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null, confirmArchive = true, freezeDirty = false, acceptDirtyGap = false, acceptOrphanFace = false, allowBatchTick = false, acceptUnmapped = false, acceptEmptyFace = false }) {
|
|
904
951
|
const changeDir = join(specBase, 'changes', change)
|
|
905
952
|
const st = readFlowState(changeDir)
|
|
906
953
|
if (!st) {
|
|
@@ -968,6 +1015,9 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
|
|
|
968
1015
|
const r = verifyFlowDrafts({ changeDir, change, runtimeRoot, skipDesign: adoptedDesign })
|
|
969
1016
|
if (r.applicable && r.schema === 2) {
|
|
970
1017
|
// v2 纯 markdown 轨(2026-10-04-thin-docs-v2):violations 拒收 → 漂移 advisory → spec 断点机器门
|
|
1018
|
+
// 问题行机器回填留痕(2026-10-13-design-question-autorepair):先于拒收渲染(评审 P3 清偿:
|
|
1019
|
+
// 混合拒收场景回填已落盘,若渲染在 exit(1) 之后则留痕不可达且幂等重跑后永久丢失)。
|
|
1020
|
+
for (const p of r.repairs || []) console.log(`🔧 [design 问题行机器回填] ${p}`)
|
|
971
1021
|
if (r.violations.length > 0) {
|
|
972
1022
|
console.error(`❌ 工件校验拒收(v2 文档↔锚对比):`)
|
|
973
1023
|
for (const v of r.violations) console.error(` - ${v}`)
|
|
@@ -1041,6 +1091,7 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
|
|
|
1041
1091
|
// 哨兵断言(2026-09-25-sentinel-wiring):tasks.md 全勾但零完成证据(区间提交消息标题与正文
|
|
1042
1092
|
// 均无 task-NN token 且无对应 review.json)→ 拒收——L0 硬门接线,两道收口同一哨兵(quick 侧同判)。
|
|
1043
1093
|
let _sentinelNonMirrorTasks = null // 镜像豁免面外的任务数(null=哨兵未跑;0=纯镜像任务面——节奏 advisory 静默)
|
|
1094
|
+
let _sentinelEvidenceComplete = false // 哨兵证据完备(status=complete——每格已勾任务均有 token/review.json):单拍降级判据(2026-10-13-batchtick-evidence-domain-hint)
|
|
1044
1095
|
let _autopilotTicked = 0 // 机器代勾格数(>0 时勾选节奏门降 advisory——代勾是单拍多格机械写,非 agent 纪律面)
|
|
1045
1096
|
// 证据面=整条提交消息(2026-09-25-thin-done-gate-calibration 坑2:%s 只取标题行,正文里的
|
|
1046
1097
|
// token 被判零证据,与文案「提交带 task-NN」口径漂移)——%B%x1e 按提交切记录,advisory 的
|
|
@@ -1107,6 +1158,7 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
|
|
|
1107
1158
|
}
|
|
1108
1159
|
_sentinelNonMirrorTasks = sent.claimTotal
|
|
1109
1160
|
if (sent.status === 'complete') {
|
|
1161
|
+
_sentinelEvidenceComplete = true
|
|
1110
1162
|
console.log(`🛡️ 哨兵:全勾 ${sent.checked}/${sent.claimTotal} 证据齐(提交 token/review.json)`)
|
|
1111
1163
|
// 勾选时点判定(2026-09-26-tick-loop-nudge,R19 行为发现):tasks.md 的 git 首次提交
|
|
1112
1164
|
// == 收口窗口最后提交(一把勾模式)→ 放行但打行为提醒(不阻断——token 证据已验,
|
|
@@ -1156,16 +1208,24 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
|
|
|
1156
1208
|
// 单源(resolveBatchTickAction)四态与豁免优先序见其 JSDoc——代勾须解释整跳才降级
|
|
1157
1209
|
const _persistedAuto = Number(st?.autopilot_ticked) || 0
|
|
1158
1210
|
const _autoN = Math.max(_autopilotTicked, _persistedAuto)
|
|
1159
|
-
const v = resolveBatchTickAction({ batchTick: batch, nonMirrorCount: _sentinelNonMirrorTasks, allowBatchTick, autopilotTicked: _autoN })
|
|
1211
|
+
const v = resolveBatchTickAction({ batchTick: batch, nonMirrorCount: _sentinelNonMirrorTasks, allowBatchTick, autopilotTicked: _autoN, evidenceComplete: _sentinelEvidenceComplete })
|
|
1160
1212
|
const at = Number.isFinite(batch.ts) ? new Date(batch.ts).toLocaleTimeString() : '未知时刻'
|
|
1161
1213
|
if (v.action === 'bypass') {
|
|
1162
1214
|
try { writeFlowState(changeDir, { allow_batch_tick: true }) } catch { /* 留痕 best-effort */ }
|
|
1163
1215
|
console.warn(`⚠️ --allow-batch-tick:单拍多格勾选(${batch.detail},${at})硬门显式旁路——留痕 flow-state 与平台时间线`)
|
|
1164
1216
|
} else if (v.action === 'advisory') {
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1217
|
+
// 证据完备降级(2026-10-13-batchtick-evidence-domain-hint):同形态 git 时点检查(一把勾
|
|
1218
|
+
// 模式)因 token 证据齐放行——事件流门对齐该语义;损害只剩 per-task 时间戳信号面,advisory
|
|
1219
|
+
// + flow-state 留痕与损害对齐。证据缺失形态不走此态(哨兵先拒 / 决策函数维持 reject)。
|
|
1220
|
+
if (v.reason === 'evidence-complete') {
|
|
1221
|
+
try { writeFlowState(changeDir, { batch_ticked: true }) } catch { /* 留痕 best-effort */ }
|
|
1222
|
+
console.warn(`⚠️ 勾选节奏:单拍多格勾选(${batch.detail},${at})——哨兵证据完备(每格已勾任务均有 token 提交/review.json),降级提醒不拒(batch_ticked 已留痕;下次完成单元即勾)`)
|
|
1223
|
+
} else {
|
|
1224
|
+
const why = v.reason === 'autopilot-ticked'
|
|
1225
|
+
? `机器代勾 ${_autoN} 格(governance-autopilot 单拍机械写,非 agent 纪律面;本拍 ${_autopilotTicked}/持久化 ${_persistedAuto})`
|
|
1226
|
+
: '哨兵任务面未知(fail-open 防误拒)'
|
|
1227
|
+
console.warn(`⚠️ 勾选节奏:单拍多格勾选(${batch.detail},${at})——${why},降级提醒不拒`)
|
|
1228
|
+
}
|
|
1169
1229
|
} else if (v.action === 'reject') {
|
|
1170
1230
|
console.error(`🚫 单拍勾选拒收:事件流观测到一拍勾选 ${batch.detail}(${at})——勾选纪律要求逐格(做一件→勾一格→下一个),勾选任务 ${_sentinelNonMirrorTasks} 格`)
|
|
1171
1231
|
console.error(' 出口:①节奏违例已既成——认知后重跑带 --allow-batch-tick 显式留痕过门(平台时间线可见旁路);②疑观测误判→sillyspec doctor 核对事件流')
|
|
@@ -1521,6 +1581,76 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
|
|
|
1521
1581
|
}
|
|
1522
1582
|
} catch { /* 守卫异常不阻断(fail-safe 不新造阻断面) */ }
|
|
1523
1583
|
}
|
|
1584
|
+
// ── 声明面自证前置(2026-10-13-thin-empty-face-crosscheck):planEntries 解析与跨仓防呆
|
|
1585
|
+
// 告警提到 ownFiles 判空之前——空面也要跑。fire-ledger-form-strict 实证(multi-agent-platform
|
|
1586
|
+
// docs/sillyspec/thin-done-empty-face-no-declared-crosscheck.md 37c7d146b):跨仓交付撞空面时
|
|
1587
|
+
// 本段整挂在非空 else 分支内从未执行,归档件永久缺 patch 且零告警;空面恰是高危形态。
|
|
1588
|
+
let planEntries = []
|
|
1589
|
+
try {
|
|
1590
|
+
const { parseFileChangeListDetailed } = await import('./change-list.js')
|
|
1591
|
+
// repoKeys(2026-10-10-cross-repo-patch-freeze D-002@v1):注册仓清单传入——跨仓
|
|
1592
|
+
// 子段/cross-repo: 前缀条目才带 repo 标注(不传则跨仓声明全落主仓口径, thin 跨仓
|
|
1593
|
+
// 对账无从谈起)
|
|
1594
|
+
const { loadRegisteredRepoKeys } = await import('./scope-audit.js')
|
|
1595
|
+
planEntries = parseFileChangeListDetailed(join(changeDir, 'design.md'), { keepSillyspecDocs: true, repoKeys: loadRegisteredRepoKeys(specBase) })
|
|
1596
|
+
} catch { /* 清单缺失/不可解析 → 实际侧 only 快照行(无 verdict,与主链路降级同语义) */ }
|
|
1597
|
+
// 跨仓声明防呆(坑 design-filelist-crossrepo-prefix-silent-main):嫌疑判定从「首段命中
|
|
1598
|
+
// 注册仓」放宽为「声明路径主仓不存在即点名」(仓根相对路径形态首段不命中是盲区);
|
|
1599
|
+
// 首段命中注册仓的条目额外标注 key,点名+三种正确语法指引。
|
|
1600
|
+
const _declared = []
|
|
1601
|
+
for (const e of planEntries) {
|
|
1602
|
+
const pe = String((e && e.path) || '').split('\\').join('/')
|
|
1603
|
+
if (!pe || pe.startsWith('.sillyspec/')) continue
|
|
1604
|
+
if (e.repo) continue
|
|
1605
|
+
_declared.push({ path: pe })
|
|
1606
|
+
}
|
|
1607
|
+
let _repoAliases = null
|
|
1608
|
+
try {
|
|
1609
|
+
const _yamlText = existsSync(join(specBase, 'local.yaml')) ? readFileSync(join(specBase, 'local.yaml'), 'utf8') : ''
|
|
1610
|
+
if (_yamlText) {
|
|
1611
|
+
const { parseRepoRegistry } = await import('./stages/plan-postcheck.js')
|
|
1612
|
+
const _repos = parseRepoRegistry(_yamlText)
|
|
1613
|
+
if (_repos && _repos.size > 0) {
|
|
1614
|
+
_repoAliases = new Map()
|
|
1615
|
+
for (const [k, v] of _repos) {
|
|
1616
|
+
_repoAliases.set(k, k)
|
|
1617
|
+
const norm = String(v || '').split('\\').join('/')
|
|
1618
|
+
const bn = norm.replace(/\/+$/, '').split('/').pop()
|
|
1619
|
+
if (bn) _repoAliases.set(bn, k)
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
} catch { /* 注册表读取失败 → 别名标注缺席,存在性判定不受影响 */ }
|
|
1624
|
+
const _suspects = []
|
|
1625
|
+
for (const d of _declared) {
|
|
1626
|
+
if (existsSync(join(cwd, d.path))) continue
|
|
1627
|
+
_suspects.push({ path: d.path, key: (_repoAliases && _repoAliases.get(d.path.split('/')[0])) || null })
|
|
1628
|
+
}
|
|
1629
|
+
if (_suspects.length > 0) {
|
|
1630
|
+
console.warn(`⚠️ 疑似跨仓声明未按规范标注(${_suspects.length} 条已按主仓路径对账):${_suspects.slice(0, 5).map((x) => `${x.path}(主仓不存在${x.key ? `,首段命中注册仓 ${x.key}` : ''})`).join('、')}${_suspects.length > 5 ? ' 等' : ''}`)
|
|
1631
|
+
console.warn(' 主仓无这些路径 → 冻结面将标「计划未动」、跨仓对账与 patch 冻结不触发。正确写法三选一:① 段头「## <注册key> 仓变更」段内写仓根相对路径;② 子段「### <注册key>」;③ 条目前缀 cross-repo:<注册key>:<仓根相对路径>——key 以 local.yaml repos 注册为准')
|
|
1632
|
+
}
|
|
1633
|
+
// ── 空面门:空冻结面 + 声明清单有未标注交付条目 → 阻断(三处置一次给全;fire-ledger-form-strict 形态补位)──
|
|
1634
|
+
if (ownFiles.length === 0 && _declared.length > 0 && !acceptEmptyFace && !st.accept_empty_face) {
|
|
1635
|
+
console.error(`❌ 冻结面为空但 design 声明清单有 ${_declared.length} 条未标注交付条目(实现不在冻结窗口——跨仓交付撞空面的实证形态)——收口阻断(不归档;处置后重跑同一条命令从断点续):`)
|
|
1636
|
+
for (const x of _suspects.slice(0, 5)) console.error(` - ${x.path}${x.key ? `(首段命中注册仓 ${x.key})` : '(主仓不存在)'}`)
|
|
1637
|
+
console.error(' ① 实现在主仓:先提交后重跑')
|
|
1638
|
+
console.error(' ② 跨仓交付:清单按规范标注后重跑——正确写法三选一:段头「## <注册key> 仓变更」段内写仓根相对路径;子段「### <注册key>」;条目前缀 cross-repo:<注册key>:<仓根相对路径>(key 以 local.yaml repos 注册为准)')
|
|
1639
|
+
console.error(` ③ 纯文档变更/清单误列:同步裁剪 design 清单后重跑;或显式签收:flow done --change ${change} --accept-empty-face(留痕 flow-state,重跑免重拦)`)
|
|
1640
|
+
reportMidFail('patch')
|
|
1641
|
+
process.exit(1)
|
|
1642
|
+
}
|
|
1643
|
+
if (ownFiles.length === 0 && _declared.length > 0 && (acceptEmptyFace || st.accept_empty_face)) {
|
|
1644
|
+
if (acceptEmptyFace && !st.accept_empty_face) {
|
|
1645
|
+
writeFlowState(changeDir, { accept_empty_face: { at: new Date().toISOString(), declaredCount: _declared.length, by: process.env.SILLYSPEC_SESSION_ID || null } })
|
|
1646
|
+
st.accept_empty_face = true
|
|
1647
|
+
}
|
|
1648
|
+
console.warn(`⚠️ --accept-empty-face:显式签收空冻结面与声明面矛盾(${_declared.length} 条声明交付不进冻结面,留痕 flow-state accept_empty_face)`)
|
|
1649
|
+
}
|
|
1650
|
+
// 纯跨仓规范标注空面:合规书写不拦,advisory 点名 thin 空面分支不做跨仓冻结的已知缺口
|
|
1651
|
+
if (ownFiles.length === 0 && _declared.length === 0 && planEntries.some((e) => e && e.repo)) {
|
|
1652
|
+
console.warn('⚠️ 声明清单全部为跨仓规范标注且主仓冻结面为空——thin 空面分支不做跨仓冻结与 patch 留档(缺口已知:归档件将无 patch 两件;如需冻结请主仓侧交付或待后续版本支持空面跨仓冻结)')
|
|
1653
|
+
}
|
|
1524
1654
|
if (ownFiles.length === 0) {
|
|
1525
1655
|
console.log('📦 patch 留档跳过:本变更可归属文件面为空(无交付面改动——治理工件不入冻结面;若实现代码应在此,先提交后重跑)')
|
|
1526
1656
|
patchOk = true
|
|
@@ -1586,15 +1716,7 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
|
|
|
1586
1716
|
// 收口留痕单套写(2026-10-09-close-trace-single-set):共用 writeCloseTraceArtifacts
|
|
1587
1717
|
// 只写 change.patch + change-patch.json 两件——对账面数据并入 JSON 的 scopeAudit
|
|
1588
1718
|
// 子对象(thin 通道同样有对账面),旧名 scope-audit.* 停写、读侧兼容兜存量归档。
|
|
1589
|
-
|
|
1590
|
-
try {
|
|
1591
|
-
const { parseFileChangeListDetailed } = await import('./change-list.js')
|
|
1592
|
-
// repoKeys(2026-10-10-cross-repo-patch-freeze D-002@v1):注册仓清单传入——跨仓
|
|
1593
|
-
// 子段/cross-repo: 前缀条目才带 repo 标注(不传则跨仓声明全落主仓口径, thin 跨仓
|
|
1594
|
-
// 对账无从谈起)
|
|
1595
|
-
const { loadRegisteredRepoKeys } = await import('./scope-audit.js')
|
|
1596
|
-
planEntries = parseFileChangeListDetailed(join(changeDir, 'design.md'), { keepSillyspecDocs: true, repoKeys: loadRegisteredRepoKeys(specBase) })
|
|
1597
|
-
} catch { /* 清单缺失/不可解析 → 实际侧 only 快照行(无 verdict,与主链路降级同语义) */ }
|
|
1719
|
+
// planEntries 解析与跨仓防呆告警已前移至 ownFiles 判空之前(2026-10-13-thin-empty-face-crosscheck)。
|
|
1598
1720
|
// 跨仓声明在场 → 共享集成层分仓对账 + patch 冻结(D-001/D-002@v1):与 heavy 通道
|
|
1599
1721
|
// (execute --done)同源 reconcileCrossRepoPlan,repos[](锚点/计数/patch 正文)
|
|
1600
1722
|
// 随 snapObj 进 change-patch.json;任何异常 fail-soft 退 ⊘ 补行现行为(不阻断收口)
|
|
@@ -2380,7 +2502,7 @@ export async function cmdFlow(args, cwd, specDir = null, opts = {}) {
|
|
|
2380
2502
|
console.log('🔄 --refreeze:patch 子步标记已重置,本次 done 将重新冻结(change.patch 按 baseline..HEAD 最新面重建)')
|
|
2381
2503
|
}
|
|
2382
2504
|
}
|
|
2383
|
-
|
|
2505
|
+
return cmdFlowDone({ change, cwd, specBase, runtimeRootOpt, freezeDirty: hasFlag('--freeze-dirty'), acceptDirtyGap: hasFlag('--accept-dirty-gap'), acceptOrphanFace: hasFlag('--accept-orphan-face'), allowBatchTick: hasFlag('--allow-batch-tick'), acceptUnmapped: hasFlag('--accept-unmapped'), acceptEmptyFace: hasFlag('--accept-empty-face') })
|
|
2384
2506
|
}
|
|
2385
2507
|
if (sub === 'amend-draft') {
|
|
2386
2508
|
// 机器稿唯一留痕修改通道(R7 切片三 / FR-08):重锚哈希 + ledger amendment 审计;
|
package/src/fr-index.js
CHANGED
|
@@ -594,8 +594,17 @@ export function indexRequirements({ changeDir, knowledgeRoot, headHash = '', del
|
|
|
594
594
|
// 落域机械改进(2026-09-27-knowledge-digest):伪域告警附交付路径推导的建议域——
|
|
595
595
|
// 归档时转达人工 1 秒确认(建议器与 digest 信号卡共用 suggestDomainFromFiles 单源)
|
|
596
596
|
const suggested = suggestDomainFromFiles(deliverableFiles || [], { knowledgeRoot })
|
|
597
|
+
// 坑 fr-redomain-advisory-and-selectivity-gap(2026-10-13):告警接专用命令 +
|
|
598
|
+
// --by-change 条目级过滤 + D-001 迁域不换号铁律明示(防手工迁移按序重编号断
|
|
599
|
+
// supersede 链)。锚定域取告警列出的伪域文件名(auto-x.md → auto-x)。
|
|
600
|
+
const pseudoDomains = [...new Set(pseudoHits.map((w) => String(w.file || '').replace(/\.md$/, '')))].filter(Boolean)
|
|
597
601
|
console.warn(`⚠️ [FR 域路由降级] 本变更 ${pseudoHits.length} 条 FR 落伪域/unmapped(${[...new Set(pseudoHits.map((w) => w.file))].join('、')})${suggested ? `——按交付路径建议域:${suggested}(确认后可迁移;` : '('}补模块卡(docs/<项目>/modules/_module-map.yaml 登记该目录)后,新变更将自动落回真域;绿地仓可跑 sillyspec run scan 校准)`)
|
|
598
|
-
if (suggested)
|
|
602
|
+
if (suggested) {
|
|
603
|
+
console.warn(` 域级整体迁移:sillyspec tests --redomain --from <伪域> --to ${suggested} --write(干跑去 --write);只迁本变更条目:加 --by-change <本变更名>`)
|
|
604
|
+
if (pseudoDomains.length > 0) console.warn(` 例:sillyspec tests --redomain --from ${pseudoDomains[0]} --to ${suggested} --by-change <本变更名> --write`)
|
|
605
|
+
console.warn(` ⚠️ 迁域不换号(D-001 铁律):条目 ID 保持原样(绑定/supersede 链按 ID 寻址)——手工搬移时勿按目标域接续重编号`)
|
|
606
|
+
console.warn(` 迁移/存量清单:sillyspec knowledge digest(伪域信号卡)`)
|
|
607
|
+
}
|
|
599
608
|
}
|
|
600
609
|
const unm = all.get('unmapped')
|
|
601
610
|
if (unm && unm.sections.length > 50) {
|
package/src/index.js
CHANGED
|
@@ -4044,6 +4044,53 @@ SillySpec worktree — git worktree 隔离管理
|
|
|
4044
4044
|
}
|
|
4045
4045
|
break;
|
|
4046
4046
|
}
|
|
4047
|
+
case 'deps': {
|
|
4048
|
+
// deps 状态显式覆写(坑 deps-gate-offline-fragile,2026-10-12 实证:离线环境
|
|
4049
|
+
// npm ci 必败 + lockfile 本地改动致链接快路径失效,agent 被迫手改 meta.json 绕过
|
|
4050
|
+
// ——未定义行为且零留痕。本命令把绕过通道 CLI 化:--set 显式值 + --reason 强制
|
|
4051
|
+
// 留痕(写 depsOverrideNote / depsOverrideAt),审计面可对账。合法值收敛工具
|
|
4052
|
+
// 认知集(n/a/linked/installed/failed)。
|
|
4053
|
+
const setIdx = filteredArgs.indexOf('--set')
|
|
4054
|
+
const setVal = setIdx !== -1 && filteredArgs[setIdx + 1] && !String(filteredArgs[setIdx + 1]).startsWith('--') ? String(filteredArgs[setIdx + 1]).trim() : null
|
|
4055
|
+
const rIdx = filteredArgs.indexOf('--reason')
|
|
4056
|
+
const reason = rIdx !== -1 && filteredArgs[rIdx + 1] ? String(filteredArgs[rIdx + 1]).trim() : null
|
|
4057
|
+
if (!wtName || !setVal || !reason) {
|
|
4058
|
+
console.error('用法: sillyspec worktree deps --change <变更名> --set <n/a|linked|installed|failed> --reason "<事实依据——必填留痕>"')
|
|
4059
|
+
console.error(' 离线/内网环境供给链失败时的显式覆写通道(写 meta.json + 覆写留痕);先确认 node_modules 确实可用再 set linked')
|
|
4060
|
+
process.exit(2)
|
|
4061
|
+
}
|
|
4062
|
+
if (!['n/a', 'linked', 'installed', 'failed'].includes(setVal)) {
|
|
4063
|
+
console.error(`❌ --set 值非法 "${setVal}"(合法:n/a | linked | installed | failed)`)
|
|
4064
|
+
process.exit(2)
|
|
4065
|
+
}
|
|
4066
|
+
try {
|
|
4067
|
+
const specBaseWd = resolvePlatformSpecDir(dir, specDir)
|
|
4068
|
+
const wtBase = join(specBaseWd, '.runtime', 'worktrees')
|
|
4069
|
+
const wtDir = join(wtBase, wtName)
|
|
4070
|
+
const metaPath = join(wtDir, 'meta.json')
|
|
4071
|
+
// 跨仓 worktree:主 specBase 下无实体时扫各注册仓(简化:本命令聚焦主仓 worktree,
|
|
4072
|
+
// 跨仓路径用 --spec-dir 指到对应 specBase;两形态 meta.json 同款)
|
|
4073
|
+
if (!existsSync(metaPath)) {
|
|
4074
|
+
console.error(`❌ 找不到 worktree meta:${metaPath}`)
|
|
4075
|
+
console.error(' 跨仓 worktree 用 --spec-dir 指向其 specBase 后重跑;变更名核对 sillyspec worktree list')
|
|
4076
|
+
process.exit(1)
|
|
4077
|
+
}
|
|
4078
|
+
const meta = JSON.parse(String(readFileSync(metaPath, 'utf8')).replace(/^\uFEFF/, ''))
|
|
4079
|
+
const prev = meta.depsStatus
|
|
4080
|
+
meta.depsStatus = setVal
|
|
4081
|
+
meta.depsOverrideNote = `手工覆写: ${reason}(前值 ${prev ?? 'null'})`
|
|
4082
|
+
meta.depsOverrideAt = new Date().toISOString()
|
|
4083
|
+
const { writeAtomicSync } = await import('./fs-atomic.js')
|
|
4084
|
+
writeAtomicSync(metaPath, JSON.stringify(meta, null, 2) + '\n')
|
|
4085
|
+
console.log(`✅ deps 状态已覆写:${wtName} depsStatus ${prev ?? 'null'} → ${setVal}`)
|
|
4086
|
+
console.log(` 留痕:${meta.depsOverrideNote}`)
|
|
4087
|
+
console.log(' verify/archive 对账时以 depsOverrideNote 为审计锚——事实依据不实属未定义行为')
|
|
4088
|
+
} catch (e) {
|
|
4089
|
+
console.error(`❌ 覆写失败: ${e.message}`)
|
|
4090
|
+
process.exit(1)
|
|
4091
|
+
}
|
|
4092
|
+
break;
|
|
4093
|
+
}
|
|
4047
4094
|
default:
|
|
4048
4095
|
console.error(`❌ 未知子命令: worktree ${wtSubCmd}`);
|
|
4049
4096
|
console.log(' 运行 sillyspec worktree --help 查看帮助');
|
package/src/progress.js
CHANGED
|
@@ -23,6 +23,10 @@ import { StepStore } from './progress/step-store.js';
|
|
|
23
23
|
import { StageMachine } from './progress/stage-machine.js';
|
|
24
24
|
import { stageRegistry } from './stages/index.js';
|
|
25
25
|
import { STAGE_ORDER, MAIN_FLOW_ORDER, VALID_STAGES, STAGE_LABELS, SPEC_DIR_NAME, CURRENT_VERSION, emptyStage } from './progress/shared.js';
|
|
26
|
+
// 2026-10-13-thin-sync-stage:叶子模块(无内部依赖),同步投影读 thin 出身用——
|
|
27
|
+
// 叶子化避免本文件为读一个状态文件拉入 flow 协议整条依赖链(评审 P3 勘误:
|
|
28
|
+
// flow.js 静态 import 闭包实测不达本文件,非循环依赖问题)。
|
|
29
|
+
import { readFlowState } from './flow-state.js';
|
|
26
30
|
// resolveSpecDir 单一真相源在 src/run/shared.js(含 home 拒绝守卫,坑 cwd-correction-home-collision
|
|
27
31
|
// 根治:home 下 .sillyspec 恒不命中,防 smoke/临时目录污染自我延续)。此处 re-export 保持
|
|
28
32
|
// 既有 import 路径兼容;run/shared.js 对 progress.js 只有动态 import,无静态循环。
|
|
@@ -600,6 +604,18 @@ export class ProgressManager {
|
|
|
600
604
|
// → 上行恒无标题,平台只能落 change_key(「英文 key」病灶的同步链一侧)。
|
|
601
605
|
title: changeRow.title ?? null,
|
|
602
606
|
};
|
|
607
|
+
// thin 出身上行投影(2026-10-13-thin-sync-stage):tier=thin 且行 status=active 时
|
|
608
|
+
// current_stage 上报 'thin'——平台 StageEnum.THIN / 列表「轻量」徽章与筛选 / ingest
|
|
609
|
+
// 双守卫均按该值消费;本地行出生恒 'brainstorm'(thin 不走阶段机),照抄上行会让
|
|
610
|
+
// 平台读侧 is_thin 判定三分支全不命中(实证:进行中轻量变更无徽章)。仅覆写载荷,
|
|
611
|
+
// 本地 DB 行不动;归档(status=archived,行已翻 'archive')与升厚(tier=thick)不
|
|
612
|
+
// 覆写;flow-state 缺失/损坏 readFlowState→null 按行值上行(fail-open 现状行为)。
|
|
613
|
+
if (
|
|
614
|
+
changeRow.status === 'active' &&
|
|
615
|
+
readFlowState(this._changePath(cwd, changeName))?.tier === 'thin'
|
|
616
|
+
) {
|
|
617
|
+
changeProj.current_stage = 'thin';
|
|
618
|
+
}
|
|
603
619
|
|
|
604
620
|
// 2. project 全局单行(只投影稳定字段 name/schema_version;created_at/updated_at 是本地库元数据,
|
|
605
621
|
// 同步无意义且破坏 round-trip 跨库等值——与 changes 排除 created_at 同理,B2)
|
|
@@ -168,21 +168,27 @@ export function detectBatchCheckCadence(events) {
|
|
|
168
168
|
/**
|
|
169
169
|
* 单拍勾选门决策(2026-09-29-batch-tick-gate,纯函数):检出单拍跳(batchTick 非 null)
|
|
170
170
|
* 时的收口动作裁决。镜像豁免分支退役(2026-10-07-thin-tasks-v3——任务面统一为 agent
|
|
171
|
-
*
|
|
171
|
+
* 工作分解,无镜像面)。五态(决策顺序即豁免优先序):
|
|
172
172
|
* 'silent' 无单拍跳;
|
|
173
173
|
* 'bypass' --allow-batch-tick 显式旁路——留痕放行(同意门先例);
|
|
174
|
-
* 'advisory'
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
174
|
+
* 'advisory' 三路:哨兵面未知(fail-open 防误拒);哨兵证据完备(status=complete——
|
|
175
|
+
* 2026-10-13-batchtick-evidence-domain-hint:同形态 git 时点检查「一把勾模式」
|
|
176
|
+
* 因 token 证据齐放行,事件流门对齐该语义——证据是提交哈希锚定的客观面,
|
|
177
|
+
* 损害只剩 per-task 时间戳信号面;证据缺失形态不走此态,维持 reject);机器代勾
|
|
178
|
+
* 可解释整跳(autopilotTicked >= 跳幅——governance-autopilot 代勾是单拍多格机械写,
|
|
179
|
+
* 非 agent 纪律面;代勾数不可解释最大跳时落到 reject——防「留一格给代勾补、自身
|
|
180
|
+
* 大跳蹭豁免」的对抗绕过,二轮评审 P3-E);
|
|
181
|
+
* 'reject' 非旁路、证据不完备、代勾解释不了整跳——agent 一把勾,拒收。
|
|
182
|
+
* @param {{batchTick:object|null, nonMirrorCount?:number|null, allowBatchTick?:boolean, autopilotTicked?:number, evidenceComplete?:boolean}} args
|
|
179
183
|
* nonMirrorCount 保留为哨兵面未知信号(null → fail-open advisory);镜像-only 语义已退役。
|
|
184
|
+
* evidenceComplete 缺省 false(additive——既有调用方零感知)。
|
|
180
185
|
* @returns {{action:'silent'|'advisory'|'bypass'|'reject', reason:string}}
|
|
181
186
|
*/
|
|
182
|
-
export function resolveBatchTickAction({ batchTick, nonMirrorCount = null, allowBatchTick = false, autopilotTicked = 0 } = {}) {
|
|
187
|
+
export function resolveBatchTickAction({ batchTick, nonMirrorCount = null, allowBatchTick = false, autopilotTicked = 0, evidenceComplete = false } = {}) {
|
|
183
188
|
if (!batchTick) return { action: 'silent', reason: 'no-batch-tick' };
|
|
184
189
|
if (allowBatchTick === true) return { action: 'bypass', reason: 'flag' };
|
|
185
190
|
if (nonMirrorCount === null) return { action: 'advisory', reason: 'sentinel-unknown' };
|
|
191
|
+
if (evidenceComplete === true) return { action: 'advisory', reason: 'evidence-complete' };
|
|
186
192
|
if (autopilotTicked >= batchTick.to - batchTick.from) return { action: 'advisory', reason: 'autopilot-ticked' };
|
|
187
193
|
return { action: 'reject', reason: 'agent-batch-tick' };
|
|
188
194
|
}
|
package/src/stage-review.js
CHANGED
|
@@ -138,6 +138,9 @@ export function renderReviewJsonContract({ stage, changeDir, reviewRunId, tier,
|
|
|
138
138
|
'## review.json 产物契约(CLI Stage Review Gate 将硬校验,以下为精确 schema —— 事前给的 == 事后查的)',
|
|
139
139
|
'',
|
|
140
140
|
'> 路径:`{SPEC_ROOT}/.runtime/stage-reviews/' + stageLbl + '-' + runIdLbl + '/review.json`(本次 reviewType=' + reviewType + ',主审查文档=' + mainDocRel + ')',
|
|
141
|
+
...(changeDir
|
|
142
|
+
? ['> **本次完整绝对路径(直接复制给审查子代理,勿手拼 runId/漏阶段前缀——坑 stage-review-path-prefix 2026-10-10 实证:marker 只有裸 runId,子代理按 runId 直写漏「<stage>-」前缀即错位)**: `' + join(dirname(dirname(changeDir)), '.runtime', 'stage-reviews', stageLbl + '-' + runIdLbl, 'review.json') + '`']
|
|
143
|
+
: []),
|
|
141
144
|
'',
|
|
142
145
|
'### 必填字段(validateStageReviewSchema 硬校验,缺任一即 schema 失败阻断)',
|
|
143
146
|
'- `schemaVersion`: ' + REVIEW_SCHEMA_VERSION,
|
package/src/stages/knowledge.js
CHANGED
|
@@ -596,6 +596,28 @@ async function tryImportSubcommandImpl(importPath, moduleName) {
|
|
|
596
596
|
* @param {object} opts - { specDir }
|
|
597
597
|
*/
|
|
598
598
|
export async function cmdKnowledge(args, dir, opts = {}) {
|
|
599
|
+
// --help/-h 全局拦截(坑:探索型调用 knowledge refresh --help 被当默认参数直接执行
|
|
600
|
+
// ——cmdRefresh 无参即全量重写 generated/,副作用写 34 文件。帮助请求在任何子命令
|
|
601
|
+
// 前消化,危险面子命令不再被误触发)。
|
|
602
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
603
|
+
const lines = [
|
|
604
|
+
'用法: sillyspec knowledge <子命令> [参数]',
|
|
605
|
+
'',
|
|
606
|
+
'子命令:',
|
|
607
|
+
' search "<关键词>" 检索知识库(命中自动注入 prompt 的同源面)',
|
|
608
|
+
' inspect <文件> 查看单个知识文件条目结构',
|
|
609
|
+
' validate 校验知识库完整性(INDEX 锚点/注册/幽灵条目——纯读)',
|
|
610
|
+
' digest 治理信号卡(rot/伪域/坏绑定——纯读)',
|
|
611
|
+
' stats 覆盖率/死条目统计(纯读)',
|
|
612
|
+
' refresh 重扫 scan 文档重写 generated/ 区(**写盘**——先 validate 再用)',
|
|
613
|
+
' propose / classify / inbox / graph / stats',
|
|
614
|
+
'',
|
|
615
|
+
'写盘面子命令(refresh/propose/classify)先跑同名只读命令确认现状;探索用法先看本帮助。',
|
|
616
|
+
]
|
|
617
|
+
for (const l of lines) console.log(l)
|
|
618
|
+
output(true, { help: true, subcommands: ['search', 'inspect', 'validate', 'digest', 'refresh', 'propose', 'classify', 'inbox', 'graph', 'stats'] })
|
|
619
|
+
return
|
|
620
|
+
}
|
|
599
621
|
const subCommand = args[0] || ''
|
|
600
622
|
|
|
601
623
|
switch (subCommand) {
|
|
@@ -17,7 +17,7 @@ const readFileSync = (filePath, encoding) => _readFileSync(filePath, encoding).r
|
|
|
17
17
|
import { join as pJoin, basename } from 'path'
|
|
18
18
|
import { tmpdir } from 'os'
|
|
19
19
|
import jsYaml from 'js-yaml'
|
|
20
|
-
import { parseFileChangeList, pathMatches } from '../change-list.js'
|
|
20
|
+
import { parseFileChangeList, pathMatches, REPO_SECTION_HEADER_RE } from '../change-list.js'
|
|
21
21
|
import { getRule } from '../stage-contract-spec.js'
|
|
22
22
|
import { TASKCARD_PLACEHOLDERS } from '../taskcard-placeholders.js'
|
|
23
23
|
import { validateScriptCommands } from './cmd-existence.js'
|
|
@@ -942,13 +942,10 @@ export function validateTaskCommands(changeDir, projectRoot, modules = null, rep
|
|
|
942
942
|
// design 清单解析与 allowed_paths 对账共用同一套匹配语义,避免两处逻辑漂移。
|
|
943
943
|
|
|
944
944
|
/**
|
|
945
|
-
*
|
|
946
|
-
*
|
|
947
|
-
*
|
|
948
|
-
* 2026-08-29)`### sillyspec 仓变更(跨仓,X1-X4)` 整段判 main、跨仓文件全量误报未覆盖。
|
|
949
|
-
* 不命中(非仓变更段头)→ null。
|
|
945
|
+
* 「<repo> 仓变更」段头识别(D-014):`## <repo> 仓变更`(如 `## sillyspec 仓变更`)。
|
|
946
|
+
* 正则已上移 change-list.js 导出(单一源,2026-10-13-h2-repo-heading-parse——thin 收口
|
|
947
|
+
* 对账解析器同步消费该段头形态),此处 import 使用;容忍语义见 change-list.js 处注释。
|
|
950
948
|
*/
|
|
951
|
-
const REPO_SECTION_HEADER_RE = /^#{2,3}\s+(?:\d+[.)]\s*)?([A-Za-z0-9_.\-]+)\s*仓变更(?:\s*[((][^()()]*[))])?\s*[::]?\s*$/
|
|
952
949
|
|
|
953
950
|
/**
|
|
954
951
|
* 疑似段头宽匹配:标题行含「<token> 仓变更」字样(token 与仓变更间有空白)。
|
|
@@ -1566,6 +1563,44 @@ export function validateTaskcardsCli({ changeDir, projectRoot = null }) {
|
|
|
1566
1563
|
errors.push(`${id}: target_files 形态非法——${bad.map(b => `${b.raw}(${b.invalid})`).join(';')}`)
|
|
1567
1564
|
}
|
|
1568
1565
|
}
|
|
1566
|
+
// provides/expects_from 结构校验(坑 taskcard-validate-vs-postcheck-gap,2026-10-10
|
|
1567
|
+
// 实证:散文行 `- contract Foo fields [a, b]` 是合法 YAML 字符串数组,--validate 过、
|
|
1568
|
+
// 契约解析器静默过滤(无 contract 键即丢)→ plan --done 才报「provider 未声明」。
|
|
1569
|
+
// 在填卡自检轮暴露:字符串元素/缺 contract/fields·needs 非数组 = 形态错,给出映射式模板。
|
|
1570
|
+
const id2 = (content.match(/^id:\s*(.+)/m)?.[1] || '').trim() || f
|
|
1571
|
+
const fm = fmParse.fm || {}
|
|
1572
|
+
const _tpl = '映射式:- contract: <名> / fields: [a, b](expects_from 同构,needs 替 fields)'
|
|
1573
|
+
if (Array.isArray(fm.provides)) {
|
|
1574
|
+
for (const pe of fm.provides) {
|
|
1575
|
+
const bad = typeof pe === 'string'
|
|
1576
|
+
|| !pe || typeof pe !== 'object'
|
|
1577
|
+
|| typeof pe.contract !== 'string' || !pe.contract.trim()
|
|
1578
|
+
|| (pe.fields !== undefined && !Array.isArray(pe.fields))
|
|
1579
|
+
if (bad) {
|
|
1580
|
+
errors.push(`${id2}: provides 条目形态非法(${typeof pe === 'string' ? `散文行「${pe.slice(0, 40)}」` : '缺 contract 键或 fields 非数组'})——${_tpl}`)
|
|
1581
|
+
break
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
if (fm.expects_from && typeof fm.expects_from === 'object' && !Array.isArray(fm.expects_from)) {
|
|
1586
|
+
for (const [prov, contracts] of Object.entries(fm.expects_from)) {
|
|
1587
|
+
if (!Array.isArray(contracts)) {
|
|
1588
|
+
errors.push(`${id2}: expects_from.${prov} 应为契约数组(映射式:- contract: <名>
|
|
1589
|
+
needs: [a, b])`)
|
|
1590
|
+
continue
|
|
1591
|
+
}
|
|
1592
|
+
for (const ce of contracts) {
|
|
1593
|
+
const bad = typeof ce === 'string'
|
|
1594
|
+
|| !ce || typeof ce !== 'object'
|
|
1595
|
+
|| typeof ce.contract !== 'string' || !ce.contract.trim()
|
|
1596
|
+
|| (ce.needs !== undefined && !Array.isArray(ce.needs))
|
|
1597
|
+
if (bad) {
|
|
1598
|
+
errors.push(`${id2}: expects_from.${prov} 条目形态非法(${typeof ce === 'string' ? `散文行「${ce.slice(0, 40)}」` : '缺 contract 键或 needs 非数组'})——${_tpl}`)
|
|
1599
|
+
break
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1569
1604
|
}
|
|
1570
1605
|
}
|
|
1571
1606
|
} catch {
|