diff --git a/internal/engine/engine_test.go b/internal/engine/engine_test.go index b3cd8be..ca7cd11 100644 --- a/internal/engine/engine_test.go +++ b/internal/engine/engine_test.go @@ -1955,6 +1955,53 @@ func TestReportTextChineseLabelsAnomalySeverityAndLoopCost(t *testing.T) { } } +func TestReportOverviewMarkdownShowsAuthority(t *testing.T) { + sessions := []Session{ + { + Name: "build", + Health: 92, + Metrics: Metrics{ + SourceTool: "aider", + ModelUsed: "gpt-4.1", + ToolCallsOK: 1, + ToolUsage: map[string]int{"go test": 1}, + ToolAuthority: map[string]int{ToolAuthorityTestOrBuild: 1}, + HighestAuthority: ToolAuthorityTestOrBuild, + }, + }, + { + Name: "shell", + Health: 70, + Metrics: Metrics{ + SourceTool: "cursor", + ModelUsed: "default", + ToolCallsOK: 1, + ToolUsage: map[string]int{"bash|prod\nsecret args": 1}, + ToolAuthority: map[string]int{ToolAuthorityShellExec: 1}, + HighestAuthority: ToolAuthorityShellExec, + }, + }, + } + out := ReportOverviewMarkdown(ComputeOverview(sessions), sessions) + for _, want := range []string{ + "## Tool authority", + "| Highest category | `shell_exec` |", + "| High-authority tools | `bash\\|prod
secret args` |", + "### Authority category counts", + "| `test_or_build` | 1 |", + "| `shell_exec` | 1 |", + } { + if !strings.Contains(out, want) { + t.Fatalf("markdown report missing %q:\n%s", want, out) + } + } + for _, unwanted := range []string{"`bash|prod", "secret args` |\n|"} { + if strings.Contains(out, unwanted) { + t.Fatalf("markdown report did not escape high-authority tool name %q:\n%s", unwanted, out) + } + } +} + func TestReportOverviewHTMLIsShareableAndEscaped(t *testing.T) { sessions := []Session{ { diff --git a/internal/engine/report.go b/internal/engine/report.go index 537b28c..3224c39 100644 --- a/internal/engine/report.go +++ b/internal/engine/report.go @@ -530,6 +530,7 @@ func ReportOverviewJSON(ov Overview, sessions []Session) string { func ReportOverviewMarkdown(ov Overview, sessions []Session) string { orderedSessions := canonicalOverviewSessions(sessions) summary := overviewReportSummary(orderedSessions) + authority := overviewAuthoritySummary(orderedSessions) trend := AnalyzeHealthTrend(orderedSessions) var b strings.Builder @@ -543,6 +544,25 @@ func ReportOverviewMarkdown(ov Overview, sessions []Session) string { fmt.Fprintf(&b, "| %s | %d |\n", i18n.T("report_total_tokens"), summary.TotalTokens) fmt.Fprintf(&b, "| %s | %d / %d (%.1f%%) |\n\n", i18n.T("report_tool_failures"), summary.FailedTools, summary.TotalTools, summary.ToolFailRate) + if authority.HasData { + fmt.Fprintf(&b, "## %s\n\n", i18n.T("report_tool_authority")) + fmt.Fprintf(&b, "| %s | %s |\n|---|---:|\n", i18n.T("report_metric"), i18n.T("report_value")) + if authority.Highest != "" { + fmt.Fprintf(&b, "| %s | `%s` |\n", i18n.T("report_highest_authority"), markdownInlineCode(authority.Highest)) + } + if len(authority.HighTools) > 0 { + fmt.Fprintf(&b, "| %s | %s |\n", i18n.T("report_high_authority_tools"), reportMarkdownCodeList(authority.HighTools)) + } + if len(authority.Counts) > 0 { + fmt.Fprintf(&b, "\n### %s\n\n", i18n.T("report_authority_category_counts")) + fmt.Fprintf(&b, "| %s | %s |\n|---|---:|\n", i18n.T("report_authority_category"), i18n.T("report_count")) + for _, item := range authority.Counts { + fmt.Fprintf(&b, "| `%s` | %d |\n", markdownInlineCode(item.Category), item.Count) + } + fmt.Fprintln(&b) + } + } + fmt.Fprintf(&b, "## %s\n\n", i18n.T("incident_timeline_title")) timelines := overviewIncidentTimelines(orderedSessions, 6) if len(timelines) == 0 { @@ -1023,6 +1043,23 @@ func reportHTMLCodeList(values []string) string { return strings.Join(parts, ", ") } +func reportMarkdownCodeList(values []string) string { + parts := make([]string, 0, len(values)) + for _, value := range values { + if value != "" { + parts = append(parts, "`"+markdownInlineCode(value)+"`") + } + } + return strings.Join(parts, ", ") +} + +func markdownInlineCode(value string) string { + value = strings.ReplaceAll(value, "`", "'") + value = strings.ReplaceAll(value, "|", "\\|") + value = strings.ReplaceAll(value, "\n", "
") + return value +} + func markdownCell(value string) string { value = strings.ReplaceAll(value, "|", "\\|") value = strings.ReplaceAll(value, "\n", "
") diff --git a/scripts/ci/check-report-semantics.sh b/scripts/ci/check-report-semantics.sh index f0fa540..277a54c 100755 --- a/scripts/ci/check-report-semantics.sh +++ b/scripts/ci/check-report-semantics.sh @@ -27,6 +27,12 @@ grep -q "$expected_cost_label" "$out_dir/semantics/overview.txt" \ || fail "text overview missing expected cost label" grep -q "| $expected_cost_label |" "$out_dir/semantics/overview.md" \ || fail "markdown overview missing expected cost label" +grep -q "## Tool authority" "$out_dir/semantics/overview.md" \ + || fail "markdown overview missing tool authority summary" +grep -q "### Authority category counts" "$out_dir/semantics/overview.md" \ + || fail "markdown overview missing authority category counts" +grep -q '`test_or_build`' "$out_dir/semantics/overview.md" \ + || fail "markdown overview missing highest demo authority category" grep -q "$expected_cost_label" "$out_dir/semantics/overview.html" \ || fail "html overview missing expected cost label" grep -q "
v$version" "$out_dir/semantics/overview.html" \