fix(skills): 展开编排报告中的证据摘要

This commit is contained in:
Mengz 2026-07-26 11:59:58 +08:00
parent 9818a05aff
commit 017e18f3d1
5 changed files with 113 additions and 13 deletions

View File

@ -151,6 +151,10 @@ pr-topology.json
}
```
每条 `evidence` 不能只有 `E-xxx`、`CR-xxx` 等编号,至少还要提供 `summary`、`source`、`ref`、`status` 和可选的 `scope`。`summary` 必须说明实际观察到什么,例如“正常路径测试通过但无权限路径未覆盖”,不能只重复结论。编排器会把这些字段直接写入六张判断卡、待办和证据台账;没有可读证据时必须写明限制,而不是只显示编号。
`top_actions.evidence``findings.evidence` 可以引用证据或发现 ID 以便追溯,但阶段结果必须同时保留对应的可读 `evidence` 对象。编排器在渲染时必须将发现 ID 展开为问题摘要和直接证据,不能在待办或最终结论中只显示 `CR-001`、`E-001` 等标识。
`conclusion` 必须是可独立阅读的专项判断,不能只写“通过”“观察”或“需要处理”;
`fact``basis` 必须针对当前目标,不能复制状态词。兼容旧阶段结果时,编排器可从
`fact` 的第一条事实生成结论,并从首条 finding/action 和 evidence 生成降级说明,但必须
@ -176,7 +180,7 @@ pr-topology.json
以及影响或下一步;不能把五个阶段压缩成一段总体叙述。全部目标 PR 的判断卡展示完后,
再给最多五项跨专项待办,每项包含对象、责任方、下一动作、严重性和一个主证据。
阻断数、高风险数、安全门禁和验证状态可以作为卡片之后的索引,不能替代解释。
完整 findings、证据台账、限制和下一次复查条件放入附录或 JSON。
完整 findings、可读证据台账、限制和下一次复查条件放入附录或 JSON。证据编号仅用于追溯,不能替代证据摘要;维护者不打开 JSON 也应能在 Markdown 中看到观察事实、来源、命令或文件位置、状态和适用范围。
Markdown 使用醒目的颜色和加粗,同时保留 `[blocking]`、`[high]`、`[pass]` 等纯文本回退JSON 不得包含 HTML、ANSI 或颜色控制符。推荐颜色blocking `#B42318`、high `#B54708`、pass `#067647`、observe `#175CD3`

View File

@ -19,7 +19,7 @@
{"id": "CR-001", "owner": "author", "severity": "high", "action": "补充错误路径回归测试", "evidence": ["shortcuts/example/example_test.go:42"]}
],
"evidence": [
{"id": "E-CR-001", "kind": "test_output", "source": "local_worktree", "status": "partial", "ref": "go test ./shortcuts/example", "scope": "head:abc1234"}
{"id": "E-CR-001", "kind": "test_output", "summary": "示例命令的正常路径测试通过,但无权限和非法路径的失败场景没有覆盖", "source": "local_worktree", "status": "partial", "ref": "go test ./shortcuts/example", "scope": "head:abc1234"}
],
"limitations": ["主线合并态尚未验证"]
}

View File

