sillyspec 3.28.2 → 3.28.3
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 -93
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -114
- package/.claude/skills/sillyspec-continue/SKILL.md +17 -47
- package/.claude/skills/sillyspec-execute/SKILL.md +222 -214
- package/.claude/skills/sillyspec-knowledge/SKILL.md +40 -270
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.claude/skills/sillyspec-propose/SKILL.md +16 -87
- package/.claude/skills/sillyspec-resume/SKILL.md +21 -70
- package/.claude/skills/sillyspec-state/SKILL.md +25 -64
- package/.claude/skills/sillyspec-workspace/SKILL.md +33 -175
- package/package.json +1 -1
- package/src/archive-delta.js +45 -2
- package/src/constants.js +6 -0
- package/src/design-facts.js +64 -0
- package/src/dispatch/backends/sillyhub-mcp.js +17 -5
- package/src/docs-check.js +119 -27
- package/src/docs-migrate.js +124 -0
- package/src/index.js +172 -22
- package/src/progress/change-registry.js +19 -0
- package/src/progress.js +2 -0
- package/src/run/command.js +129 -4
- package/src/run/complete-handlers.js +10 -3
- package/src/run/complete.js +59 -5
- package/src/run/gates.js +22 -2
- package/src/run/progress-confirm.js +66 -0
- package/src/run/prompt.js +139 -31
- package/src/run/quick-audit.js +12 -5
- package/src/run/scan-profile.js +20 -2
- package/src/run/shared.js +10 -5
- package/src/run/stage.js +7 -0
- package/src/scan-postcheck.js +5 -1
- package/src/stages/brainstorm-auto.js +2 -1
- package/src/stages/brainstorm.js +9 -17
- package/src/stages/execute.js +10 -14
- package/src/stages/plan.js +2 -1
- package/src/stages/quick.js +4 -2
- package/src/stages/verify.js +13 -15
- package/src/task-review.js +200 -0
- package/src/verify-postcheck.js +164 -30
- package/templates/agents-instruction.md +1 -1
- package/.claude/skills/verify-per-user/SKILL.md +0 -6
package/src/verify-postcheck.js
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
import { execSync, spawnSync } from 'child_process'
|
|
26
|
+
import { IR_STRICT_SINCE } from './constants.js'
|
|
26
27
|
import { gitQuiet } from './git-helper.js'
|
|
27
28
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'fs'
|
|
28
29
|
import { join } from 'path'
|
|
@@ -40,6 +41,25 @@ import { runVerifyProbes } from './verify-probes.js'
|
|
|
40
41
|
// 测试命令最长执行时间;超时视为失败(防止 CLI 被挂起的测试卡死)
|
|
41
42
|
const TEST_TIMEOUT_MS = Number(process.env.SILLYSPEC_TEST_TIMEOUT_MS) || 10 * 60 * 1000
|
|
42
43
|
const OUTPUT_TAIL_CHARS = 4000
|
|
44
|
+
// 判账失败行台账上限(坑 verify-test-reconcile-tail-blindspot):对账按完整输出判、tail 只存
|
|
45
|
+
// 末 4000 字符 → 失败行落在盲区时只剩 reason 里 5 行×120 字符采样,归因必须全量复跑。修法是
|
|
46
|
+
// 判账行集(remaining/exempted 全量原文)随结果透传落盘/打印;台账只防病态体量——单行截
|
|
47
|
+
// 300 字符、行数截 200,超出注明计数。judge 判账仍用全量行集,不受此截断影响。
|
|
48
|
+
const FAILURE_LEDGER_MAX_LINES = 200
|
|
49
|
+
const FAILURE_LEDGER_MAX_CHARS = 300
|
|
50
|
+
|
|
51
|
+
/** 失败行台账截断(落盘/展示用;判账本身不受影响)。export 供 test 验证截断语义 */
|
|
52
|
+
export function capFailureLedger(lines) {
|
|
53
|
+
if (!Array.isArray(lines) || lines.length === 0) return []
|
|
54
|
+
const out = lines.slice(0, FAILURE_LEDGER_MAX_LINES).map(l => {
|
|
55
|
+
const s = String(l)
|
|
56
|
+
return s.length > FAILURE_LEDGER_MAX_CHARS ? s.slice(0, FAILURE_LEDGER_MAX_CHARS) + '…' : s
|
|
57
|
+
})
|
|
58
|
+
if (lines.length > FAILURE_LEDGER_MAX_LINES) {
|
|
59
|
+
out.push(`…(共 ${lines.length} 行,仅列前 ${FAILURE_LEDGER_MAX_LINES} 行;判账按全量行集不受此截断影响)`)
|
|
60
|
+
}
|
|
61
|
+
return out
|
|
62
|
+
}
|
|
43
63
|
|
|
44
64
|
/**
|
|
45
65
|
* 行尾归一(坑 verify-modules-crlf-blanket-fallback,2026-08-20 实证):Windows 仓的
|
|
@@ -586,24 +606,37 @@ export function extractKnownFailures(yamlText) {
|
|
|
586
606
|
if (inline) {
|
|
587
607
|
return inline[1].split(',').map(s => s.trim().replace(/^['"]|['"]$/g, '')).filter(Boolean)
|
|
588
608
|
}
|
|
589
|
-
// 坑 verify-known-failures-comment-line-truncation(2026-08-28
|
|
590
|
-
//
|
|
591
|
-
//
|
|
592
|
-
//
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
609
|
+
// 坑 verify-known-failures-comment-line-truncation(2026-08-28 连续踩两次)→ 坑
|
|
610
|
+
// verify-known-failures-block-fragile-chain(2026-09-07 工具复盘升级修复):块式捕获曾要求
|
|
611
|
+
// 「- 项行 / 注释行 / 空行」**连续成链**((...)+),任何一行不合形态——手改残留的缩进杂行、
|
|
612
|
+
// 空项 ` -`、误删半截的段残迹——链条即断,其后所有条目**静默丢失**(实证形态:G/H 注释段
|
|
613
|
+
// 踩着 F 段残迹重写,F 段遗留行断链 → G/H 条目全丢 → 清单残缺 → verify 假红,且丢得多静默
|
|
614
|
+
// 就多难归因)。改为逐行扫描:块 = known_failures: 头之后连续的「缩进行 / 注释行(任意缩进,
|
|
615
|
+
// 含列首)/ 空行」,首个列首正文行收块;块内杂行一律容忍,`- ` 项行无论隔着什么都提取——
|
|
616
|
+
// 只有 YAML 本就不合法的形态(列首裸 `- 项`)才不取,不再因断链静默丢项。
|
|
617
|
+
const lines = yaml.split('\n')
|
|
618
|
+
const headIdx = lines.findIndex(l => /^known_failures:\s*(?:#.*)?$/.test(l))
|
|
619
|
+
if (headIdx === -1) return []
|
|
620
|
+
const itemRe = /^[ \t]+-[ \t]+(.+)$/
|
|
621
|
+
const items = []
|
|
622
|
+
for (let i = headIdx + 1; i < lines.length; i++) {
|
|
623
|
+
const l = lines[i]
|
|
624
|
+
if (l.trim() === '') continue // 空行:块内分隔,放行
|
|
625
|
+
if (/^[ \t]*#/.test(l)) continue // 注释行(任意缩进含列首):段头/归因注记,放行
|
|
626
|
+
if (!/^[ \t]/.test(l)) break // 列首正文行:下一个顶层键,收块
|
|
627
|
+
const m = l.match(itemRe) // 其余缩进行一律留在块内;项行才提取(杂行不提取但不断链)
|
|
628
|
+
if (m) items.push(m[1])
|
|
629
|
+
}
|
|
630
|
+
return items
|
|
631
|
+
.map(s => {
|
|
632
|
+
const item = s.trim()
|
|
633
|
+
const full = item.match(/^(['"])(.*)\1$/)
|
|
634
|
+
if (full) return full[2] // 整体引号值原样保留(内部 # 不是注释)
|
|
635
|
+
const stripped = item.replace(/[ \t]+#.*$/, '').trim() // 行尾注释须 # 前有空白(YAML 语义),裸 # 不截
|
|
636
|
+
const q2 = stripped.match(/^(['"])(.*)\1$/)
|
|
637
|
+
return q2 ? q2[2] : stripped
|
|
638
|
+
})
|
|
639
|
+
.filter(Boolean)
|
|
607
640
|
}
|
|
608
641
|
|
|
609
642
|
// 单条失败行标记(跨 pytest/jest/vitest/go/generic)。大小写不敏感。
|
|
@@ -698,11 +731,15 @@ export function partitionFailures(output, knownFailures) {
|
|
|
698
731
|
if (ENV_NOISE_RE.test(bare)) continue
|
|
699
732
|
if (PER_TEST_FAIL_RE.test(bare) && !SUMMARY_LINE_RE.test(bare)) failureLines.push(l)
|
|
700
733
|
}
|
|
701
|
-
|
|
734
|
+
// 豁免匹配同样在剥 ANSI 后的行上做(坑 verify-known-failures-ansi-exemption-split,2026-09-07
|
|
735
|
+
// 工具复盘):TTY 捕获的失败行里色码把可见词拦腰拆开(`× \x1b[31mtests/foo.test.ts\x1b[0m > case`
|
|
736
|
+
// 连不成 `tests/foo.test.ts > case`),同一行被迫拆成两段各配一条模式才能豁免。模式侧同步剥
|
|
737
|
+
// ANSI(从原始输出誊抄来的模式可能自带码);返回仍保留原文(remaining/exempted 展示不变形)。
|
|
738
|
+
const pats = (knownFailures || []).map(p => String(p).replace(ANSI_RE, '').toLowerCase()).filter(Boolean)
|
|
702
739
|
const exempted = []
|
|
703
740
|
const remaining = []
|
|
704
741
|
for (const l of failureLines) {
|
|
705
|
-
const ll = l.toLowerCase()
|
|
742
|
+
const ll = l.replace(ANSI_RE, '').toLowerCase()
|
|
706
743
|
if (pats.some(p => ll.includes(p))) exempted.push(l)
|
|
707
744
|
else remaining.push(l)
|
|
708
745
|
}
|
|
@@ -718,19 +755,28 @@ export function partitionFailures(output, knownFailures) {
|
|
|
718
755
|
* 有未豁免失败行 / 检测不到失败行(保守)→ failed
|
|
719
756
|
* fail-safe:检测不到失败行绝不自动 pass(避免解析盲区导致假 PASS)。
|
|
720
757
|
*
|
|
721
|
-
*
|
|
758
|
+
* 判账行集全量随结果返回(坑 verify-test-reconcile-tail-blindspot):对账按完整输出判,但
|
|
759
|
+
* outputTail 只留末 OUTPUT_TAIL_CHARS 字符——失败行落在盲区时 reason 采样不足以归因,只能
|
|
760
|
+
* 全量复跑。remainingLines/exemptedLines(全量原文行)由调用方透传到 test-result.json 与
|
|
761
|
+
* 控制台明细,盲区归因不再依赖复跑;passed 分支同样带 exemptedLines(豁免披露复核需要)。
|
|
762
|
+
*
|
|
763
|
+
* @returns {{ status: 'passed'|'failed', reason: string|null, exemptedCount: number,
|
|
764
|
+
* remainingLines: string[], exemptedLines: string[] }}
|
|
722
765
|
*/
|
|
723
766
|
export function judgeWithKnownFailures(exitCode, output, baseReason, knownFailures) {
|
|
724
|
-
if (exitCode === 0) return { status: 'passed', reason: baseReason, exemptedCount: 0 }
|
|
767
|
+
if (exitCode === 0) return { status: 'passed', reason: baseReason, exemptedCount: 0, remainingLines: [], exemptedLines: [] }
|
|
768
|
+
const { failureLines, exempted, remaining } = partitionFailures(output, knownFailures || [])
|
|
725
769
|
if (!knownFailures || knownFailures.length === 0) {
|
|
726
|
-
|
|
770
|
+
// 无清单分支同样带行集:豁免与否都不该丢判账依据(tail 盲区归因同权)
|
|
771
|
+
return { status: 'failed', reason: baseReason, exemptedCount: 0, remainingLines: remaining, exemptedLines: [] }
|
|
727
772
|
}
|
|
728
|
-
const { failureLines, exempted, remaining } = partitionFailures(output, knownFailures)
|
|
729
773
|
if (failureLines.length > 0 && remaining.length === 0) {
|
|
730
774
|
return {
|
|
731
775
|
status: 'passed',
|
|
732
776
|
reason: `全部 ${failureLines.length} 个失败行命中 known_failures 已豁免(${exempted.length} 条)— 请人工复核豁免清单是否过宽`,
|
|
733
777
|
exemptedCount: exempted.length,
|
|
778
|
+
remainingLines: [],
|
|
779
|
+
exemptedLines: exempted,
|
|
734
780
|
}
|
|
735
781
|
}
|
|
736
782
|
// 剩余未豁免失败行:列出具体行 + 预存债指引(坑 verify-known-failures-stale-list)。
|
|
@@ -742,7 +788,9 @@ export function judgeWithKnownFailures(exitCode, output, baseReason, knownFailur
|
|
|
742
788
|
const t = String(l).trim()
|
|
743
789
|
return t.length > 120 ? t.slice(0, 120) + '…' : t
|
|
744
790
|
})
|
|
745
|
-
|
|
791
|
+
// 指针改指台账(坑 verify-test-reconcile-tail-blindspot):原「见上方测试输出」只对 tail
|
|
792
|
+
// 窗口内的行成立,盲区行必须指向完整台账(控制台明细段 + test-result.json 的 failure_remaining)
|
|
793
|
+
const more = remaining.length > 5 ? `\n …(其余 ${remaining.length - 5} 行完整清单见下方「未豁免失败行」明细,已全量落盘 test-result.json 的 failure_remaining——不受输出 tail 截断影响)` : ''
|
|
746
794
|
detail = `${remaining.length} 个失败行未命中 known_failures 清单:\n - ${sample.join('\n - ')}${more}\n → 若是预存债(变更前就失败),加入 local.yaml 的 known_failures 清单;若是本次变更引入的真实失败,请修复`
|
|
747
795
|
} else {
|
|
748
796
|
detail = '失败输出未检测到可豁免的失败行(保守判 fail)'
|
|
@@ -751,6 +799,8 @@ export function judgeWithKnownFailures(exitCode, output, baseReason, knownFailur
|
|
|
751
799
|
status: 'failed',
|
|
752
800
|
reason: baseReason ? `${baseReason}(${detail})` : detail,
|
|
753
801
|
exemptedCount: exempted.length,
|
|
802
|
+
remainingLines: remaining,
|
|
803
|
+
exemptedLines: exempted,
|
|
754
804
|
}
|
|
755
805
|
}
|
|
756
806
|
|
|
@@ -814,6 +864,8 @@ function runOneModule(name, testCommand, cwd, knownFailures = []) {
|
|
|
814
864
|
outputTail,
|
|
815
865
|
reason: judged.reason,
|
|
816
866
|
exemptedCount: judged.exemptedCount,
|
|
867
|
+
failureRemaining: judged.remainingLines,
|
|
868
|
+
failureExempted: judged.exemptedLines,
|
|
817
869
|
}
|
|
818
870
|
}
|
|
819
871
|
|
|
@@ -1062,6 +1114,13 @@ export function runVerifyTestCheck({ cwd, specBase, changeName = null, ctx = nul
|
|
|
1062
1114
|
|
|
1063
1115
|
const rawStrategy = extractTestStrategy(yamlText)
|
|
1064
1116
|
const knownFailures = extractKnownFailures(yamlText)
|
|
1117
|
+
if (knownFailures.length > 0) {
|
|
1118
|
+
// 豁免清单加载可见性(坑 verify-known-failures-block-fragile-chain 的观测面):条数即时报出
|
|
1119
|
+
// ——清单被块内残迹截断/误删段时「N 条」与预期不符一眼可见,不必等假红再反推。
|
|
1120
|
+
// 走 stderr(console.warn):本模块运行期叙述统一不碰 stdout(machine-interface 调用时
|
|
1121
|
+
// stdout 留给机器可读输出)
|
|
1122
|
+
console.warn(`📋 known_failures 豁免清单已加载:${knownFailures.length} 条模式(local.yaml)`)
|
|
1123
|
+
}
|
|
1065
1124
|
|
|
1066
1125
|
// —— evidence-auto 生效策略解析(D-005@v2 / task-11)——
|
|
1067
1126
|
// 按变更目录 module-impact.md 影响面取生效策略(行为→module、纯文档/门禁→skip、
|
|
@@ -1304,6 +1363,8 @@ function runCrossRepoFullTest(entry) {
|
|
|
1304
1363
|
mode: 'cross-repo-full',
|
|
1305
1364
|
repoKey: entry.repoKey,
|
|
1306
1365
|
exemptedCount: judged.exemptedCount,
|
|
1366
|
+
failureRemaining: judged.remainingLines,
|
|
1367
|
+
failureExempted: judged.exemptedLines,
|
|
1307
1368
|
}
|
|
1308
1369
|
}
|
|
1309
1370
|
|
|
@@ -1327,6 +1388,9 @@ function mergeResultStatus({ status, mainResult, crossResults, reason }) {
|
|
|
1327
1388
|
resultPath: mainResult.resultPath,
|
|
1328
1389
|
mode: 'cross-repo-merged',
|
|
1329
1390
|
fallbackReason: mainResult.fallbackReason || null,
|
|
1391
|
+
// 判账行集跨仓合并(坑 verify-test-reconcile-tail-blindspot):mergedTail 截断不影响台账
|
|
1392
|
+
failureRemaining: [...(mainResult.failureRemaining || []), ...crossResults.flatMap(r => r.result.failureRemaining || [])],
|
|
1393
|
+
failureExempted: [...(mainResult.failureExempted || []), ...crossResults.flatMap(r => r.result.failureExempted || [])],
|
|
1330
1394
|
}
|
|
1331
1395
|
}
|
|
1332
1396
|
|
|
@@ -1457,6 +1521,8 @@ function runFullCommand({ yamlText, localYamlPath, cwd, specBase, changeName, fa
|
|
|
1457
1521
|
mode: 'full',
|
|
1458
1522
|
fallbackReason,
|
|
1459
1523
|
exemptedCount: judged.exemptedCount,
|
|
1524
|
+
failureRemaining: judged.remainingLines,
|
|
1525
|
+
failureExempted: judged.exemptedLines,
|
|
1460
1526
|
}
|
|
1461
1527
|
|
|
1462
1528
|
writeRunResult({ specBase, changeName, result, extra: fallbackReason ? { fallback_reason: fallbackReason } : {} })
|
|
@@ -1500,6 +1566,10 @@ function runModuleSubset({ cwd, specBase, changeName, hits, knownFailures = [] }
|
|
|
1500
1566
|
mode: 'module-subset',
|
|
1501
1567
|
fallbackReason: null,
|
|
1502
1568
|
exemptedCount: perModule.reduce((n, r) => n + (r.exemptedCount || 0), 0),
|
|
1569
|
+
// 判账行集聚合(坑 verify-test-reconcile-tail-blindspot):合并 tail 会二次截断,行集不截——
|
|
1570
|
+
// 每模块台账另见 extra.modules[].failure_remaining(带模块归属)
|
|
1571
|
+
failureRemaining: perModule.flatMap(r => r.failureRemaining || []),
|
|
1572
|
+
failureExempted: perModule.flatMap(r => r.failureExempted || []),
|
|
1503
1573
|
}
|
|
1504
1574
|
|
|
1505
1575
|
writeRunResult({
|
|
@@ -1515,6 +1585,8 @@ function runModuleSubset({ cwd, specBase, changeName, hits, knownFailures = [] }
|
|
|
1515
1585
|
duration_ms: r.durationMs,
|
|
1516
1586
|
output_tail: r.outputTail,
|
|
1517
1587
|
reason: r.reason,
|
|
1588
|
+
...(r.failureRemaining && r.failureRemaining.length ? { failure_remaining: capFailureLedger(r.failureRemaining) } : {}),
|
|
1589
|
+
...(r.failureExempted && r.failureExempted.length ? { failure_exempted: capFailureLedger(r.failureExempted) } : {}),
|
|
1518
1590
|
})),
|
|
1519
1591
|
},
|
|
1520
1592
|
})
|
|
@@ -1523,9 +1595,9 @@ function runModuleSubset({ cwd, specBase, changeName, hits, knownFailures = [] }
|
|
|
1523
1595
|
|
|
1524
1596
|
/**
|
|
1525
1597
|
* 结果落盘到 .runtime/verify-runs/<ts>/test-result.json(供追溯与 SillyHub 消费)。
|
|
1526
|
-
* 多模块时 extra.modules 描述各模块明细。
|
|
1598
|
+
* 多模块时 extra.modules 描述各模块明细。export 供 test 验证台账落盘形状。
|
|
1527
1599
|
*/
|
|
1528
|
-
function writeRunResult({ specBase, changeName, result, extra = {} }) {
|
|
1600
|
+
export function writeRunResult({ specBase, changeName, result, extra = {} }) {
|
|
1529
1601
|
try {
|
|
1530
1602
|
const ts = new Date().toISOString().slice(0, 19).replace(/[-T:]/g, '')
|
|
1531
1603
|
const runDir = join(specBase, '.runtime', 'verify-runs', ts)
|
|
@@ -1538,6 +1610,11 @@ function writeRunResult({ specBase, changeName, result, extra = {} }) {
|
|
|
1538
1610
|
status: result.status,
|
|
1539
1611
|
duration_ms: result.durationMs,
|
|
1540
1612
|
output_tail: result.outputTail,
|
|
1613
|
+
// 判账失败行台账(坑 verify-test-reconcile-tail-blindspot):按完整输出判账的行集全量
|
|
1614
|
+
// 落盘(受 capFailureLedger 防病态体量截断)——失败行落在 output_tail 盲区时归因读这里,
|
|
1615
|
+
// 不再需要全量复跑。空集不落键(成功/跳过运行不添噪声)。
|
|
1616
|
+
...(result.failureRemaining && result.failureRemaining.length ? { failure_remaining: capFailureLedger(result.failureRemaining) } : {}),
|
|
1617
|
+
...(result.failureExempted && result.failureExempted.length ? { failure_exempted: capFailureLedger(result.failureExempted) } : {}),
|
|
1541
1618
|
reason: result.reason,
|
|
1542
1619
|
ran_at: new Date().toISOString(),
|
|
1543
1620
|
...extra,
|
|
@@ -1565,11 +1642,30 @@ export function printVerifyTestCheck(result) {
|
|
|
1565
1642
|
if (result.exemptedCount > 0) {
|
|
1566
1643
|
console.log(`\n✅ Verify 实测通过(含 ${result.exemptedCount} 个 known_failures 豁免):\`${result.command}\` — ${result.reason}`)
|
|
1567
1644
|
console.warn(' ⚠️ 本次 PASS 依赖 known_failures 豁免清单——请人工复核清单是否过宽(避免误豁免本变更引入的真实失败)。')
|
|
1645
|
+
// 豁免披露明细(坑 verify-test-reconcile-tail-blindspot):被豁免的失败行同样可能落在
|
|
1646
|
+
// tail 盲区——复核"清单是否过宽"不能只看 tail。前 10 条样例进控制台,全量台账在
|
|
1647
|
+
// test-result.json 的 failure_exempted。
|
|
1648
|
+
if (result.failureExempted && result.failureExempted.length) {
|
|
1649
|
+
console.warn(` 已豁免失败行(共 ${result.failureExempted.length} 行,前 10 条;全量见 test-result.json 的 failure_exempted):`)
|
|
1650
|
+
for (const line of capFailureLedger(result.failureExempted).slice(0, 10)) console.warn(` ~ ${line}`)
|
|
1651
|
+
}
|
|
1568
1652
|
} else {
|
|
1569
1653
|
console.log(`\n✅ Verify 实测通过:\`${result.command}\` 退出码 0(${(result.durationMs / 1000).toFixed(1)}s)`)
|
|
1570
1654
|
}
|
|
1571
1655
|
} else {
|
|
1572
1656
|
console.error(`\n❌ Verify 实测失败:\`${result.command}\` — ${result.reason}`)
|
|
1657
|
+
// 未豁免失败行台账(坑 verify-test-reconcile-tail-blindspot):判账按完整输出、下方 tail
|
|
1658
|
+
// 只留末段——失败行落在盲区时 tail 里看不到,归因靠本段全量行集(≤30 条样例进控制台,
|
|
1659
|
+
// 全量落盘 failure_remaining),不再需要全量复跑定位。
|
|
1660
|
+
if (result.failureRemaining && result.failureRemaining.length) {
|
|
1661
|
+
const ledger = capFailureLedger(result.failureRemaining)
|
|
1662
|
+
const shown = ledger.slice(0, 30)
|
|
1663
|
+
console.error(` 未豁免失败行(按完整输出判账,不受 tail 截断影响;共 ${result.failureRemaining.length} 行):`)
|
|
1664
|
+
for (const line of shown) console.error(` ! ${line}`)
|
|
1665
|
+
if (ledger.length > shown.length) {
|
|
1666
|
+
console.error(` ! …(其余 ${ledger.length - shown.length} 行全量见 test-result.json 的 failure_remaining)`)
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1573
1669
|
if (result.outputTail) {
|
|
1574
1670
|
const tail = result.outputTail.split('\n').slice(-20).join('\n')
|
|
1575
1671
|
console.error(' 输出(末尾):')
|
|
@@ -2101,7 +2197,7 @@ function attributeSuspectTasks(runtimeRoot, changeName, paths) {
|
|
|
2101
2197
|
* sources: string[], // actual 实际用到的源(诊断可见性;additive)
|
|
2102
2198
|
* }}
|
|
2103
2199
|
*/
|
|
2104
|
-
export function reconcileTargetFiles({ cwd, specBase = null, changeName = null, runtimeRoot = null }) {
|
|
2200
|
+
export function reconcileTargetFiles({ cwd, specBase = null, changeName = null, runtimeRoot = null, strictMode = false }) {
|
|
2105
2201
|
const matched = []
|
|
2106
2202
|
const missing = []
|
|
2107
2203
|
const undeclared = []
|
|
@@ -2124,6 +2220,19 @@ export function reconcileTargetFiles({ cwd, specBase = null, changeName = null,
|
|
|
2124
2220
|
skipReason: `变更 ${changeName} 无 task 卡(tasks/ 缺失或为空),target_files 对账跳过`, notes, form: null, sources: [] }
|
|
2125
2221
|
}
|
|
2126
2222
|
if (decl.declarations.length === 0) {
|
|
2223
|
+
// IR 严格档(2026-09-07-ir-hardening D-003@v1,FR-01):主仓卡全部零声明 → strictViolation。
|
|
2224
|
+
// 判据(Grill P2-④修订):noDeclarationCount > 0 且 == cardCount - 跨仓卡数——跨仓卡不参与
|
|
2225
|
+
// 计数也不充当豁免(防「塞一张跨仓卡让全部主仓卡免检」绕过)。部分主仓卡已声明 → 不触发(灰度 WARNING)。
|
|
2226
|
+
if (strictMode && decl.noDeclarationCount > 0
|
|
2227
|
+
&& decl.noDeclarationCount === decl.cardCount - decl.crossRepoCards.length) {
|
|
2228
|
+
return { status: 'skipped', matched, missing, undeclared,
|
|
2229
|
+
skipReason: `严格模式变更(created_at ≥ IR_STRICT_SINCE)主仓 task 卡全部未声明 target_files(${decl.noDeclarationCount}/${decl.cardCount - decl.crossRepoCards.length} 张主仓卡${decl.crossRepoCards.length > 0 ? `,另 ${decl.crossRepoCards.length} 张跨仓卡已剔除` : ''})`,
|
|
2230
|
+
notes, form: null, sources: [],
|
|
2231
|
+
strictViolation: {
|
|
2232
|
+
code: 'target_files_all_missing_strict',
|
|
2233
|
+
message: `严格模式要求每个主仓 task 卡声明 target_files(计划改动的文件清单,taskcard 骨架已预置字段)——逐卡 Edit 填入:已存在文件写仓根相对精确路径,新建文件加 NEW: 前缀;填毕重跑 --done(进度不丢)`,
|
|
2234
|
+
} }
|
|
2235
|
+
}
|
|
2127
2236
|
// 存量零红门禁:全部卡无声明 / 全部跨仓 → WARNING 语义跳过,不产生 missing/undeclared
|
|
2128
2237
|
const reason = decl.noDeclarationCount === 0
|
|
2129
2238
|
? `本变更 task 卡全部为跨仓卡(${decl.crossRepoCards.length} 张),主仓 target_files 对账跳过`
|
|
@@ -2337,7 +2446,20 @@ function detectHeadAdvanceSinceFacts(cwd, facts) {
|
|
|
2337
2446
|
* // 诊断(additive):探针子节在场性;报告未读到的早期 skip 为 null
|
|
2338
2447
|
* }}
|
|
2339
2448
|
*/
|
|
2340
|
-
|
|
2449
|
+
/**
|
|
2450
|
+
* IR 严格模式判别(change: 2026-09-07-ir-hardening,D-001@v1):
|
|
2451
|
+
* changes.created_at ≥ IR_STRICT_SINCE → true(严格档:P3b 探针段缺失 / P3a 主仓卡全零声明 ERROR)。
|
|
2452
|
+
* fail-open:读不到 created_at(无行/db 异常/pm 缺失)→ false,落存量豁免不误伤。
|
|
2453
|
+
* @param {{ getChangeCreatedAt?: Function }} opts.pm - ProgressManager(或含同名方法的对象)
|
|
2454
|
+
*/
|
|
2455
|
+
export function isStrictChange({ pm, cwd, changeName } = {}) {
|
|
2456
|
+
if (!pm || typeof pm.getChangeCreatedAt !== 'function' || !changeName) return false
|
|
2457
|
+
const createdAt = pm.getChangeCreatedAt(cwd, changeName)
|
|
2458
|
+
if (!createdAt || typeof createdAt !== 'string') return false
|
|
2459
|
+
return createdAt >= IR_STRICT_SINCE
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
export function checkProbeConsistency({ cwd, specBase = null, changeName = null, runtimeRoot = null, strictMode = false }) {
|
|
2341
2463
|
if (!changeName) {
|
|
2342
2464
|
return { status: 'skipped', severity: null, mismatches: [],
|
|
2343
2465
|
skipReason: '无 changeName(quick 等无关联变更场景),探针一致性抽查跳过', subsections: null }
|
|
@@ -2370,8 +2492,20 @@ export function checkProbeConsistency({ cwd, specBase = null, changeName = null,
|
|
|
2370
2492
|
note: 'verify-facts.json 在场而正文探针子节全缺——疑似 agent 删除机械预填段(对比基准是正文,删 facts.json 绕不过防护)',
|
|
2371
2493
|
}], null)
|
|
2372
2494
|
}
|
|
2495
|
+
if (strictMode) {
|
|
2496
|
+
// IR 严格档(2026-09-07-ir-hardening D-002@v1,FR-01):闸门后变更不跑 verify-probes --init
|
|
2497
|
+
// 即零子节——不再是「存量兼容 skip」而是 ERROR(可绕过口收紧)。文案必须带可执行指引(R-02)。
|
|
2498
|
+
return finish('mismatch', 'error', [{
|
|
2499
|
+
probe: 'prefill',
|
|
2500
|
+
code: 'probe_prefill_missing_strict',
|
|
2501
|
+
expected: '#### 探针 预填子节在场',
|
|
2502
|
+
actual: '全部缺失',
|
|
2503
|
+
severity: 'error',
|
|
2504
|
+
note: '严格模式变更(created_at ≥ IR_STRICT_SINCE)的 verify-result.md 缺探针预填子节——先跑 sillyspec verify-probes --change <变更名> --init 生成九章节骨架并如实填写,再重跑 --done',
|
|
2505
|
+
}], null)
|
|
2506
|
+
}
|
|
2373
2507
|
return finish('skipped', null, [],
|
|
2374
|
-
'verify-result.md 无 #### 探针 子节且无 verify-facts.json
|
|
2508
|
+
'verify-result.md 无 #### 探针 子节且无 verify-facts.json(存量旧格式报告,闸门前变更),探针一致性抽查跳过')
|
|
2375
2509
|
}
|
|
2376
2510
|
|
|
2377
2511
|
// —— 重跑探针取当前指标(specDir 显式传 sb:根口径由调用方给定,不做祖先推断/漂移锚定) ——
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
5. **执行顺序**:文档 → 读代码 → 写测试 → 写实现 → 跑测试 → 验收 → 更新文档。
|
|
12
12
|
6. **判规模选档**:≤3 文件、范围明确走 `quick`;多阶段 / 架构级走完整流程。
|
|
13
13
|
7. **代码先行不补流程(倒推 B 模式)**:代码若已先写好,**不回头补 brainstorm/plan 装样子**——用 `quick --done` 收尾 + 补 quicklog 条目,把已落盘改动如实登记进进度库。
|
|
14
|
-
8. **实证核验再 `--done
|
|
14
|
+
8. **实证核验再 `--done`**:触及 `src`/`test` 的改动,CLI 会在 `quick --done` 时**亲自实测** `.sillyspec/local.yaml` 的 `commands.test` / `commands.lint`(门禁:实测失败阻断 --done 回 pending;纯 doc/配置与未配置命令自动跳过;倒推 B 模式按 --files 声明边界兜底判定)。agent 侧预跑**可选**——想省「--done 被拦→修复→重跑」一轮时才预跑;纯 doc/配置改动无需跑。以落盘文件与测试结果为准,不信口头"已完成"。
|
|
15
15
|
9. **中途停下不靠额外命令存进度**——进度已由上一次 `--done` 自动落盘;恢复时用 `sillyspec progress show` 查看进度,再用 `sillyspec run <stage>` 续跑,不直接 commit 半成品。
|
|
16
16
|
10. **实现完成后对照文档验收**(design.md / 模块文档),并检查是否影响已有测试。
|
|
17
17
|
11. **非测试逻辑本身有误时,禁止改测试来"通过"**——修逻辑,不修测试。
|