fix(skills): 展开编排报告中的证据摘要
This commit is contained in:
parent
9818a05aff
commit
017e18f3d1
|
|
@ -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`。
|
||||
|
||||
|
|
|
|||
|
|
@ -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": ["主线合并态尚未验证"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue