sillyspec 3.28.12 → 3.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/sillyspec-execute/SKILL.md +222 -223
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.claude/skills/sillyspec-quick/SKILL.md +8 -8
- package/.claude/skills/sillyspec-verify/SKILL.md +87 -91
- package/package.json +1 -1
- package/src/blast-surface.js +168 -0
- package/src/ceremony-tier.js +359 -0
- package/src/change-risk-profile.js +75 -202
- package/src/config-schema.js +21 -0
- package/src/decision-distill.js +32 -25
- package/src/decisions-io.js +32 -0
- package/src/design-facts.js +1 -0
- package/src/doctor-diagnostics.js +148 -1
- package/src/fr-index.js +359 -0
- package/src/index.js +92 -5
- package/src/knowledge-stats.js +124 -2
- package/src/modules.js +28 -0
- package/src/prefill.js +491 -0
- package/src/probe7-anchor-check.js +8 -6
- package/src/quick-gate-profile.js +8 -4
- package/src/quick-session-owner.js +63 -0
- package/src/review-dispatch.js +473 -1
- package/src/review-material-pack.js +265 -0
- package/src/review-tier.js +185 -41
- package/src/run/archive-distill.js +35 -0
- package/src/run/command.js +158 -87
- package/src/run/complete-handlers.js +82 -1
- package/src/run/complete.js +37 -1
- package/src/run/gates.js +301 -19
- package/src/run/prompt.js +454 -21
- package/src/run/shared.js +36 -0
- package/src/run/verify-quality-scan.js +14 -6
- package/src/scope-audit.js +8 -1
- package/src/span-risk-surface.js +127 -0
- package/src/stage-contract-spec.js +2 -2
- package/src/stage-contract.js +172 -98
- package/src/stage-review.js +26 -2
- package/src/stages/brainstorm.js +34 -16
- package/src/stages/execute.js +10 -2
- package/src/stages/plan.js +29 -12
- package/src/stages/verify.js +7 -7
- package/src/taskcard.js +3 -0
- package/src/verify-postcheck.js +375 -20
- package/src/verify-probes.js +964 -30
- package/src/worktree.js +1 -1
- package/templates/prompts/taskcard-rules.md +1 -0
- package/templates/prompts/verify-probes.md +1 -1
package/src/stage-contract.js
CHANGED
|
@@ -9,7 +9,9 @@ import { existsSync, readdirSync, readFileSync, writeFileSync } from 'fs'
|
|
|
9
9
|
import { join, basename } from 'path'
|
|
10
10
|
import { safeGit } from './git-helper.js'
|
|
11
11
|
import { nowWallClock } from './datetime.js'
|
|
12
|
-
import {
|
|
12
|
+
import { resolveChangeRisk, extractExplicitRiskLevel, checkIntegrationEvidence, VERIFICATION_NEEDS, RISK_LEVEL_CAUSES } from './change-risk-profile.js'
|
|
13
|
+
import { loadBlastDeclarationsAllProjects } from './blast-surface.js'
|
|
14
|
+
import { parseFileChangeListDetailed } from './change-list.js'
|
|
13
15
|
import { parseEvidenceSlots } from './verify-facts-schema.js'
|
|
14
16
|
import { IR_STRICT_SINCE } from './constants.js'
|
|
15
17
|
import { SCAN_REQUIRED_DOCS, AUXILIARY_STAGES } from './constants.js'
|
|
@@ -17,9 +19,13 @@ import { evaluateRules } from './stage-contract-engine.js'
|
|
|
17
19
|
import { getRule } from './stage-contract-spec.js'
|
|
18
20
|
import { backfillFrontmatter } from './scan-postcheck.js'
|
|
19
21
|
import { parseAllowedPaths } from './stages/plan-postcheck.js'
|
|
22
|
+
// FR 索引消费(2026-09-18-fr-index-l1 L1):重复嫌疑软门(advisory)——fr-index 是纯 fs+decision-distill
|
|
23
|
+
// 叶子链,静态引入无环;knowledge-hits 同为叶子。
|
|
24
|
+
import { parseChangeRequirements, resolveTouchedDomains, readActiveFrDigest, frTitleOverlap } from './fr-index.js'
|
|
25
|
+
import { discoverModuleIndex } from './decision-distill.js'
|
|
26
|
+
import { appendKnowledgeHit } from './knowledge-hits.js'
|
|
20
27
|
// design.md 文件清单解析(2026-09-17-pass-cap-semantics task-02 事实③):change-list 是纯 fs
|
|
21
28
|
// 叶子模块(design-facts / scope-audit / verify-probes 等同款直连惯例),静态引入无环。
|
|
22
|
-
import { parseFileChangeListDetailed } from './change-list.js'
|
|
23
29
|
|
|
24
30
|
/**
|
|
25
31
|
* 校验结果
|
|
@@ -294,6 +300,21 @@ function validateScanOutputs(cwd, changeName, context = {}) {
|
|
|
294
300
|
/**
|
|
295
301
|
* brainstorm 完成校验:检查四件套规范文件是否生成
|
|
296
302
|
*/
|
|
303
|
+
/**
|
|
304
|
+
* 变更目录级声明面判级(2026-09-19-ceremony-pricing-five-cuts task-03 / D-008):
|
|
305
|
+
* design 文件变更清单(change-list 单一真相源解析,双形态标题)× blast 声明(多项目并集,
|
|
306
|
+
* 调用方无 project 语境)+ design frontmatter explicit(只压 tier 不豁免 evidence,D-009)。
|
|
307
|
+
* 词表散文判级(detectChangeRisk)退役后的 stage-contract 四消费点统一入口。
|
|
308
|
+
*/
|
|
309
|
+
function resolveChangeRiskForChangeDir(changeDir, specBase) {
|
|
310
|
+
const designPath = join(changeDir, 'design.md')
|
|
311
|
+
let designText = ''
|
|
312
|
+
try { designText = readFileSync(designPath, 'utf8') } catch { designText = '' }
|
|
313
|
+
const declaredFiles = parseFileChangeListDetailed(designPath, { keepSillyspecDocs: true }).map(e => e.path)
|
|
314
|
+
const blastDeclarations = loadBlastDeclarationsAllProjects({ specBase })
|
|
315
|
+
return resolveChangeRisk({ files: declaredFiles, blastDeclarations, explicitRiskLevel: designText ? extractExplicitRiskLevel(designText) : null })
|
|
316
|
+
}
|
|
317
|
+
|
|
297
318
|
function validateBrainstormOutputs(cwd, changeName, context = {}) {
|
|
298
319
|
const { specRoot } = context
|
|
299
320
|
const changesRoot = specRoot ? join(specRoot, 'changes') : join(cwd, '.sillyspec', 'changes')
|
|
@@ -321,6 +342,37 @@ function validateBrainstormOutputs(cwd, changeName, context = {}) {
|
|
|
321
342
|
const errors = [...engineResult.errors]
|
|
322
343
|
const warnings = [...engineResult.warnings]
|
|
323
344
|
|
|
345
|
+
// ── FR 重复嫌疑软门(2026-09-18-fr-index-l1 L1,D-005:advisory 永不阻断)──
|
|
346
|
+
// 新 requirements 的无承接 FR × 同域 active 索引条目标题 bigram 重叠 ≥0.6 → warning
|
|
347
|
+
// (双出路:加承接行或改标题区分)+ fr-duplicate-warning 遥测(L3 证据发生器指标③)。
|
|
348
|
+
// fail-soft 全包:索引不存在/解析失败零打扰(同步实现——全部叶子静态导入)。
|
|
349
|
+
try {
|
|
350
|
+
const frKnowledgeRoot = specRoot ? join(specRoot, 'knowledge') : join(cwd, '.sillyspec', 'knowledge')
|
|
351
|
+
const req = parseChangeRequirements(changeDir)
|
|
352
|
+
if (!req.missing && req.frs.length > 0) {
|
|
353
|
+
const domains = resolveTouchedDomains(changeDir, discoverModuleIndex(frKnowledgeRoot))
|
|
354
|
+
const active = readActiveFrDigest(frKnowledgeRoot, domains)
|
|
355
|
+
if (active.length > 0) {
|
|
356
|
+
for (const fr of req.frs) {
|
|
357
|
+
if (fr.supersedes.length > 0 || !fr.title) continue
|
|
358
|
+
let hit = null
|
|
359
|
+
for (const a of active) {
|
|
360
|
+
const o = frTitleOverlap(fr.title, a.title)
|
|
361
|
+
if (!hit || o > hit.o) hit = { a, o }
|
|
362
|
+
}
|
|
363
|
+
if (hit && hit.o >= 0.6) {
|
|
364
|
+
warnings.push(`疑似重复 FR:新「${fr.local} ${fr.title}」与现行 ${hit.a.id}「${hit.a.title}」标题重叠度 ${(hit.o * 100).toFixed(0)}%——若为改写/取代请在该 FR 块加承接行 \`承接: ${hit.a.id}\`,若为不同需求请改标题区分(advisory,L1 观察指标)`)
|
|
365
|
+
try {
|
|
366
|
+
appendKnowledgeHit(join(specRoot || join(cwd, '.sillyspec'), '.runtime'), {
|
|
367
|
+
type: 'fr-duplicate-warning', change: changeName, title: fr.title, candidate: hit.a.id, overlap: Number(hit.o.toFixed(2)),
|
|
368
|
+
})
|
|
369
|
+
} catch { /* 遥测 fail-soft */ }
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
} catch { /* FR 软门 fail-soft:索引/解析异常零打扰 */ }
|
|
375
|
+
|
|
324
376
|
if (designContent) {
|
|
325
377
|
const content = designContent
|
|
326
378
|
|
|
@@ -340,23 +392,22 @@ function validateBrainstormOutputs(cwd, changeName, context = {}) {
|
|
|
340
392
|
}
|
|
341
393
|
}
|
|
342
394
|
|
|
343
|
-
// ── 风险判级提前提示(坑 risk-first-use-opaque,2026-08-24
|
|
344
|
-
// verify --done
|
|
345
|
-
//
|
|
346
|
-
//
|
|
347
|
-
//
|
|
395
|
+
// ── 风险判级提前提示(坑 risk-first-use-opaque,2026-08-24;2026-09-19-ceremony-pricing-five-cuts
|
|
396
|
+
// task-03 改声明面口径)── verify --done 的证据门既往只在流程末段暴露,首次使用者撞墙后
|
|
397
|
+
// 才知道出路。design --done 即按「design 文件清单 × 项目声明危险面」预判一次(advisory
|
|
398
|
+
// 不作定论):evidence:true 命中 → 透出证据要求 + 出路;explicit 已声明 → 透出「压档不豁免
|
|
399
|
+
// 证据」(D-009——旧显式短路连证据门一起免的洞已收口)。
|
|
348
400
|
{
|
|
349
|
-
const
|
|
350
|
-
if (
|
|
401
|
+
const riskPreview = resolveChangeRiskForChangeDir(changeDir, context.specRoot || specRoot || join(cwd, '.sillyspec'))
|
|
402
|
+
if (riskPreview.evidenceRequired && !riskPreview.explicit) {
|
|
351
403
|
warnings.push(
|
|
352
|
-
`[risk] 本次 design
|
|
353
|
-
|
|
404
|
+
`[risk] 本次 design 声明文件面命中项目声明危险面(${riskPreview.hitPrefixes.join('、')},evidence:true)——verify --done 将强制真实集成证据门控。` +
|
|
405
|
+
`若认为不该要求集成证据:改 _module-map.yaml 顶层 blast 段(git 可见);design frontmatter risk_level 只压仪式档、不豁免证据要求(D-009)。`
|
|
354
406
|
)
|
|
355
407
|
}
|
|
356
|
-
if (
|
|
408
|
+
if (riskPreview.evidenceRequired && riskPreview.explicit) {
|
|
357
409
|
warnings.push(
|
|
358
|
-
`[risk]
|
|
359
|
-
`若实际涉及对应改动,请在 design.md frontmatter 声明高危级(抑制可审计,防静默降级逃证据门控)。`
|
|
410
|
+
`[risk] 声明文件面命中 evidence:true 危险面(${riskPreview.hitPrefixes.join('、')}),且 frontmatter 已显式 risk_level——注意:显式声明压仪式档(tier=${riskPreview.tier}),但集成证据要求不被豁免(D-009)。`
|
|
360
411
|
)
|
|
361
412
|
}
|
|
362
413
|
}
|
|
@@ -619,40 +670,26 @@ function validateVerifyOutputs(cwd, changeName, context = {}) {
|
|
|
619
670
|
}
|
|
620
671
|
|
|
621
672
|
// ── P0: Change Risk Gate — 核心功能缺少真实集成验证时 FAIL ──
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
})
|
|
673
|
+
// 2026-09-19-ceremony-pricing-five-cuts task-03:声明面口径——design 文件清单 × blast 声明,
|
|
674
|
+
// evidenceRequired 驱动证据门(D-009:explicit 只压仪式档不豁免证据)。
|
|
675
|
+
const changeRiskProfile = resolveChangeRiskForChangeDir(changeDir, context.specRoot || specRoot || join(cwd, '.sillyspec'))
|
|
626
676
|
const conclusion = conclusionStr // 槽优先解析结果复用(此前同输入重复扫两遍)
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
// frontmatter risk_level 仍是双向权威覆盖通道。
|
|
631
|
-
if ((changeRiskProfile.suppressedTriggers || []).length > 0) {
|
|
632
|
-
warnings.push(
|
|
633
|
-
`[${changeRiskProfile.level}] 风险判级:关键词命中 ${changeRiskProfile.suppressedTriggers.join(', ')} 已被同句否定语境(不/未/无/避免…)抑制,未参与判级。` +
|
|
634
|
-
`若本变更实际涉及跨进程/状态机/启动入口改动,请在 design.md frontmatter 加 risk_level: integration-critical(或 deployment-critical)显式声明或提供真实集成证据——抑制可审计,不许用来静默降级。`
|
|
635
|
-
)
|
|
636
|
-
}
|
|
637
|
-
if (['integration-critical', 'deployment-critical'].includes(changeRiskProfile.level)) {
|
|
638
|
-
// 关键词误伤早期引导(坑2,FR-02):命中高危关键词且无 frontmatter risk_level 时,
|
|
639
|
-
// 无条件透出 frontmatter 覆盖指引(不依赖 conclusion / evidence),让 agent 早期即可
|
|
640
|
-
// 判断是否属误判并显式覆盖,而非撞到 evidence gate 末尾才发现出路③。含否定语境的
|
|
641
|
-
// 命中已在 detectChangeRisk 内被同句否定抑制(见 change-risk-profile.js NEGATION_CUES),
|
|
642
|
-
// 此处命中的都是无否定语境的真命中;仍误伤的走 frontmatter 显式覆盖通道。
|
|
677
|
+
if (changeRiskProfile.evidenceRequired) {
|
|
678
|
+
// 声明面命中披露 + 出路(旧关键词误伤引导随词表退役改写):命中 evidence:true 声明路径
|
|
679
|
+
// → 证据门强制;出路=改 map(git 可见)或提供真实证据;risk_level 压档不豁免证据。
|
|
643
680
|
if (!changeRiskProfile.explicit) {
|
|
644
681
|
warnings.push(
|
|
645
|
-
`[${changeRiskProfile.level}]
|
|
646
|
-
|
|
682
|
+
`[${changeRiskProfile.level}] 本次变更声明文件面命中项目声明危险面(evidence:true:${changeRiskProfile.hitPrefixes.join(', ')})。` +
|
|
683
|
+
`若认为不该要求集成证据:改 _module-map.yaml 顶层 blast 段(git 可见);design frontmatter risk_level 只压仪式档、不豁免证据要求(D-009)。`
|
|
647
684
|
)
|
|
648
685
|
}
|
|
649
|
-
// ── 显式 risk_level
|
|
650
|
-
// ① explicit
|
|
651
|
-
//
|
|
652
|
-
// ② explicit
|
|
653
|
-
//
|
|
654
|
-
//
|
|
655
|
-
// ③
|
|
686
|
+
// ── 显式 risk_level 分层(D-002@v1 延续 + D-009 修订,2026-09-19-ceremony-pricing-five-cuts)──
|
|
687
|
+
// ① explicit 与否不豁免证据(D-009):evidence:true 命中即进本块——explicit 低档声明不再
|
|
688
|
+
// 是证据逃生门(旧「豁免级免证据」洞已收口,出路=改 map);
|
|
689
|
+
// ② explicit + PASS WITH NOTES:必须携带结构化 handover(facts.handover 有效行;blocking 级
|
|
690
|
+
// 计入封顶口径同 validatePassEligibility 条件②)或齐全集成证据,二选一——无 handover 即挂
|
|
691
|
+
// 证据门(checkIntegrationEvidence 不过 → error);
|
|
692
|
+
// ③ 非显式(声明面判级)判定式一字不动(防 PASS WITH NOTES 绕证据门控)。
|
|
656
693
|
const notesExplicitCritical = conclusion === 'PASS WITH NOTES' && changeRiskProfile.explicit
|
|
657
694
|
const handoverRows = notesExplicitCritical ? countFactsHandoverItems(changeDir) : 0
|
|
658
695
|
const requiresEvidence = conclusion === 'PASS'
|
|
@@ -728,10 +765,11 @@ function validateVerifyOutputs(cwd, changeName, context = {}) {
|
|
|
728
765
|
` 缺失项(需在 verify-result.md 如实提供并满足):${evidenceCheck.errors.join('; ')}\n` +
|
|
729
766
|
` 每项要提供什么:${needs}\n` +
|
|
730
767
|
` 风险判级原因:${cause}\n` +
|
|
731
|
-
`
|
|
768
|
+
` 命中声明危险面(evidence:true):${changeRiskProfile.hitPrefixes.join(', ') || '(无)'}\n` +
|
|
732
769
|
` 出路:① 补全上述缺失的真实集成证据(真实启动 daemon/backend、集成测试、运行日志)后保持 PASS;` +
|
|
733
770
|
`或 ② 如实改结论 FAIL(承认端到端未验,留待部署后补);` +
|
|
734
|
-
`或 ③
|
|
771
|
+
`或 ③ 若认为该路径不该要求集成证据,改 _module-map.yaml 顶层 blast 段的 evidence 声明(git 可见)——` +
|
|
772
|
+
`design frontmatter risk_level 只压仪式档、不豁免证据要求(D-009)。` +
|
|
735
773
|
`仅改结论文案/措辞蹭字面关键词会被对账。`
|
|
736
774
|
)
|
|
737
775
|
}
|
|
@@ -746,11 +784,11 @@ function validateVerifyOutputs(cwd, changeName, context = {}) {
|
|
|
746
784
|
// ============ 探针 7 验收×测试覆盖矩阵门禁(2026-09-14-acceptance-test-matrix FR-02) ============
|
|
747
785
|
// 段格式与 src/verify-probes.js renderProbe7Lines 骨架渲染字面同源(禁第二套解析文法):
|
|
748
786
|
// 段标题「#### 探针 7:验收×测试覆盖矩阵」(全/半角冒号皆认);每 task 前 **task-NN** 锚行;
|
|
749
|
-
// 五列表 | acceptance 条目 | 归属测试文件 | 关键词命中 | 判定 | 证据 |;判定槽
|
|
787
|
+
// 五列表 | acceptance 条目 | 归属测试文件 | 关键词命中 | 判定 | 证据 |;判定槽 <待填:五选一>、
|
|
750
788
|
// 证据槽 <TODO>;列表防御行(卡无 acceptance…)与不适用行无槽不计。
|
|
751
789
|
|
|
752
|
-
/**
|
|
753
|
-
const MATRIX_VERDICT_WHITELIST = new Set(['covered', 'partial', 'uncovered', 'non-testable'])
|
|
790
|
+
/** 判定列五枚举白名单(骨架口径注记字面同源;covered-service=service 层承接,2026-09-19-api-matrix-service-coverage) */
|
|
791
|
+
const MATRIX_VERDICT_WHITELIST = new Set(['covered', 'covered-service', 'partial', 'uncovered', 'non-testable'])
|
|
754
792
|
/** 证据列未填占位(骨架字面同源) */
|
|
755
793
|
const MATRIX_EVIDENCE_TODO = '<TODO>'
|
|
756
794
|
|
|
@@ -787,11 +825,12 @@ function matrixEvidenceHasAnchor(evidence) {
|
|
|
787
825
|
}
|
|
788
826
|
|
|
789
827
|
/**
|
|
790
|
-
* 行级证据口径:covered/partial 须非 TODO 且含测试锚点;non-testable 须非 TODO
|
|
791
|
-
*
|
|
828
|
+
* 行级证据口径:covered/covered-service/partial 须非 TODO 且含测试锚点;non-testable 须非 TODO
|
|
829
|
+
* 且非空(一句话理由);uncovered 无证据要求;判定未填(不在需证据枚举内)的行只计 unfilled 不重复
|
|
830
|
+
* 计证据(covered-service 与 covered 同口径:证据即测试锚点三形态,2026-09-19-api-matrix-service-coverage)。
|
|
792
831
|
*/
|
|
793
832
|
function matrixEvidenceMissing(verdict, evidence) {
|
|
794
|
-
if (verdict !== 'covered' && verdict !== 'partial' && verdict !== 'non-testable') return false
|
|
833
|
+
if (verdict !== 'covered' && verdict !== 'covered-service' && verdict !== 'partial' && verdict !== 'non-testable') return false
|
|
795
834
|
const e = String(evidence || '').trim()
|
|
796
835
|
if (e === '' || e === MATRIX_EVIDENCE_TODO || e.startsWith('<待填')) return true
|
|
797
836
|
if (verdict === 'non-testable') return false // 非 TODO 且非空即合规(理由一句话)
|
|
@@ -889,15 +928,15 @@ function validateAcceptanceMatrix(cwd, changeName, context = {}) {
|
|
|
889
928
|
if (matrix.unfilled > 0) {
|
|
890
929
|
const list = matrix.rows.filter(r => r.unfilled).map(rowLabel).join(';')
|
|
891
930
|
errors.push(
|
|
892
|
-
`探针 7 验收×测试覆盖矩阵有 ${matrix.unfilled}
|
|
893
|
-
`修复:编辑 verify-result.md 探针 7 段,把
|
|
931
|
+
`探针 7 验收×测试覆盖矩阵有 ${matrix.unfilled} 行判定未填(五选一 covered/covered-service/partial/uncovered/non-testable):${list}。` +
|
|
932
|
+
`修复:编辑 verify-result.md 探针 7 段,把 <待填:五选一> 替换为判定值。`
|
|
894
933
|
)
|
|
895
934
|
}
|
|
896
935
|
if (matrix.missingEvidence > 0) {
|
|
897
936
|
const list = matrix.rows.filter(r => r.evidenceMissing).map(rowLabel).join(';')
|
|
898
937
|
errors.push(
|
|
899
938
|
`探针 7 验收×测试覆盖矩阵有 ${matrix.missingEvidence} 行证据缺失` +
|
|
900
|
-
`(covered/partial 证据须含测试锚点:\`.test.\` 文件名 / file:line / 反引号包裹的测试名;non-testable 证据须写一句理由):${list}。` +
|
|
939
|
+
`(covered/covered-service/partial 证据须含测试锚点:\`.test.\` 文件名 / file:line / 反引号包裹的测试名;non-testable 证据须写一句理由):${list}。` +
|
|
901
940
|
`修复:在证据列补测试锚点(如 \`test/foo.test.mjs\` 或 \`src/x.js:42\`)或 non-testable 理由。`
|
|
902
941
|
)
|
|
903
942
|
}
|
|
@@ -949,11 +988,13 @@ function validateAcceptanceMatrix(cwd, changeName, context = {}) {
|
|
|
949
988
|
// 与探针 7(验收×测试承接面)并排互补的独立对账门(design §4,D-009 非目标 / R-07 口径注记
|
|
950
989
|
// 互指):探针 7 管「每条 acceptance 由哪些测试承接」,本矩阵管「design 接口段每个端点由哪些
|
|
951
990
|
// 验证用例/冒烟步骤覆盖」——fail-closed 记账,机械封住「接口层零派生」的 P1 缺陷面:
|
|
952
|
-
// - covered 记账:分子=判定 covered
|
|
953
|
-
//
|
|
991
|
+
// - covered 记账:分子=判定 covered+covered-service 的端点行(covered-service 计分子——
|
|
992
|
+
// service 层承接是已完成、覆盖层不同;另有 advisory 单独计数承接面);有效分母=N−non-testable
|
|
993
|
+
// 行数(N=解析端点数或声明数,D-005);分子<有效分母或解析面有未覆盖端点 → error 逐条列缺覆盖端点
|
|
954
994
|
// - 锚点五形态(design §4 Grill #10):design接口表#<METHOD /path> 解析级(须命中
|
|
955
995
|
// facts.apiFace 解析产出集,防空指);权限矩阵[...]/契约表@.../DDL@.../载荷@... 形态级
|
|
956
|
-
// 存在即认;covered/partial 行缺锚点 → error(missingEvidence 口径同 probe7
|
|
996
|
+
// 存在即认;covered/partial 行缺锚点 → error(missingEvidence 口径同 probe7);
|
|
997
|
+
// covered-service 行只须测试锚点三形态(matrixEvidenceHasAnchor 同源,2026-09-19-api-matrix-service-coverage)
|
|
957
998
|
// - 移交联动(probe7 条件④同款形态):partial/uncovered 端点行>0 且 facts.handover 零
|
|
958
999
|
// 有效行 → error;blocking 是否封顶归 validatePassEligibility 条件②(④管有去向/②管去向级)
|
|
959
1000
|
// - 探索行(uncovered+[探索] 标记)与消费端子行(两空格缩进 ↳ 前缀)不进分母分子
|
|
@@ -1057,7 +1098,7 @@ function apiEvidenceHasAnchorForm(evidence) {
|
|
|
1057
1098
|
* facts?: object|null, // verify-facts.json(handover 联动用,缺 null)
|
|
1058
1099
|
* factsExpected?: boolean, // false(存量未跑管线)→ 兼容 ok 不误伤
|
|
1059
1100
|
* strict?: boolean, // isIrStrictVerifyChange(段缺失分层用)
|
|
1060
|
-
* riskLevel?: string|null, //
|
|
1101
|
+
* riskLevel?: string|null, // resolveChangeRisk().level(声明面判级兼容字段)
|
|
1061
1102
|
* changeName?: string,
|
|
1062
1103
|
* permSectionText?: string, // design.md 权限段(段头含 权限/角色 的段合并文本,壳内提取)
|
|
1063
1104
|
* designText?: string, // design.md 全文(写端点豁免行级检测用)
|
|
@@ -1136,13 +1177,22 @@ export function judgeApiCoverageMatrix(args) {
|
|
|
1136
1177
|
const unfilledRows = judgedRows.filter(r => r.unfilled)
|
|
1137
1178
|
if (unfilledRows.length > 0) {
|
|
1138
1179
|
errors.push(
|
|
1139
|
-
`接口验证覆盖矩阵有 ${unfilledRows.length}
|
|
1140
|
-
`修复:编辑 verify-result.md 接口验证覆盖矩阵段,把
|
|
1180
|
+
`接口验证覆盖矩阵有 ${unfilledRows.length} 行判定未填(五选一 covered/covered-service/partial/uncovered/non-testable):${unfilledRows.map(rowLabel).join(';')}。` +
|
|
1181
|
+
`修复:编辑 verify-result.md 接口验证覆盖矩阵段,把 <待填:五选一> 替换为判定值。`
|
|
1141
1182
|
)
|
|
1142
1183
|
}
|
|
1143
1184
|
const parseSet = new Set(endpoints.map(e => `${e.method} ${e.path}`))
|
|
1144
1185
|
const anchorViolations = []
|
|
1145
1186
|
for (const r of judgedRows) {
|
|
1187
|
+
if (r.verdict === 'covered-service') {
|
|
1188
|
+
// covered-service(service 层承接):证据即测试本身,只校验测试锚点三形态
|
|
1189
|
+
// (matrixEvidenceHasAnchor 同源口径,禁第二套解析文法);不做 design接口表# 解析级
|
|
1190
|
+
// 核对与五形态校验(不要求 design 侧锚点)——校验后 continue,不走 covered/partial 分支。
|
|
1191
|
+
if (!matrixEvidenceHasAnchor(r.evidence)) {
|
|
1192
|
+
anchorViolations.push(`${rowLabel(r)}:covered-service 证据缺测试锚点(\`.test.\` 文件名 / file:line / 反引号包裹的测试名)`)
|
|
1193
|
+
}
|
|
1194
|
+
continue
|
|
1195
|
+
}
|
|
1146
1196
|
if (r.verdict !== 'covered' && r.verdict !== 'partial') continue
|
|
1147
1197
|
const e = String(r.evidence || '')
|
|
1148
1198
|
// design接口表# 锚点解析级核对(防空指):提取的 METHOD /path 须全部命中解析产出集
|
|
@@ -1156,7 +1206,7 @@ export function judgeApiCoverageMatrix(args) {
|
|
|
1156
1206
|
if (anchorViolations.length > 0) {
|
|
1157
1207
|
errors.push(
|
|
1158
1208
|
`接口验证覆盖矩阵有 ${anchorViolations.length} 项证据锚点缺失/空指` +
|
|
1159
|
-
`(covered/partial 证据须含锚点五形态之一:design接口表#METHOD /path(须命中 design 接口段解析面)/ 权限矩阵[角色×动作] / 契约表@行标识 / DDL@列名 /
|
|
1209
|
+
`(covered/partial 证据须含锚点五形态之一:design接口表#METHOD /path(须命中 design 接口段解析面)/ 权限矩阵[角色×动作] / 契约表@行标识 / DDL@列名 / 载荷@构造点路径;covered-service 证据须含测试锚点三形态之一:\`.test.\` 文件名 / file:line / 反引号包裹的测试名):${anchorViolations.join(';')}。` +
|
|
1160
1210
|
`修复:在证据列补真实锚点(design接口表# 锚点须与 design.md 接口段端点一致,防编造端点)。`
|
|
1161
1211
|
)
|
|
1162
1212
|
}
|
|
@@ -1169,17 +1219,19 @@ export function judgeApiCoverageMatrix(args) {
|
|
|
1169
1219
|
)
|
|
1170
1220
|
}
|
|
1171
1221
|
|
|
1172
|
-
// ── covered 记账(D-005/D-008):分子=covered
|
|
1173
|
-
//
|
|
1174
|
-
//
|
|
1175
|
-
//
|
|
1176
|
-
//
|
|
1222
|
+
// ── covered 记账(D-005/D-008):分子=covered+covered-service 端点行(covered-service 并入
|
|
1223
|
+
// 分子满足覆盖等式,service 层承接是「已完成、覆盖层不同」非「未完成待移交」);有效分母=
|
|
1224
|
+
// N−non-testable 行数;N=解析端点数,解析零行按声明数(declaredRow 优先——矩阵实态,
|
|
1225
|
+
// facts.declared 兜底)。缺覆盖清单解析集驱动:解析面端点 − covered/covered-service 行命中
|
|
1226
|
+
// − non-testable 行命中(partial/uncovered/unfilled 端点行不在命中集即自然进清单;探索行
|
|
1227
|
+
// 不进分母分子——其 uncovered 判定使之天然不计分子,且不计入 non-testable 扣减)。──
|
|
1177
1228
|
if (!zeroFace) {
|
|
1178
1229
|
const N = endpoints.length > 0 ? endpoints.length : (declaredFromRow != null ? declaredFromRow : (declared != null ? declared : 0))
|
|
1179
1230
|
const nonTestableCount = rows.filter(r => r.verdict === 'non-testable').length
|
|
1180
1231
|
const validDenominator = Math.max(0, N - nonTestableCount)
|
|
1181
|
-
const
|
|
1182
|
-
const
|
|
1232
|
+
const serviceCoveredCount = rows.filter(r => r.verdict === 'covered-service').length
|
|
1233
|
+
const coveredCount = rows.filter(r => r.verdict === 'covered' || r.verdict === 'covered-service').length
|
|
1234
|
+
const coveredSet = new Set(rows.filter(r => (r.verdict === 'covered' || r.verdict === 'covered-service') && r.method && r.path).map(r => `${r.method} ${r.path}`))
|
|
1183
1235
|
const nonTestSet = new Set(rows.filter(r => r.verdict === 'non-testable' && r.method && r.path).map(r => `${r.method} ${r.path}`))
|
|
1184
1236
|
const missingEndpoints = endpoints.filter(e => {
|
|
1185
1237
|
const k = `${e.method} ${e.path}`
|
|
@@ -1188,10 +1240,17 @@ export function judgeApiCoverageMatrix(args) {
|
|
|
1188
1240
|
if (coveredCount < validDenominator || missingEndpoints.length > 0) {
|
|
1189
1241
|
const list = missingEndpoints.length > 0
|
|
1190
1242
|
? missingEndpoints.map(e => `${e.method} ${e.path}`).join(';')
|
|
1191
|
-
: `声明 ${N} 端点而 covered 端点行仅 ${coveredCount}(解析零行降级,无法逐条列端点——按声明行拆出每端点行填判定)`
|
|
1243
|
+
: `声明 ${N} 端点而 covered/covered-service 端点行仅 ${coveredCount}(解析零行降级,无法逐条列端点——按声明行拆出每端点行填判定)`
|
|
1192
1244
|
errors.push(
|
|
1193
|
-
`接口验证覆盖矩阵覆盖不足:有效分母 ${validDenominator}(N=${N} − non-testable ${nonTestableCount}),covered 分子 ${coveredCount}——缺覆盖端点:${list}。` +
|
|
1194
|
-
`修复:补验证用例/冒烟步骤后把端点行改 covered
|
|
1245
|
+
`接口验证覆盖矩阵覆盖不足:有效分母 ${validDenominator}(N=${N} − non-testable ${nonTestableCount}),covered+covered-service 分子 ${coveredCount}——缺覆盖端点:${list}。` +
|
|
1246
|
+
`修复:补验证用例/冒烟步骤后把端点行改 covered 并填五形态锚点;端点行为由 service 层测试锁定的改 covered-service 并填测试锚点(\`.test.\` / file:line / 反引号);不适用端点改 non-testable 并写一句理由;确未覆盖的走「## 移交项(结构化)」承载并保持 partial/uncovered(FR-04)。`
|
|
1247
|
+
)
|
|
1248
|
+
}
|
|
1249
|
+
// covered-service 承接 advisory(D-001/R-01 第一版,只进 warnings 不进 errors):端点级与
|
|
1250
|
+
// 间接覆盖在矩阵统计上可区分——先 advisory 观察滥用面,不设占比上限不阻断。
|
|
1251
|
+
if (serviceCoveredCount > 0) {
|
|
1252
|
+
warnings.push(
|
|
1253
|
+
`[advisory] ${serviceCoveredCount} 端点由 service 层测试承接(非端点级)——端点级与间接覆盖在矩阵统计上可区分,承接占比先 advisory 观察(D-001/R-01,不阻断)`
|
|
1195
1254
|
)
|
|
1196
1255
|
}
|
|
1197
1256
|
// 声明与解析并存以解析为准并注记(骨架 :1573 漂移信号同款口径,数据面 facts.apiFace)
|
|
@@ -1304,7 +1363,7 @@ function extractPermissionMatrixText(designMd) {
|
|
|
1304
1363
|
* - factsExpected=false(存量未跑管线,resolveFactsExpected 同源口径)→ no-op 零行为
|
|
1305
1364
|
* - verify-facts.json(apiFace/consumerHints/handover,缺失容 null——fail-closed 归纯函数)
|
|
1306
1365
|
* - 矩阵段 MD 槽解析(X-05 防篡改锚点:锚定当前文档实态)
|
|
1307
|
-
* -
|
|
1366
|
+
* - resolveChangeRisk 声明面判级(design 文件清单 × blast 声明 + explicit,task-03)
|
|
1308
1367
|
* - design.md 权限矩阵段提取(fs 只读,壳非纯函数)
|
|
1309
1368
|
*/
|
|
1310
1369
|
export function validateApiCoverageMatrix(cwd, changeName, context = {}) {
|
|
@@ -1322,10 +1381,9 @@ export function validateApiCoverageMatrix(cwd, changeName, context = {}) {
|
|
|
1322
1381
|
|
|
1323
1382
|
const facts = readFactsForEligibility(changeDir)
|
|
1324
1383
|
const designText = readIfExists(join(changeDir, 'design.md'))
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
})
|
|
1384
|
+
// 2026-09-19-ceremony-pricing-five-cuts task-03:声明面判级(design 文件清单 × blast 声明 +
|
|
1385
|
+
// explicit;level 为五级词兼容字段——evidenceRequired → integration-critical)
|
|
1386
|
+
const riskProfile = resolveChangeRiskForChangeDir(changeDir, context.specRoot || specRoot || join(cwd, '.sillyspec'))
|
|
1329
1387
|
return judgeApiCoverageMatrix({
|
|
1330
1388
|
matrix: extractApiCoverageMatrixSlots(readFileSync(verifyResultPath, 'utf8')),
|
|
1331
1389
|
apiFace: facts && facts.apiFace && typeof facts.apiFace === 'object' ? facts.apiFace : null,
|
|
@@ -1344,10 +1402,12 @@ export function validateApiCoverageMatrix(cwd, changeName, context = {}) {
|
|
|
1344
1402
|
//
|
|
1345
1403
|
// 「要不要集成证据」(validateVerifyOutputs 的 requiresEvidence)与「能不能写 PASS」(本
|
|
1346
1404
|
// validator)解耦:结论=PASS 时四个已知未验证区任一在场即 error——
|
|
1347
|
-
// ① 集成实测未跑(facts.integrationRan
|
|
1348
|
-
// ② blocking 级移交项在场(facts.handover[].severity,D-005@v2——④管「有去向」、②管「去向是否 blocking
|
|
1405
|
+
// ① 集成实测未跑(facts.integrationRan≠ran,D-006 判定表——字段不在场按未跑处理,与⑤同族口径)
|
|
1406
|
+
// ② blocking 级移交项在场(facts.handover[].severity,D-005@v2——④管「有去向」、②管「去向是否 blocking」;
|
|
1407
|
+
// facts.handover 不在场=无法核对 blocking 行,fail-closed 拦下)
|
|
1349
1408
|
// ③ db 脚本未声明执行(verify 时点文件集 ∩ db/**/*.sql 对账 facts.dbScriptDeclarations,D-012)
|
|
1350
|
-
// ④ 矩阵含 partial/uncovered 且移交项零有效行(facts.matrixPartialRows × facts.handover
|
|
1409
|
+
// ④ 矩阵含 partial/uncovered 且移交项零有效行(facts.matrixPartialRows × facts.handover——
|
|
1410
|
+
// matrixPartialRows 不在场=无法核对矩阵,按含 partial 处理,仍以「零有效移交行」为触发前提)
|
|
1351
1411
|
// 附加:runtimeEndpointExcluded=true 且判级 integration/deployment-critical 且 handover 零行
|
|
1352
1412
|
// → 计入①事实面(D-004/FR-03,仅判级时计入)。
|
|
1353
1413
|
// ⑤ 接口冒烟未跑(facts.smokeRan≠ran,仅判级 integration/deployment-critical 时计入,
|
|
@@ -1417,6 +1477,8 @@ export function evaluatePassEligibility(args) {
|
|
|
1417
1477
|
const factsMissing = !a.facts || typeof a.facts !== 'object'
|
|
1418
1478
|
const facts = factsMissing ? {} : a.facts
|
|
1419
1479
|
const handoverItems = (facts.handover && Array.isArray(facts.handover.items)) ? facts.handover.items : []
|
|
1480
|
+
const handoverFieldMissing = !factsMissing && !(facts.handover && Array.isArray(facts.handover.items))
|
|
1481
|
+
const matrixPartialMissing = !factsMissing && facts.matrixPartialRows == null
|
|
1420
1482
|
const blockingRows = handoverItems.filter(it => it && it.severity === 'blocking')
|
|
1421
1483
|
const criticalLevel = a.changeRiskProfile && ['integration-critical', 'deployment-critical'].includes(a.changeRiskProfile.level)
|
|
1422
1484
|
const NOTES_FIX = '改写结论为 PASS WITH NOTES 并在「## 移交项(结构化)」表格如实分行(类型枚举 env-blocked/manual-acceptance/db-script/other)'
|
|
@@ -1429,24 +1491,30 @@ export function evaluatePassEligibility(args) {
|
|
|
1429
1491
|
)
|
|
1430
1492
|
}
|
|
1431
1493
|
|
|
1432
|
-
// ① 集成实测未跑(时序口径 X-01:validator 时点只认已落盘 quality-scan 记录,被拦出路=重跑质量扫描步或降级 NOTES
|
|
1433
|
-
|
|
1434
|
-
|
|
1494
|
+
// ① 集成实测未跑(时序口径 X-01:validator 时点只认已落盘 quality-scan 记录,被拦出路=重跑质量扫描步或降级 NOTES,不算失败;
|
|
1495
|
+
// 字段不在场 fail-closed 按未跑处理——⑤同族口径,防存量底稿缺字段静默 PASS)
|
|
1496
|
+
if (factsMissing || facts.integrationRan !== 'ran') {
|
|
1497
|
+
const integrationDetail = factsMissing
|
|
1498
|
+
? 'facts 缺失(fail-closed 按未跑处理)'
|
|
1499
|
+
: `facts.integrationRan=${facts.integrationRan == null ? '不在场(按未跑处理)' : facts.integrationRan}`
|
|
1500
|
+
triggered.push({ fact: 'integration-not-run', detail: integrationDetail })
|
|
1435
1501
|
errors.push(
|
|
1436
|
-
`[fact integration-not-run] 集成实测未跑(${
|
|
1437
|
-
`出路:重跑质量扫描步落实测记录,或提供跨层实测回执后重跑 verify-probes 刷新 facts
|
|
1502
|
+
`[fact integration-not-run] 集成实测未跑(${integrationDetail})——结论=PASS 须集成实测已跑(D-006 判定表:quality-scan 实测记录(skip 除外)或跨层回执条目)。` +
|
|
1503
|
+
`出路:重跑质量扫描步落实测记录,或提供跨层实测回执后重跑 verify-probes 刷新 facts(字段不在场多为存量底稿未含新字段,重跑即刷新);仍未跑则${NOTES_FIX}承载(不算失败)。`
|
|
1438
1504
|
)
|
|
1439
1505
|
}
|
|
1440
1506
|
|
|
1441
|
-
// ② blocking 级移交项在场(blocking 移交项与结论 PASS
|
|
1442
|
-
if (factsMissing || blockingRows.length > 0) {
|
|
1507
|
+
// ② blocking 级移交项在场(blocking 移交项与结论 PASS 互斥;facts.handover 不在场=无法核对,fail-closed)
|
|
1508
|
+
if (factsMissing || handoverFieldMissing || blockingRows.length > 0) {
|
|
1443
1509
|
const list = blockingRows.map(it => `${it.type || '未知类型'}|${String(it.item || '').slice(0, 60)}`).join(';')
|
|
1444
|
-
triggered.push({ fact: 'blocking-handover-present', detail: factsMissing ? 'facts 缺失(fail-closed)' : `${blockingRows.length} 条 blocking 行:${list}` })
|
|
1510
|
+
triggered.push({ fact: 'blocking-handover-present', detail: factsMissing ? 'facts 缺失(fail-closed)' : handoverFieldMissing ? 'facts.handover 不在场(fail-closed 无法核对)' : `${blockingRows.length} 条 blocking 行:${list}` })
|
|
1445
1511
|
errors.push(
|
|
1446
1512
|
`[fact blocking-handover-present] ${factsMissing
|
|
1447
1513
|
? '移交项 blocking 级行无法核对(verify-facts.json 缺失,fail-closed 按在场触发)'
|
|
1448
|
-
:
|
|
1449
|
-
|
|
1514
|
+
: handoverFieldMissing
|
|
1515
|
+
? '移交项 blocking 级行无法核对(facts.handover 不在场——存量底稿未含该字段或管线故障,fail-closed 拦下;重跑 verify-probes 幂等刷新 facts 即可消解)'
|
|
1516
|
+
: `移交项含 ${blockingRows.length} 条 blocking 级行——承认有必须兜底的未竟事项即不是 PASS(④管「有去向」、②管「去向是否 blocking」)`}。` +
|
|
1517
|
+
`触发行:${factsMissing ? '(facts 缺失,无法核对——fail-closed)' : (list || '(handover 不可核对,无行可列)')}。修复:${NOTES_FIX};确属 advisory 的按降级文法「(降级:<理由>,依据 <file:line 或 D-xxx>)」显式降级。`
|
|
1450
1518
|
)
|
|
1451
1519
|
}
|
|
1452
1520
|
|
|
@@ -1462,11 +1530,18 @@ export function evaluatePassEligibility(args) {
|
|
|
1462
1530
|
)
|
|
1463
1531
|
}
|
|
1464
1532
|
|
|
1465
|
-
// ④ 矩阵含 partial/uncovered 且移交项零有效行(任意 severity
|
|
1466
|
-
|
|
1467
|
-
|
|
1533
|
+
// ④ 矩阵含 partial/uncovered 且移交项零有效行(任意 severity——部分实现必须有移交去向;
|
|
1534
|
+
// matrixPartialRows 不在场=无法核对矩阵,fail-closed 按含 partial 处理,触发前提仍是零有效移交行——
|
|
1535
|
+
// 有真移交行在场即「去向已承载」,不因字段缺失误拦)
|
|
1536
|
+
if (factsMissing || ((matrixPartialMissing || facts.matrixPartialRows > 0) && handoverItems.length === 0)) {
|
|
1537
|
+
const matrixDesc = factsMissing
|
|
1538
|
+
? '(facts 缺失,无法核对——fail-closed)'
|
|
1539
|
+
: matrixPartialMissing
|
|
1540
|
+
? '(facts.matrixPartialRows 不在场,无法核对——fail-closed 按含 partial 处理;重跑 verify-probes 刷新 facts 即可消解)'
|
|
1541
|
+
: `${facts.matrixPartialRows} 行 partial/uncovered`
|
|
1542
|
+
triggered.push({ fact: 'matrix-partial-no-handover', detail: factsMissing ? 'facts 缺失(fail-closed)' : matrixPartialMissing ? 'facts.matrixPartialRows 不在场(fail-closed 按含 partial 处理)且 handover 零有效行' : `matrixPartialRows=${facts.matrixPartialRows} 且 handover 零有效行` })
|
|
1468
1543
|
errors.push(
|
|
1469
|
-
`[fact matrix-partial-no-handover] 验收×测试覆盖矩阵含 ${
|
|
1544
|
+
`[fact matrix-partial-no-handover] 验收×测试覆盖矩阵含 ${matrixDesc} 且「## 移交项(结构化)」零有效行——部分实现必须有移交去向。修复:${NOTES_FIX}承载(任意 severity 均可,blocking 级另受条件②约束)。`
|
|
1470
1545
|
)
|
|
1471
1546
|
}
|
|
1472
1547
|
|
|
@@ -1565,7 +1640,7 @@ function collectDbScriptCandidates({ cwd, changeName, specRoot, changeDir }) {
|
|
|
1565
1640
|
* PASS 封顶 validator 注册壳(与 validateAcceptanceMatrix 同三参签名同构,D-010:注册进
|
|
1566
1641
|
* contracts.verify.validators 即覆盖 gates / machine-interface 等全部 runValidators 调用方,
|
|
1567
1642
|
* gates.js 零改动)。壳内取数组装:verify-result.md 结论(槽优先,legacy 回退)、
|
|
1568
|
-
* verify-facts.json(缺失容 null)、
|
|
1643
|
+
* verify-facts.json(缺失容 null)、resolveChangeRisk 声明面判级、事实③文件集;判定调
|
|
1569
1644
|
* evaluatePassEligibility 纯函数。
|
|
1570
1645
|
*/
|
|
1571
1646
|
export function validatePassEligibility(cwd, changeName, context = {}) {
|
|
@@ -1585,10 +1660,9 @@ export function validatePassEligibility(cwd, changeName, context = {}) {
|
|
|
1585
1660
|
conclusion,
|
|
1586
1661
|
facts,
|
|
1587
1662
|
factsExpected: isIrStrictVerifyChange(changeDir) || facts !== null,
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
}),
|
|
1663
|
+
// 2026-09-19-ceremony-pricing-five-cuts task-03:声明面判级(level 兼容字段驱动 evaluatePassEligibility
|
|
1664
|
+
// 的 critical 判定——evidenceRequired → integration-critical,D-009 证据不被 explicit 豁免)
|
|
1665
|
+
changeRiskProfile: resolveChangeRiskForChangeDir(changeDir, context.specRoot || specRoot || join(cwd, '.sillyspec')),
|
|
1592
1666
|
changeName,
|
|
1593
1667
|
dbScriptCandidates: collectDbScriptCandidates({ cwd, changeName, specRoot, changeDir }),
|
|
1594
1668
|
})
|
package/src/stage-review.js
CHANGED
|
@@ -445,7 +445,7 @@ export function getLatestStageReviewRunId(runtimeRoot, stage, changeName) {
|
|
|
445
445
|
* @param {string} changeName - 变更名(跨变更过滤锚)
|
|
446
446
|
* @returns {{ priorRunId: string, verdicts: string, openFindings: string[], passItems: string[], notesPreview: string }|null}
|
|
447
447
|
*/
|
|
448
|
-
export function collectSameStagePriorReview(runtimeRoot, stage, changeName) {
|
|
448
|
+
export async function collectSameStagePriorReview(runtimeRoot, stage, changeName) {
|
|
449
449
|
if (!runtimeRoot || !stage || !changeName) return null
|
|
450
450
|
const dir = join(runtimeRoot, 'stage-reviews')
|
|
451
451
|
if (!existsSync(dir)) return null
|
|
@@ -490,11 +490,27 @@ export function collectSameStagePriorReview(runtimeRoot, stage, changeName) {
|
|
|
490
490
|
openFindings.push(`[verdict-fail] verdict=fail 但 checklist 无明细——以 reviewerNotes 为准:${notes || '(无 notes)'}`)
|
|
491
491
|
}
|
|
492
492
|
if (openFindings.length === 0 && passItems.length === 0) return null
|
|
493
|
+
// fixDiff 腿(QA Gap 2 修正,design Wave 2.4 兑现):复审唯一基准面的另一半——上一轮之后
|
|
494
|
+
// 主文档的修复 diff 摘要(git diff --stat 半边,主文档相对 HEAD 的未提交改动;纯函数侧
|
|
495
|
+
// 不再追多提交窗口——复审场景主文档几乎总在工作树)。fail 时留空串不阻断渲染。
|
|
496
|
+
let fixDiff = ''
|
|
497
|
+
try {
|
|
498
|
+
const metaPath = join(runtimeRoot, '..', 'changes', changeName)
|
|
499
|
+
const docCandidates = ['design.md', 'plan.md', 'proposal.md']
|
|
500
|
+
for (const doc of docCandidates) {
|
|
501
|
+
const dp = join(metaPath, doc)
|
|
502
|
+
if (!existsSync(dp)) continue
|
|
503
|
+
const { safeGit } = await import('./git-helper.js')
|
|
504
|
+
fixDiff = String(safeGit(join(runtimeRoot, '..', '..'), ['diff', '--stat', 'HEAD', '--', dp], { trim: false }) || '')
|
|
505
|
+
if (fixDiff.trim()) break
|
|
506
|
+
}
|
|
507
|
+
} catch { /* fixDiff best-effort:无 git/无 diff → 空串(渲染体仍成立) */ }
|
|
493
508
|
return {
|
|
494
509
|
priorRunId: latest.runId,
|
|
495
510
|
verdicts: `spec=${latest.review.specVerdict}, quality=${latest.review.qualityVerdict}`,
|
|
496
511
|
openFindings,
|
|
497
512
|
passItems,
|
|
513
|
+
fixDiff: fixDiff.trim().slice(0, 2000),
|
|
498
514
|
notesPreview: String(latest.review.reviewerNotes || '').replace(/\s+/g, ' ').slice(0, 160),
|
|
499
515
|
}
|
|
500
516
|
}
|
|
@@ -513,17 +529,25 @@ export function collectSameStagePriorReview(runtimeRoot, stage, changeName) {
|
|
|
513
529
|
export function renderPriorRoundFindingsMd(collected, cap = 15) {
|
|
514
530
|
if (!collected) return ''
|
|
515
531
|
const L = []
|
|
516
|
-
|
|
532
|
+
// 2026-09-19-review-material-pack D-002/Grill 交叉点 1:建议语→排他语——本块是再审的
|
|
533
|
+
// **唯一基准面**(fixDiff 由采集端并入 collected.openFindings 的证据行,渲染体扩展、
|
|
534
|
+
// 占位符机制零改动)。子代理服从必读清单而非回灌块的教训:排他语必须明写「唯一」。
|
|
535
|
+
L.push(`**同阶段上一轮审查结论(run ${collected.priorRunId},verdict ${collected.verdicts})——本材料是本轮复审的唯一基准面:只核验下列未决项的修复状态与本次改版新增面,不重演全量审查(全量重读=重做已付的评审)**:`)
|
|
517
536
|
if (collected.openFindings.length > 0) {
|
|
518
537
|
const shown = collected.openFindings.slice(0, cap)
|
|
519
538
|
L.push(`- **上一轮未决问题(逐项核验修复状态:已修给证据、未解决必须如实再次 fail——漏放行=假通过)**(${shown.length}/${collected.openFindings.length} 条):`)
|
|
520
539
|
for (const f of shown) L.push(` - ${f}`)
|
|
521
540
|
}
|
|
541
|
+
if (collected.fixDiff && String(collected.fixDiff).trim()) {
|
|
542
|
+
L.push('- **上一轮后的修复 diff 摘要(唯一基准面的证据半边——核验修复是否真实落地)**:')
|
|
543
|
+
L.push('```diff\n' + String(collected.fixDiff) + '\n```')
|
|
544
|
+
}
|
|
522
545
|
if (collected.passItems.length > 0) {
|
|
523
546
|
const shown = collected.passItems.slice(0, cap)
|
|
524
547
|
L.push(`- **上一轮已实证 pass 面(勿重复报告/重验——除非本次修复改动明确触及该面才定向复查)**(${shown.length}/${collected.passItems.length} 条):`)
|
|
525
548
|
for (const p of shown) L.push(` - ${p}`)
|
|
526
549
|
}
|
|
550
|
+
L.push(`- **包外查证口径**:本基准面之外的文件可定向查证但须在 reviewerNotes 列明查证清单;禁全量扫读仓。材料不足以核验某未决项→该项 cannot_verify+列缺件,不得靠全量重读自救。`)
|
|
527
551
|
return `\n${L.join('\n')}\n`
|
|
528
552
|
}
|
|
529
553
|
|