diff --git a/skills/gitlink-maintenance-orchestrator/SKILL.md b/skills/gitlink-maintenance-orchestrator/SKILL.md index 680710a..b239665 100644 --- a/skills/gitlink-maintenance-orchestrator/SKILL.md +++ b/skills/gitlink-maintenance-orchestrator/SKILL.md @@ -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`。 diff --git a/skills/gitlink-maintenance-orchestrator/examples/fixtures/code-review.json b/skills/gitlink-maintenance-orchestrator/examples/fixtures/code-review.json index 9f8d2ce..dd0e3b9 100644 --- a/skills/gitlink-maintenance-orchestrator/examples/fixtures/code-review.json +++ b/skills/gitlink-maintenance-orchestrator/examples/fixtures/code-review.json @@ -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": ["主线合并态尚未验证"] } diff --git a/skills/gitlink-maintenance-orchestrator/scripts/run-maintenance-pipeline.ps1 b/skills/gitlink-maintenance-orchestrator/scripts/run-maintenance-pipeline.ps1 index 94f2f3a..dbf33f2 100644 --- a/skills/gitlink-maintenance-orchestrator/scripts/run-maintenance-pipeline.ps1 +++ b/skills/gitlink-maintenance-orchestrator/scripts/run-maintenance-pipeline.ps1 @@ -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") diff --git a/skills/gitlink-maintenance-orchestrator/scripts/test_validate_chinese_report.py b/skills/gitlink-maintenance-orchestrator/scripts/test_validate_chinese_report.py index 15422eb..24e0351 100644 --- a/skills/gitlink-maintenance-orchestrator/scripts/test_validate_chinese_report.py +++ b/skills/gitlink-maintenance-orchestrator/scripts/test_validate_chinese_report.py @@ -11,24 +11,49 @@ class ChineseReportValidatorTests(unittest.TestCase): **范围:** Gitlink/gitlink-cli **风险:** 阻断 1 项,高风险 2 项,以下内容用于帮助维护者快速确认处理顺序和责任人。 ## PR #123 -**代码审查:** 需要修改 **[action_required]**:失败路径缺少覆盖;依据:当前 Diff 与专项测试;下一步:补回归测试。 -**CLI 契约:** 兼容性部分成立 **[partial]**:旧调用正常但 JSON 缺边界;依据:帮助和输出对照;下一步:补 golden。 -**仓库关系:** 需要调整顺序 **[reorder]**:目标消费上游字段;依据:主线、open 和 merged 索引;影响:先稳定上游。 -**集成门禁:** 当前被阻断 **[blocked]**:关键测试未完成;依据:合并态和验证账本;下一步:补测试。 -**维护状态:** 需要维护者接单 **[action_required]**:责任方尚未确认;依据:时间和分配快照;下一步:安排 reviewer。 -**最终结论:** 修复阻断问题后重新审查 **[blocked]**:存在未解决高风险项;依据:CR-001 与 IN-001;下一步:修复并重跑。 +**代码审查:** 需要修改 **[action_required]**:失败路径缺少覆盖;依据:当前 Diff 与专项测试;证据摘录:无权限测试未覆盖,来源:`shortcuts/example/example_test.go:42`;下一步:补回归测试。 +**CLI 契约:** 兼容性部分成立 **[partial]**:旧调用正常但 JSON 缺边界;依据:帮助和输出对照;证据摘录:`--help` 可用但 JSON golden 缺少非法参数样例;下一步:补 golden。 +**仓库关系:** 需要调整顺序 **[reorder]**:目标消费上游字段;依据:主线、open 和 merged 索引;证据摘录:open PR #124 修改同一字段;影响:先稳定上游。 +**集成门禁:** 当前被阻断 **[blocked]**:关键测试未完成;依据:合并态和验证账本;证据摘录:`go test ./...` 尚未在合并工作树执行;下一步:补测试。 +**维护状态:** 需要维护者接单 **[action_required]**:责任方尚未确认;依据:时间和分配快照;证据摘录:队列快照中的 reviewer 与 assignee 均为空;下一步:安排 reviewer。 +**最终结论:** 修复阻断问题后重新审查 **[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 +**代码审查:** 需要修改 **[action_required]**:失败路径缺少覆盖;依据:当前 Diff;证据摘录:E-CR-001。 +**CLI 契约:** 兼容 **[passed]**:旧调用可用;依据:帮助;证据摘录:帮助输出正常。 +**仓库关系:** 独立 **[passed]**:无重叠;依据:open 索引;证据摘录:已扫描 open PR。 +**集成门禁:** 待验证 **[partial]**:全量测试未跑;依据:测试账本;证据摘录:测试未运行。 +**维护状态:** 待处理 **[action_required]**:等待作者;依据:队列;证据摘录:等待方为作者。 +**最终结论:** 重新审查 **[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() diff --git a/skills/gitlink-maintenance-orchestrator/scripts/validate_chinese_report.py b/skills/gitlink-maintenance-orchestrator/scripts/validate_chinese_report.py index 2b00376..87dd867 100644 --- a/skills/gitlink-maintenance-orchestrator/scripts/validate_chinese_report.py +++ b/skills/gitlink-maintenance-orchestrator/scripts/validate_chinese_report.py @@ -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