@ -95,6 +95,52 @@ function Get-DisplaySeverity {
return $Severity.ToLowerInvariant()
}
function Format-EvidenceItem {
param([object]$Evidence)
if ($null -eq $Evidence) { return '' }
$id = [string](Get-Value $Evidence 'id' '')
$summary = [string](Get-Value $Evidence 'summary' '')
if ([string]::IsNullOrWhiteSpace($summary)) { $summary = [string](Get-Value $Evidence 'observed' '') }
if ([string]::IsNullOrWhiteSpace($summary)) { $summary = [string](Get-Value $Evidence 'description' '') }
$source = [string](Get-Value $Evidence 'source' '未知来源')
$reference = [string](Get-Value $Evidence 'ref' '未提供位置或命令')
$status = [string](Get-Value $Evidence 'status' 'unknown')
$scope = [string](Get-Value $Evidence 'scope' '')
if ([string]::IsNullOrWhiteSpace($summary)) {
$summary = "已采集 $source$reference"
}
$scopeText = if ([string]::IsNullOrWhiteSpace($scope)) { '' } else { ";范围 $scope" }
$idText = if ([string]::IsNullOrWhiteSpace($id)) { '' } else { "[$id] " }
return "$idText$summary(来源:$source;位置或命令:$reference;状态:$status$scopeText"
}
function Resolve-EvidenceText {
param([object[]]$EvidenceValues, [hashtable]$EvidenceIndex, [hashtable]$FindingIndex = @{}, [int]$Maximum = 2)
$items = New-Object Collections.Generic.List[string]
foreach ($value in @($EvidenceValues)) {
if ($items.Count -ge $Maximum -or $null -eq $value) { continue }
if ($value -is [string]) {
$key = [string]$value
if ($EvidenceIndex.ContainsKey($key)) {
$items.Add((Format-EvidenceItem $EvidenceIndex[$key]))
} elseif ($FindingIndex.ContainsKey($key)) {
$finding = $FindingIndex[$key]
$summary = [string](Get-Value $finding 'summary' '未提供问题摘要')
$findingEvidence = @(Get-Value $finding 'evidence' @())
$resolvedFindingEvidence = Resolve-EvidenceText -EvidenceValues $findingEvidence -EvidenceIndex $EvidenceIndex -FindingIndex @{} -Maximum 1
$items.Add("问题 $key$summary;直接证据:$resolvedFindingEvidence")
} else {
# A stage may point directly at a file, command, or API field rather than an evidence object.
$items.Add("观察位置或命令:$key")
}
continue
}
$items.Add((Format-EvidenceItem $value))
}
if ($items.Count -eq 0) { return '未采集到可直接展示的专项证据,结论已降级处理。' }
return ($items -join '')
}
function Get-StageSummary {
param([string]$Producer, [object]$Artifact)
$findings = @()
@ -145,6 +191,7 @@ function Get-StageSummary {
conclusion = $conclusion
focus = $focus
basis = $basis
evidence = @($evidence | Select-Object -First 2)
}
}
@ -385,6 +432,16 @@ function Get-DecisionImpact {
function Write-MarkdownReport {
param([string]$Path, [object]$Report)
$lines = New-Object Collections.Generic.List[string]
$evidenceIndex = @{}
foreach ($item in @($Report.evidence)) {
$id = [string](Get-Value $item 'id' '')
if (-not [string]::IsNullOrWhiteSpace($id)) { $evidenceIndex[$id] = $item }
}
$findingIndex = @{}
foreach ($item in @($Report.findings)) {
$id = [string](Get-Value $item 'id' '')
if (-not [string]::IsNullOrWhiteSpace($id)) { $findingIndex[$id] = $item }
}
$lines.Add('# PR 维护全流程摘要')
$lines.Add('')
$lines.Add("**范围:** $($Report.scope.owner)/$($Report.scope.repo) | **运行时间:** $($Report.run.as_of) | **运行 ID** ``$($Report.run.run_id)``")
@ -396,7 +453,8 @@ function Write-MarkdownReport {
foreach ($stage in @($Report.stages)) {
$aspect = Get-StageAspect ([string]$stage.producer)
$impact = Get-DecisionImpact ([string]$stage.decision)
$lines.Add("**${aspect}** $(Get-ConclusionLabel ([string]$stage.conclusion) ([string]$stage.decision))$($stage.focus);依据:$($stage.basis);影响:$impact")
$evidenceText = Resolve-EvidenceText -EvidenceValues @($stage.evidence) -EvidenceIndex $evidenceIndex -FindingIndex $findingIndex
$lines.Add("**${aspect}** $(Get-ConclusionLabel ([string]$stage.conclusion) ([string]$stage.decision))$($stage.focus);依据:$($stage.basis);证据摘录:$evidenceText;影响:$impact")
}
$finalReason = if (@($Report.top_actions).Count -gt 0) {
[string]$Report.top_actions[0].action
@ -405,7 +463,8 @@ function Write-MarkdownReport {
}
$finalNext = Get-DecisionImpact ([string]$Report.decision)
$finalConclusion = if (@($Report.top_actions).Count -gt 0) { $finalReason } else { $finalNext }
$lines.Add("**最终结论:** $(Get-ConclusionLabel $finalConclusion ([string]$Report.decision)):该动作决定当前集成状态;依据:阻断 $($Report.counts.blocking) 项、高风险 $($Report.counts.high) 项,安全门禁 ``$($Report.security_gate)``、验证 ``$($Report.verification)``;下一步:$finalNext")
$finalEvidence = if (@($Report.top_actions).Count -gt 0) { Resolve-EvidenceText -EvidenceValues @($Report.top_actions[0].evidence) -EvidenceIndex $evidenceIndex -FindingIndex $findingIndex -Maximum 1 } else { '没有高优先级动作,按各专项证据继续观察。' }
$lines.Add("**最终结论:** $(Get-ConclusionLabel $finalConclusion ([string]$Report.decision)):该动作决定当前集成状态;依据:阻断 $($Report.counts.blocking) 项、高风险 $($Report.counts.high) 项,安全门禁 ``$($Report.security_gate)``、验证 ``$($Report.verification)``;证据摘录:$finalEvidence;下一步:$finalNext")
$lines.Add('')
$lines.Add('## 先处理这几项')
if (@($Report.top_actions).Count -eq 0) {
@ -414,8 +473,8 @@ function Write-MarkdownReport {
$index = 0
foreach ($action in @($Report.top_actions)) {
$index++
$evidenceText = if (@($action.evidence).Count -gt 0) { ";证据:``$($action.evidence[0])``" } else { '' }
$lines.Add("$index. **[$($action.id)]** $(Get-ColorLabel $action.severity) $($action.action)(责任:$($action.owner)$evidenceText")
$evidenceText = Resolve-EvidenceText -EvidenceValues @($action.evidence) -EvidenceIndex $evidenceIndex -FindingIndex $findingIndex -Maximum 1
$lines.Add("$index. **[$($action.id)]** $(Get-ColorLabel $action.severity) $($action.action)(责任:$($action.owner);证据:$evidenceText")
}
}
$lines.Add('')
@ -428,6 +487,13 @@ function Write-MarkdownReport {
$lines.Add('')
$lines.Add('## 完整证据与限制')
if (@($Report.limitations).Count -gt 0) { foreach ($item in @($Report.limitations)) { $lines.Add("- 限制:$item") } } else { $lines.Add('- 未发现额外限制。') }
$lines.Add('')
$lines.Add('## 证据台账')
if (@($Report.evidence).Count -eq 0) {
$lines.Add('- 未采集到专项证据;所有依赖该证据的结论应视为受限。')
} else {
foreach ($item in @($Report.evidence)) { $lines.Add("- $(Format-EvidenceItem $item)") }
}
$lines.Add("- 详细 JSON``final-report.json``;各专项原始结果保存在同一运行目录。")
$lines.Add('- 颜色仅用于首屏强调;方括号严重性标签可在不支持 HTML 的渲染器中继续阅读。')
Write-Utf8Text -Path (Join-Path $Path 'final-report.md') -Text ($lines -join "`r`n")

View File

@ -11,24 +11,49 @@ class ChineseReportValidatorTests(unittest.TestCase):
**范围** Gitlink/gitlink-cli
**风险** 阻断 1 高风险 2 以下内容用于帮助维护者快速确认处理顺序和责任人
## PR #123
**代码审查** <span><strong>需要修改</strong></span> **[action_required]**失败路径缺少覆盖依据当前 Diff 与专项测试下一步补回归测试
**CLI 契约** <span><strong>兼容性部分成立</strong></span> **[partial]**旧调用正常但 JSON 缺边界依据帮助和输出对照下一步 golden
**仓库关系** <span><strong>需要调整顺序</strong></span> **[reorder]**目标消费上游字段依据主线open merged 索引影响先稳定上游
**集成门禁** <span><strong>当前被阻断</strong></span> **[blocked]**关键测试未完成依据合并态和验证账本下一步补测试
**维护状态** <span><strong>需要维护者接单</strong></span> **[action_required]**责任方尚未确认依据时间和分配快照下一步安排 reviewer
**最终结论** <span><strong>修复阻断问题后重新审查</strong></span> **[blocked]**存在未解决高风险项依据CR-001 IN-001下一步修复并重跑
**代码审查** <span><strong>需要修改</strong></span> **[action_required]**失败路径缺少覆盖依据当前 Diff 与专项测试证据摘录无权限测试未覆盖来源`shortcuts/example/example_test.go:42`下一步补回归测试
**CLI 契约** <span><strong>兼容性部分成立</strong></span> **[partial]**旧调用正常但 JSON 缺边界依据帮助和输出对照证据摘录`--help` 可用但 JSON golden 缺少非法参数样例下一步 golden
**仓库关系** <span><strong>需要调整顺序</strong></span> **[reorder]**目标消费上游字段依据主线open merged 索引证据摘录open PR #124 修改同一字段;影响:先稳定上游
**集成门禁** <span><strong>当前被阻断</strong></span> **[blocked]**关键测试未完成依据合并态和验证账本证据摘录`go test ./...` 尚未在合并工作树执行下一步补测试
**维护状态** <span><strong>需要维护者接单</strong></span> **[action_required]**责任方尚未确认依据时间和分配快照证据摘录队列快照中的 reviewer assignee 均为空下一步安排 reviewer
**最终结论** <span><strong>修复阻断问题后重新审查</strong></span> **[blocked]**存在未解决高风险项依据CR-001 IN-001证据摘录失败路径缺测试且全量验证未运行下一步修复并重跑
## 先处理这几项
先修复真实响应错误再补充测试然后重新执行完整验证并由维护者复看
## 五个专项状态索引
gitlink-code-review | gitlink-cli-contract-guard | gitlink-pr-topology | gitlink-pr-integrator | gitlink-maintainer-radar
## 完整证据与限制
当前结论来自固定时间快照目标提交差异构建测试和只读平台数据未验证内容已明确标记"""
当前结论来自固定时间快照目标提交差异构建测试和只读平台数据未验证内容已明确标记
## 证据台账
- [E-001] 无权限路径未覆盖来源本地测试位置或命令`shortcuts/example/example_test.go:42`状态partial"""
self.assertEqual([], validate_report(report, [123]))
def test_rejects_english_template(self) -> None:
errors = validate_report("# PR Maintenance Summary\n## Core Judgment\n**Final decision:** blocked")
self.assertTrue(any("English report template" in error for error in errors))
def test_rejects_identifier_only_evidence(self) -> None:
report = """# PR 维护全流程摘要
**范围** Gitlink/gitlink-cli
**风险** 阻断 0 高风险 1
## PR #123
**代码审查** <span><strong>需要修改</strong></span> **[action_required]**失败路径缺少覆盖依据当前 Diff证据摘录E-CR-001
**CLI 契约** <span><strong>兼容</strong></span> **[passed]**旧调用可用依据帮助证据摘录帮助输出正常
**仓库关系** <span><strong>独立</strong></span> **[passed]**无重叠依据open 索引证据摘录已扫描 open PR
**集成门禁** <span><strong>待验证</strong></span> **[partial]**全量测试未跑依据测试账本证据摘录测试未运行
**维护状态** <span><strong>待处理</strong></span> **[action_required]**等待作者依据队列证据摘录等待方为作者
**最终结论** <span><strong>重新审查</strong></span> **[action_required]**需要补测依据CR-001证据摘录E-CR-001
## 先处理这几项
1. **[CR-001]** 补测试证据``E-CR-001``
## 五个专项状态索引
已生成
## 完整证据与限制
限制已记录
## 证据台账
- E-CR-001
"""
errors = validate_report(report, [123])
self.assertTrue(any("identifier" in error for error in errors))
if __name__ == "__main__":
unittest.main()

View File

@ -18,6 +18,7 @@ REQUIRED_MARKERS = (
"## 先处理这几项",
"## 五个专项状态索引",
"## 完整证据与限制",
"## 证据台账",
)
REQUIRED_ASPECTS = (
"代码审查",
@ -85,6 +86,10 @@ def validate_report(text: str, required_prs: list[int] | None = None) -> list[st
errors.append(f"PR #{number} aspect {aspect} is not substantive")
if "依据:" not in rationale:
errors.append(f"PR #{number} aspect {aspect} is missing evidence")
if "证据摘录:" not in rationale:
errors.append(f"PR #{number} aspect {aspect} does not include readable evidence")
if re.search(r"证据:\s*``(?:E|CR|CG|TP|IN|MR)-[^`]+``", text):
errors.append("report exposes an evidence identifier without a readable evidence summary")
return errors