feat: render N14 selector reports

This commit is contained in:
石头
2026-07-09 16:21:33 +08:00
parent 696ee2c7ab
commit e1e2ca92b0
2 changed files with 140 additions and 1 deletions

View File

@@ -1,6 +1,13 @@
package uiaselector
import "time"
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"time"
)
const (
StrictExitOK = 0
@@ -100,3 +107,90 @@ func StrictExitCode(report Report) int {
}
return StrictExitOK
}
func RenderReportJSON(report Report) ([]byte, error) {
return json.MarshalIndent(report, "", " ")
}
func WriteReportJSON(report Report, path string) error {
body, err := RenderReportJSON(report)
if err != nil {
return err
}
return writeFileWithParents(path, body)
}
func RenderReportMarkdown(report Report) string {
var b strings.Builder
b.WriteString("# N14 UIA Selector Report\n\n")
b.WriteString("- Source dump: `" + escapeMarkdownInline(report.SourceDump) + "`\n")
b.WriteString(fmt.Sprintf("- Catalog size: %d\n", report.CatalogSize))
b.WriteString(fmt.Sprintf("- Matched: %d\n", report.MatchedCount))
b.WriteString(fmt.Sprintf("- Unmatched: %d\n", report.UnmatchedCount))
b.WriteString(fmt.Sprintf("- Ambiguous: %d\n", report.AmbiguousCount))
b.WriteString("\n## Summary\n\n")
b.WriteString("| selector_id | status | matches | stability | evidence_path |\n")
b.WriteString("| --- | --- | ---: | ---: | --- |\n")
for _, selector := range report.Selectors {
status := "unmatched"
if selector.MatchCount > 1 {
status = "ambiguous"
} else if selector.Matched {
status = "matched"
}
b.WriteString(fmt.Sprintf(
"| %s | %s | %d | %d | %s |\n",
escapeMarkdownCell(selector.SelectorID),
status,
selector.MatchCount,
selector.StabilityScore,
escapeMarkdownCell(selector.Evidence.UIAPath),
))
}
b.WriteString("\n## Selector Details\n")
for _, selector := range report.Selectors {
b.WriteString("\n### " + escapeMarkdownInline(selector.SelectorID) + "\n\n")
b.WriteString("- Description: " + escapeMarkdownInline(selector.Description) + "\n")
b.WriteString("- Allowed use: " + escapeMarkdownInline(selector.AllowedUse) + "\n")
b.WriteString(fmt.Sprintf("- Stability score: %d\n", selector.StabilityScore))
b.WriteString(fmt.Sprintf("- Match count: %d\n", selector.MatchCount))
b.WriteString("\nMatches:\n\n")
b.WriteString("| path | control_type | automation_id | class_name | framework_id | has_name | name_length |\n")
b.WriteString("| --- | --- | --- | --- | --- | --- | ---: |\n")
for _, match := range selector.Matches {
b.WriteString(fmt.Sprintf(
"| %s | %s | %s | %s | %s | %t | %d |\n",
escapeMarkdownCell(match.Path),
escapeMarkdownCell(match.ControlType),
escapeMarkdownCell(match.AutomationID),
escapeMarkdownCell(match.ClassName),
escapeMarkdownCell(match.FrameworkID),
match.HasName,
match.NameLength,
))
}
}
return b.String()
}
func WriteReportMarkdown(report Report, path string) error {
return writeFileWithParents(path, []byte(RenderReportMarkdown(report)))
}
func writeFileWithParents(path string, body []byte) error {
dir := filepath.Dir(path)
if dir != "." && dir != "" {
if err := os.MkdirAll(dir, 0o755); err != nil {
return err
}
}
return os.WriteFile(path, body, 0o644)
}
func escapeMarkdownInline(value string) string {
return strings.NewReplacer("`", "'", "\r", " ", "\n", " ").Replace(value)
}
func escapeMarkdownCell(value string) string {
return strings.NewReplacer("|", "\\|", "\r", " ", "\n", " ").Replace(value)
}