sillyspec 3.28.3 → 3.28.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/sillyspec-auto/SKILL.md +54 -54
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -114
- package/.claude/skills/sillyspec-continue/SKILL.md +17 -17
- package/.claude/skills/sillyspec-execute/SKILL.md +222 -222
- package/.claude/skills/sillyspec-knowledge/SKILL.md +40 -40
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.claude/skills/sillyspec-propose/SKILL.md +16 -16
- package/.claude/skills/sillyspec-resume/SKILL.md +21 -21
- package/.claude/skills/sillyspec-state/SKILL.md +25 -25
- package/.claude/skills/sillyspec-workspace/SKILL.md +33 -33
- package/AGENTS.md +25 -0
- package/INSTRUCTIONS.md +12 -0
- package/package.json +1 -1
- package/results-review-tier.md +18 -0
- package/src/agent-session-log.js +4 -3
- package/src/archive-delta.js +82 -0
- package/src/change-delete.js +2 -1
- package/src/change-list.js +18 -3
- package/src/change-risk-profile.js +77 -6
- package/src/commit-suggest.js +52 -1
- package/src/config-cat.js +17 -1
- package/src/constants.js +3 -1
- package/src/decision-distill.js +45 -5
- package/src/decisions-io.js +96 -0
- package/src/design-facts.js +3 -0
- package/src/dispatch/probe.js +31 -8
- package/src/docs-check.js +102 -3
- package/src/doctor-diagnostics.js +441 -5
- package/src/foreign-declared.js +53 -2
- package/src/index.js +606 -22
- package/src/mcp-server.js +144 -0
- package/src/module-impact.js +84 -12
- package/src/modules.js +46 -34
- package/src/plan-adopt-waves.js +13 -5
- package/src/progress/change-registry.js +34 -0
- package/src/progress.js +75 -2
- package/src/quick-recommend.js +6 -1
- package/src/quicklog.js +70 -8
- package/src/review-dispatch.js +377 -0
- package/src/run/archive-distill.js +48 -0
- package/src/run/command.js +48 -13
- package/src/run/complete-handlers.js +215 -9
- package/src/run/complete.js +184 -2
- package/src/run/gate-snapshot.js +88 -0
- package/src/run/gates.js +206 -58
- package/src/run/prompt.js +150 -14
- package/src/run/quick-audit.js +38 -13
- package/src/run/shared.js +183 -12
- package/src/run/stage.js +52 -2
- package/src/run/verify-quality-scan.js +336 -0
- package/src/runtime-hygiene.js +84 -0
- package/src/scope-audit.js +735 -0
- package/src/sillyhub-mcp/client.js +58 -3
- package/src/spec-sync.js +63 -8
- package/src/stage-contract.js +83 -3
- package/src/stage-review-checklist.js +44 -0
- package/src/stage-review.js +125 -4
- package/src/stages/archive.js +60 -167
- package/src/stages/brainstorm.js +7 -17
- package/src/stages/doctor.js +41 -515
- package/src/stages/execute.js +13 -10
- package/src/stages/plan-postcheck.js +191 -7
- package/src/stages/plan.js +7 -35
- package/src/stages/quick.js +11 -11
- package/src/stages/verify.js +24 -39
- package/src/sync-noise.js +158 -0
- package/src/sync.js +93 -27
- package/src/task-review.js +133 -17
- package/src/taskcard.js +46 -3
- package/src/validate-artifacts.js +163 -0
- package/src/verify-facts-schema.js +141 -0
- package/src/verify-postcheck.js +347 -29
- package/src/verify-probes.js +275 -24
- package/src/workflow.js +6 -0
- package/src/worktree-apply.js +167 -24
- package/src/worktree.js +5 -1
- package/templates/agents-instruction.md +1 -0
package/src/verify-probes.js
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* 探针2(关键词提取半语义)/探针3.4 集成盲区/3.5 断言抽查/探针4(决策追踪语义)留 agent。
|
|
12
12
|
*
|
|
13
13
|
* verify-result.md 骨架:七章节固定结构 + 探针结果机械预填 + 其余章节 <!--TODO--> 占位。
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* 结论走「结论枚举:」固定槽行(刀③)——占位符不含枚举词,槽未填 extractVerifyConclusionSlot
|
|
15
|
+
* 返回 '' 即判不过,骨架不能直接过门(与 symbol-impact 骨架同款防偷懒语义)。
|
|
16
16
|
* P3b 增量:①章节标题行末尾 claims 层标注(可复跑探针/确定性检查/人工判断,纯后缀不新增行);
|
|
17
17
|
* ②--init 同步落盘 verify-facts.json 机器底稿(探针命令行 + 首跑关键指标 + 时间戳,CLI 全权写,
|
|
18
18
|
* 供事后独立复跑审计;重复 --init 覆盖为最近一次 init 快照)。
|
|
@@ -20,7 +20,11 @@
|
|
|
20
20
|
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'fs'
|
|
21
21
|
import { join, dirname, basename } from 'path'
|
|
22
22
|
import { gitQuiet } from './git-helper.js'
|
|
23
|
+
import {
|
|
24
|
+
FACTS_SCHEMA_VERSION, EVIDENCE_SLOT_HEADING, RECEIPT_SLOT_HEADING, parseEvidenceSlots,
|
|
25
|
+
} from './verify-facts-schema.js'
|
|
23
26
|
import { parseFileChangeListDetailed } from './change-list.js'
|
|
27
|
+
import { parseDecisions } from './decision-distill.js'
|
|
24
28
|
import { parseAllowedPaths } from './stages/plan-postcheck.js'
|
|
25
29
|
import { verifyApiParity, _readWorktreeMeta } from './contract-matrix.js'
|
|
26
30
|
import { splitOwnVsForeignDiffFiles } from './foreign-declared.js'
|
|
@@ -149,16 +153,18 @@ export function runVerifyProbes({ cwd, changeName, specDir = null }) {
|
|
|
149
153
|
const moduleDirs = [...new Set(allowed.map(p => dirname(p.split('\\').join('/'))).filter(d => d && d !== '.'))]
|
|
150
154
|
const testFiles = []
|
|
151
155
|
for (const d of moduleDirs) {
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
160
|
-
for (const
|
|
161
|
-
|
|
156
|
+
// 双根并集扫描(坑 probe3-worktree-test-false-negative,2026-09-10 驾驭小结第五批②,
|
|
157
|
+
// 用户实证 5 条假 warning):apply 前新测试文件只在 worktree(untracked),而模块目录在
|
|
158
|
+
// 主仓**已存在**——旧「主仓目录缺失才回退 worktree」条件不触发,探针 3 报「未找到测试
|
|
159
|
+
// 文件」假阴。改为无条件双根并集(主仓 ∪ worktree,各自存在才扫,路径相对各自根呈现,
|
|
160
|
+
// 按 rel 去重主仓优先——与探针 5 的三根并集同族)。主仓 in-place(wtRoot=null)零变化。
|
|
161
|
+
const roots = []
|
|
162
|
+
if (existsSync(join(cwd, d))) roots.push({ root: join(cwd, d), base: cwd })
|
|
163
|
+
if (wtRoot && wtRoot !== cwd && existsSync(join(wtRoot, d))) roots.push({ root: join(wtRoot, d), base: wtRoot })
|
|
164
|
+
for (const s of roots) {
|
|
165
|
+
for (const f of findTestFiles(s.root, s.base)) {
|
|
166
|
+
if (!testFiles.includes(f)) testFiles.push(f)
|
|
167
|
+
}
|
|
162
168
|
}
|
|
163
169
|
}
|
|
164
170
|
probe3.tasks.push({
|
|
@@ -310,8 +316,11 @@ export function buildVerifyFacts(result, { changeName, now } = {}) {
|
|
|
310
316
|
const p3 = (result && result.probe3) || {}
|
|
311
317
|
const p5 = (result && result.probe5) || {}
|
|
312
318
|
const p6 = (result && result.probe6) || {}
|
|
319
|
+
// v2(2026-09-08-ir-verify-facts):probes 机器段原样;conclusion/tests/requiredEvidence/
|
|
320
|
+
// runtimeEvidence/factsConsistency 五段是 slot-backfill/实测回填段(D-001@v2),--init 快照
|
|
321
|
+
// 不落键(writeVerifyFacts 分段合并时保留既有固化段),由 backfillFactsFromMdAndTests 填。
|
|
313
322
|
return {
|
|
314
|
-
schemaVersion:
|
|
323
|
+
schemaVersion: 2,
|
|
315
324
|
change: changeName,
|
|
316
325
|
generatedAt: now || new Date().toISOString(),
|
|
317
326
|
probes: {
|
|
@@ -350,37 +359,279 @@ export function buildVerifyFacts(result, { changeName, now } = {}) {
|
|
|
350
359
|
}
|
|
351
360
|
|
|
352
361
|
/**
|
|
353
|
-
*
|
|
354
|
-
*
|
|
355
|
-
*
|
|
362
|
+
* 平台模式产物路径回显注记(2026-09-10 驾驭小结②,用户实锤「显示混乱」):
|
|
363
|
+
* verify-probes --init 在平台模式(.sillyspec-platform.json pointer 存在)下 spec 根解析为
|
|
364
|
+
* hub 镜像目录——回显的物理路径(骨架生成/已存在/facts 刷新)都在镜像根下,而产物经
|
|
365
|
+
* daemon spec-sync 落主仓 .sillyspec/changes/<change>/。agent/人工核对以主仓路径为准,
|
|
366
|
+
* 回显行尾追加本注记消歧义(本地模式返回空串,回显零变化)。
|
|
367
|
+
* @param {string|null} platformBase 平台镜像根(null/undefined = 非平台模式)
|
|
368
|
+
* @param {string} changeName 变更名
|
|
369
|
+
* @param {string} file 产物文件名(verify-result.md / verify-facts.json)
|
|
370
|
+
* @returns {string} 注记文本(非平台模式为 '')
|
|
371
|
+
*/
|
|
372
|
+
export function formatPlatformPathNote(platformBase, changeName, file) {
|
|
373
|
+
if (!platformBase) return ''
|
|
374
|
+
return `(平台模式:物理写盘在 hub 镜像根 ${platformBase};主仓同步位置 .sillyspec/changes/${changeName}/${file},核对以主仓路径为准)`
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* verify-probes --init 落盘 verify-facts.json(CLI 全权写)。v2(2026-09-08-ir-verify-facts)改
|
|
379
|
+
* 分段合并:re-init 刷新机器段(probes/generatedAt),保留既有 slot-backfill/实测固化段
|
|
380
|
+
* (conclusion/tests/requiredEvidence/runtimeEvidence)——旧「无条件覆盖」会抹掉 --done 回填数据
|
|
381
|
+
* (设计审查 P1-5)。agent 勿手改:复跑审计的对照快照,防篡改基准是 verify-result.md 正文。
|
|
356
382
|
* @param {string} changeDir 变更目录(spec 根下 changes/<name>)
|
|
357
383
|
* @param {object} result runVerifyProbes 返回值
|
|
358
384
|
* @param {string} changeName 变更名(统一命令行呈现)
|
|
385
|
+
* @param {{ platformNote?: string }} [opts] platformNote 回显行尾追加(平台模式路径消歧,纯回显层不进落盘)
|
|
359
386
|
* @returns {{ facts: object, path: string }}
|
|
360
387
|
*/
|
|
361
|
-
export function writeVerifyFacts(changeDir, result, changeName) {
|
|
362
|
-
const
|
|
388
|
+
export function writeVerifyFacts(changeDir, result, changeName, opts = {}) {
|
|
389
|
+
const fresh = buildVerifyFacts(result, { changeName })
|
|
363
390
|
const factsPath = join(changeDir, 'verify-facts.json')
|
|
391
|
+
let facts = fresh
|
|
392
|
+
try {
|
|
393
|
+
const prev = JSON.parse(readFileSync(factsPath, 'utf8'))
|
|
394
|
+
if (prev && prev.schemaVersion === FACTS_SCHEMA_VERSION) {
|
|
395
|
+
facts = {
|
|
396
|
+
...fresh,
|
|
397
|
+
conclusion: prev.conclusion,
|
|
398
|
+
tests: prev.tests,
|
|
399
|
+
requiredEvidence: prev.requiredEvidence,
|
|
400
|
+
runtimeEvidence: prev.runtimeEvidence,
|
|
401
|
+
factsConsistency: prev.factsConsistency, // 保留至上次对比结论(下次 --done 重算覆盖)
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
} catch { /* 首次落盘/v1/损坏 → 全新快照(v1 无固化段,直接升 v2) */ }
|
|
364
405
|
writeFileSync(factsPath, JSON.stringify(facts, null, 2) + '\n')
|
|
365
|
-
console.log(`📝 已刷新 verify-facts.json 机器底稿: ${factsPath}(CLI
|
|
406
|
+
console.log(`📝 已刷新 verify-facts.json 机器底稿: ${factsPath}(v2 分段合并,固化段保留;CLI 全权写,勿手改)${opts.platformNote || ''}`)
|
|
366
407
|
return { facts, path: factsPath }
|
|
367
408
|
}
|
|
368
409
|
|
|
410
|
+
/**
|
|
411
|
+
* --done 回填(D-001@v2 slot-backfill):结论枚举槽 + 证据/回执槽解析固化进 facts;
|
|
412
|
+
* testCheckResult 提供时回填 tests 段(实测在 runValidators 之后 → gates 二次回填,FR-03 次序)。
|
|
413
|
+
* 机器段(probes/factsConsistency)不经本函数(writeVerifyFacts / checkProbeConsistency 所有)。
|
|
414
|
+
* fail-soft:facts 缺失时原地升 v2(probes 保留或空);落盘失败 warn 不抛。
|
|
415
|
+
* @param {string} factsPath verify-facts.json 路径
|
|
416
|
+
* @param {{ verifyMd: string, testCheckResult?: object|null }} opts
|
|
417
|
+
* @returns {{ facts: object, conclusion: string|null, evidenceCount: number, receiptCount: number, testsBackfilled: boolean }}
|
|
418
|
+
*/
|
|
419
|
+
export function backfillFactsFromMdAndTests(factsPath, { verifyMd, testCheckResult = null, conclusion = null }) {
|
|
420
|
+
let facts = null
|
|
421
|
+
try { facts = JSON.parse(readFileSync(factsPath, 'utf8')) } catch { /* 缺失见下 */ }
|
|
422
|
+
// 只固化既有底稿,不无中生有(2026-09-08-ir-verify-facts 接线实证):无 facts 的存量变更
|
|
423
|
+
// (未跑过 --init)若被回填凭空建出 facts.json,checkProbeConsistency 的「有 facts 无探针
|
|
424
|
+
// 子节 → error」判别会把存量兼容 skip 误升 error(run-complete-step-verify e2e 抓出)。
|
|
425
|
+
// 底稿创建唯一入口是 verify-probes --init。
|
|
426
|
+
if (!facts) {
|
|
427
|
+
return { facts: null, skipped: true, conclusion: null, evidenceCount: 0, receiptCount: 0, testsBackfilled: false, reason: '无 verify-facts.json(未跑 --init),回填跳过——底稿创建唯一入口是 --init' }
|
|
428
|
+
}
|
|
429
|
+
if (facts.schemaVersion !== FACTS_SCHEMA_VERSION) {
|
|
430
|
+
facts = { ...facts, schemaVersion: FACTS_SCHEMA_VERSION, probes: (facts && facts.probes) || {} }
|
|
431
|
+
}
|
|
432
|
+
const slots = parseEvidenceSlots(verifyMd || '')
|
|
433
|
+
// conclusion 由调用方传 stage-contract.extractVerifyConclusionSlot(verifyMd) 的结果
|
|
434
|
+
// (避免 verify-probes → stage-contract 静态 import 的潜在环,分层单向)
|
|
435
|
+
const conclusionSlot = conclusion || null
|
|
436
|
+
if (conclusionSlot) facts.conclusion = conclusionSlot
|
|
437
|
+
if (slots.hasEvidenceSlot && slots.requiredEvidence.length > 0) {
|
|
438
|
+
facts.requiredEvidence = slots.requiredEvidence.map(e => ({
|
|
439
|
+
task: e.task, status: e.status, verifiedFiles: e.verifiedFiles,
|
|
440
|
+
...(e.exempt ? { exempt: true, exemptionReason: e.exemptionReason } : {}),
|
|
441
|
+
}))
|
|
442
|
+
}
|
|
443
|
+
if (slots.hasReceiptSlot && slots.runtimeEvidence.length > 0) {
|
|
444
|
+
facts.runtimeEvidence = slots.runtimeEvidence
|
|
445
|
+
}
|
|
446
|
+
let testsBackfilled = false
|
|
447
|
+
if (testCheckResult && testCheckResult.status && testCheckResult.status !== 'skipped') {
|
|
448
|
+
facts.tests = {
|
|
449
|
+
command: testCheckResult.command || null,
|
|
450
|
+
exitCode: typeof testCheckResult.exitCode === 'number' ? testCheckResult.exitCode : null,
|
|
451
|
+
strategy: testCheckResult.mode || testCheckResult.strategy || null,
|
|
452
|
+
failedRemaining: testCheckResult.failureNames || testCheckResult.failedTests || [],
|
|
453
|
+
passedAt: new Date().toISOString(),
|
|
454
|
+
}
|
|
455
|
+
testsBackfilled = true
|
|
456
|
+
}
|
|
457
|
+
try {
|
|
458
|
+
writeFileSync(factsPath, JSON.stringify(facts, null, 2) + '\n')
|
|
459
|
+
} catch (e) {
|
|
460
|
+
console.warn(`⚠️ facts 回填落盘失败(fail-soft,不阻断): ${e && e.message ? e.message : e}`)
|
|
461
|
+
}
|
|
462
|
+
return { facts, conclusion: conclusionSlot, evidenceCount: slots.requiredEvidence.length, receiptCount: slots.runtimeEvidence.length, testsBackfilled }
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* md 槽段缺失补齐(--init 段落级补齐,2026-09-08-ir-verify-facts R-02):已存在的
|
|
467
|
+
* verify-result.md 缺「证据账/集成验证回执」槽段时仅追加骨架(不触碰既有正文),幂等二跑零改动。
|
|
468
|
+
* @param {string} mdPath verify-result.md 路径
|
|
469
|
+
* @param {Array<{task:string,evidence:string[]}>} [requiredEvidenceItems] verify-required-evidence.json items(预填 task 行)
|
|
470
|
+
* @returns {{ added: string[] }} 追加的槽段标题列表
|
|
471
|
+
*/
|
|
472
|
+
export function backfillMissingEvidenceSlots(mdPath, requiredEvidenceItems = []) {
|
|
473
|
+
let text = ''
|
|
474
|
+
try { text = readFileSync(mdPath, 'utf8') } catch { return { added: [] } }
|
|
475
|
+
const normalized = text.replace(/\r\n/g, '\n')
|
|
476
|
+
const added = []
|
|
477
|
+
const blocks = []
|
|
478
|
+
if (!normalized.includes(EVIDENCE_SLOT_HEADING)) {
|
|
479
|
+
const lines = [EVIDENCE_SLOT_HEADING, '[层:人工判断——CLI 核验]', '', '<!-- 无 cannot_verify 任务时本节写「无」 -->']
|
|
480
|
+
if (requiredEvidenceItems.length > 0) {
|
|
481
|
+
for (const it of requiredEvidenceItems) {
|
|
482
|
+
lines.push(`- ${it.task}: <待填:三选一> | verifiedFiles: <精确路径,逗号分隔>(satisfied 必填;豁免时填 missing 并加(豁免:<一句话理由>)后缀)`)
|
|
483
|
+
}
|
|
484
|
+
} else {
|
|
485
|
+
lines.push('无(本次变更无 cannot_verify 任务)')
|
|
486
|
+
}
|
|
487
|
+
lines.push('')
|
|
488
|
+
blocks.push(lines.join('\n'))
|
|
489
|
+
added.push(EVIDENCE_SLOT_HEADING)
|
|
490
|
+
}
|
|
491
|
+
if (!normalized.includes(RECEIPT_SLOT_HEADING)) {
|
|
492
|
+
blocks.push([
|
|
493
|
+
RECEIPT_SLOT_HEADING, '[层:自述声明——CLI 一致性校验]', '',
|
|
494
|
+
'<!-- integration-critical/deployment-critical 变更必填;其余写「无」 -->',
|
|
495
|
+
'- claim: <待填:一句话> | command: <待填:命令> | exit: <待填:0 或非 0> | log: <待填:日志路径>',
|
|
496
|
+
'',
|
|
497
|
+
].join('\n'))
|
|
498
|
+
added.push(RECEIPT_SLOT_HEADING)
|
|
499
|
+
}
|
|
500
|
+
if (added.length > 0) {
|
|
501
|
+
writeFileSync(mdPath, normalized.replace(/\n?$/, '\n') + blocks.join('\n') + '\n')
|
|
502
|
+
}
|
|
503
|
+
return { added }
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* 结论草稿槽行预填(P0-1 前置三,noai-ir-roadmap §3):机械事实全绿时把骨架「结论枚举:」
|
|
508
|
+
* 待填槽替换为 PASS 草稿——显式「草稿待确认」内联标注(非默认值语义),agent 复核后可改写;
|
|
509
|
+
* gate 对槽行的独立复核逻辑不经本函数(判定链不动,预填只省打字成本)。纯文本变换三态:
|
|
510
|
+
* 槽行未填(仍含「待填」)→ 改写;已填(任何枚举值)→ 原样返回;无槽行(legacy 存量)→
|
|
511
|
+
* 原样返回。改写后槽行满足 extractVerifyConclusionSlot 行首锚定枚举解析(返回 PASS)。
|
|
512
|
+
*/
|
|
513
|
+
export function applyConclusionDraftToText(text, draft = 'PASS') {
|
|
514
|
+
const normalized = String(text).replace(/\r\n/g, '\n')
|
|
515
|
+
const lines = normalized.split('\n')
|
|
516
|
+
for (let i = 0; i < lines.length; i++) {
|
|
517
|
+
if (/^结论枚举:/.test(lines[i]) && lines[i].includes('待填')) {
|
|
518
|
+
lines[i] = `结论枚举:${draft}(🤖 CLI 草稿待确认——机械事实全绿自动预填:noAI 实测通过 + 探针 1/3/5/6 干净 + 风险门非 integration/deployment;复核后不同意即改写本行枚举值,gate 独立复核不受预填影响)`
|
|
519
|
+
return lines.join('\n')
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return text
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* 决策追踪矩阵机械半边(P0-1,noai-ir-roadmap §3):D→FR→task 链自结构化字段构建——
|
|
527
|
+
* decisions.md 条目(parseDecisions 双格式解析,与 distill 同源单一实现)× tasks/task-NN.md
|
|
528
|
+
* frontmatter 的 decision_ids / requirement_ids(taskcard 9 硬校验字段,CLI 骨架写入——比
|
|
529
|
+
* tasks.md 行内注解可靠)。Evidence / 状态两列是人工判断置 <待填>;D 无 task 回指 →
|
|
530
|
+
* ⚠️ 未闭环显式列出(这正是矩阵要暴露的风险,不静默)。
|
|
531
|
+
* @returns {{ rows: string[], decisionCount: number, taskCount: number }|null}
|
|
532
|
+
* null = 无 decisions.md / 解析 0 条(矩阵段留 TODO 不注入)
|
|
533
|
+
*/
|
|
534
|
+
export function buildDecisionChainMatrix(changeDir) {
|
|
535
|
+
let decisions
|
|
536
|
+
try {
|
|
537
|
+
decisions = parseDecisions(changeDir)
|
|
538
|
+
} catch {
|
|
539
|
+
return null
|
|
540
|
+
}
|
|
541
|
+
if (!decisions || decisions.missing || (decisions.entries || []).length === 0) return null
|
|
542
|
+
// task 卡 frontmatter 结构化字段反查(列表 [a, b] 形态;frontmatter 由 plan-postcheck 硬校验)
|
|
543
|
+
const taskRefs = []
|
|
544
|
+
const tasksDir = join(changeDir, 'tasks')
|
|
545
|
+
try {
|
|
546
|
+
if (existsSync(tasksDir)) {
|
|
547
|
+
for (const f of readdirSync(tasksDir).filter((f) => /^task-\d+\.md$/.test(f)).sort()) {
|
|
548
|
+
const raw = readFileSync(join(tasksDir, f), 'utf8')
|
|
549
|
+
const id = (raw.match(/^id:\s*(\S+)/m) || [])[1] || f.replace(/\.md$/, '')
|
|
550
|
+
const decLine = (raw.match(/^decision_ids:\s*\[([^\]]*)\]/m) || [])[1] || ''
|
|
551
|
+
const reqLine = (raw.match(/^requirement_ids:\s*\[([^\]]*)\]/m) || [])[1] || ''
|
|
552
|
+
taskRefs.push({
|
|
553
|
+
task: id,
|
|
554
|
+
decisions: new Set(decLine.split(',').map((s) => s.trim()).filter(Boolean)),
|
|
555
|
+
reqs: new Set(reqLine.split(',').map((s) => s.trim()).filter(Boolean)),
|
|
556
|
+
})
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
} catch { /* tasks 目录不可读 → 矩阵 task 列全标未闭环(信息仍在) */ }
|
|
560
|
+
const rows = []
|
|
561
|
+
for (const d of decisions.entries) {
|
|
562
|
+
// task 卡 decision_ids 可能写 D-001(无 @vN)——id 与 number 双匹配同源
|
|
563
|
+
const hitTasks = taskRefs.filter((t) => t.decisions.has(d.id) || t.decisions.has(d.number))
|
|
564
|
+
const frs = [...new Set(hitTasks.flatMap((t) => [...t.reqs]))].sort()
|
|
565
|
+
const taskCell = hitTasks.length > 0 ? hitTasks.map((t) => t.task).join('、') : '⚠️ 未闭环(无 task 回指)'
|
|
566
|
+
const frCell = frs.length > 0 ? frs.join('、') : '⚠️ 未映射'
|
|
567
|
+
rows.push(`| ${d.id} | ${frCell} | ${taskCell} | <待填:证据回指> | <待填> |`)
|
|
568
|
+
}
|
|
569
|
+
return { rows, decisionCount: decisions.entries.length, taskCount: taskRefs.length }
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* 矩阵机械半边注入 verify-result.md(--init 接线):只在「决策追踪矩阵」段内仍是骨架
|
|
574
|
+
* TODO 行(含 D-xxx 形态)且段内无既有表格时替换注入——幂等(agent 已写/前次注入零改动),
|
|
575
|
+
* 不触碰正文其余部分。落盘失败 fail-soft 返回 null。
|
|
576
|
+
* @returns {{ decisions: number, tasks: number }|null} null = 无 decisions/无可替换 TODO/已注入
|
|
577
|
+
*/
|
|
578
|
+
export function injectDecisionChainDraft(mdPath, changeDir) {
|
|
579
|
+
let text
|
|
580
|
+
try { text = readFileSync(mdPath, 'utf8') } catch { return null }
|
|
581
|
+
const headingIdx = text.indexOf('## 决策追踪矩阵')
|
|
582
|
+
if (headingIdx === -1) return null
|
|
583
|
+
const nextHeading = text.indexOf('\n## ', headingIdx + 1)
|
|
584
|
+
const sectionEnd = nextHeading === -1 ? text.length : nextHeading
|
|
585
|
+
const section = text.slice(headingIdx, sectionEnd)
|
|
586
|
+
if (/^\|/m.test(section)) return null // 已有表格 → 尊重既有内容
|
|
587
|
+
const todoRe = /<!--TODO:[^\n]*D-xxx[^\n]*-->/
|
|
588
|
+
if (!todoRe.test(section)) return null // 无骨架 TODO(手写正文)→ 不动
|
|
589
|
+
const matrix = buildDecisionChainMatrix(changeDir)
|
|
590
|
+
if (!matrix || matrix.rows.length === 0) return null
|
|
591
|
+
const block = [
|
|
592
|
+
'<!-- 机械半边预填(CLI,P0-1):D→FR→task 链自 decisions.md × tasks/*.md frontmatter 结构化字段构建;',
|
|
593
|
+
' Evidence / 状态两列是人工判断——逐格复核,未闭环行必须在报告标注风险 -->',
|
|
594
|
+
'| 决策 ID | FR | Task | Evidence | 状态 |',
|
|
595
|
+
'|---|---|---|---|---|',
|
|
596
|
+
...matrix.rows,
|
|
597
|
+
].join('\n')
|
|
598
|
+
const next = text.slice(0, headingIdx) + section.replace(todoRe, block) + text.slice(sectionEnd)
|
|
599
|
+
try {
|
|
600
|
+
writeFileSync(mdPath, next)
|
|
601
|
+
} catch (e) {
|
|
602
|
+
console.warn(`⚠️ 决策追踪矩阵预填落盘失败(fail-soft): ${e && e.message ? e.message : e}`)
|
|
603
|
+
return null
|
|
604
|
+
}
|
|
605
|
+
return { decisions: matrix.decisionCount, tasks: matrix.taskCount }
|
|
606
|
+
}
|
|
607
|
+
|
|
369
608
|
/**
|
|
370
609
|
* 生成 verify-result.md 骨架(七章节;探针结果机械预填,语义章节 <!--TODO--> 占位)。
|
|
371
|
-
*
|
|
610
|
+
* 结论走「结论枚举:」固定槽行(刀③,2026-09-08)——占位符不含枚举词(<待填:三选一>),
|
|
611
|
+
* 槽未填 → extractVerifyConclusionSlot 返回 '' → gate 判不过(fail-closed;旧占位符
|
|
612
|
+
* `<待填:PASS 或 FAIL>` 含 PASS 字样会被窗口正则误读成已填 PASS,已修)。
|
|
372
613
|
* 章节标题行末尾带 claims 层标注(P3b):探针结果=可复跑探针 / 测试结果=确定性检查 / 其余=
|
|
373
|
-
*
|
|
614
|
+
* 人工判断——纯渲染层后缀,不新增行;结论解析槽优先于标题关键词窗口(存量兼容)。
|
|
374
615
|
* @returns {string|null} 骨架全文;无 design.md/tasks.md(非完整流程变更)→ null
|
|
375
616
|
*/
|
|
376
617
|
export function generateVerifyResultSkeleton(result) {
|
|
377
618
|
const L = [
|
|
378
619
|
'# 验证报告(骨架由 `sillyspec verify-probes --change <变更名> --init` 生成)',
|
|
379
620
|
'',
|
|
380
|
-
'> 探针结果已机械预填;其余章节把 `<!--TODO-->`
|
|
381
|
-
'>
|
|
621
|
+
'> 探针结果已机械预填;其余章节把 `<!--TODO-->` 替换为真实内容。**结论只认「结论枚举:」槽行**——',
|
|
622
|
+
'> 槽行留「<待填:三选一>」会被 gate 判不过(fail-closed),正文其他位置的 PASS/FAIL 字样不参与判定。',
|
|
623
|
+
'',
|
|
624
|
+
'## 结论 [层:人工判断]',
|
|
625
|
+
'',
|
|
626
|
+
'结论枚举:`<待填:三选一>`(把尖括号占位整体替换为 PASS / PASS WITH NOTES / FAIL 之一;一句话理由写在枚举后同行或下一行)',
|
|
627
|
+
'',
|
|
628
|
+
'## 证据账(cannot_verify 任务) [层:人工判断——CLI 核验]',
|
|
629
|
+
'<!-- 无 cannot_verify 任务时本节写「无」;有则逐 task 一行 -->',
|
|
630
|
+
'- task-NN: <待填:三选一> | verifiedFiles: <精确路径,逗号分隔>(satisfied 必填;豁免时填 missing 并加(豁免:<一句话理由>)后缀)',
|
|
382
631
|
'',
|
|
383
|
-
'##
|
|
632
|
+
'## 集成验证回执 [层:自述声明——CLI 一致性校验]',
|
|
633
|
+
'<!-- integration-critical/deployment-critical 变更必填;其余写「无」 -->',
|
|
634
|
+
'- claim: <待填:一句话> | command: <待填:命令> | exit: <待填:0 或非 0> | log: <待填:日志路径>',
|
|
384
635
|
'',
|
|
385
636
|
'## 任务完成度 [层:人工判断]',
|
|
386
637
|
'<!--TODO: 逐 task 对照 tasks.md 勾选与验收标准,完成/未完成/存疑三态-->',
|
package/src/workflow.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { readFileSync, existsSync, readdirSync, writeFileSync, mkdirSync } from 'fs'
|
|
13
13
|
import { join, resolve, basename } from 'path'
|
|
14
14
|
import jsYaml from 'js-yaml'
|
|
15
|
+
import { pruneTimestampedEntries } from './runtime-hygiene.js'
|
|
15
16
|
import { WORKFLOW_STATUS } from './constants.js'
|
|
16
17
|
import { contentNonEmpty, lineCount, missingSections, placeholderLineMatches } from './check-primitives.js'
|
|
17
18
|
import { collectInvalidDocRefs } from './docs-check.js'
|
|
@@ -626,6 +627,11 @@ export function saveWorkflowRun(result, options = {}) {
|
|
|
626
627
|
|
|
627
628
|
try {
|
|
628
629
|
writeFileSync(filepath, JSON.stringify(record, null, 2), 'utf8')
|
|
630
|
+
// 写入侧滚动裁剪(runtime-hygiene.js,2026-09-08 系统性回收):workflow-runs/ 只写不收,
|
|
631
|
+
// 实证本仓 21 份累积。文件名以零填充时间戳开头,name 字典序 == 时间序。fail-open 静默。
|
|
632
|
+
try {
|
|
633
|
+
pruneTimestampedEntries({ dir: runDir, keep: 30 })
|
|
634
|
+
} catch { /* 卫生动作 fail-open */ }
|
|
629
635
|
return filepath
|
|
630
636
|
} catch (e) {
|
|
631
637
|
console.warn('⚠️ 保存 workflow run 失败:', e.message)
|