Files
awesome-coding-agents/readme.go
T
tiennm99andGitHub 0d5e4b3266 feat: momentum metrics, agents.yml validation, drift detection, hardening (#9)
Review fixes:
- applyMigrations resolves chained renames deterministically (cycle guard)
- GraphQL errors name the failing owner/repo, not the alias
- sanitizeCell escapes backslash + angle brackets (README md/HTML injection)
- timeNow/graphqlURL seams; new httptest fetch tests, site + delta tests
  (coverage 30% -> 65.6%)

Features:
- delta30d computed from history; dashboard gains sortable D30d column
- README shows Top 7-day mover line
- 'go run . -check' validates agents.yml offline (dupes, categories,
  name patterns); wired into CI for contributor PRs
- isArchived + rename drift warnings in daily run; archived badge on
  dashboard (E2E found 3 archived repos: gpt-engineer, void, Roo-Code)
2026-08-09 03:26:01 +07:00

123 lines
3.3 KiB
Go

package main
import (
"fmt"
"os"
"path/filepath"
"strings"
"text/template"
)
type Row struct {
Rank int
NameWithOwner string
URL string
Stars int
Delta7d int
HasDelta bool
Language string
PushedAt string
Description string
Category string
}
// TopMover is the biggest 7-day gainer, surfaced as a one-line callout above
// the README table. HasMover is false when no repo has a 7-day delta yet
// (e.g. history younger than 7 days) — the template omits the line then.
type TopMover struct {
NameWithOwner string
URL string
Delta int
HasMover bool
}
func renderReadme(tmplPath, outPath string, stats []Stat, deltas map[string]int) error {
rows := make([]Row, len(stats))
var topMover TopMover
for i, s := range stats {
// Deltas are keyed by CanonicalKey (owner/repo from agents.yml).
delta, has := deltas[s.CanonicalKey]
rows[i] = Row{
Rank: i + 1,
NameWithOwner: s.NameWithOwner,
URL: s.URL,
Stars: s.Stars,
Delta7d: delta,
HasDelta: has,
Language: s.Language,
PushedAt: s.PushedAt.Format("2006-01-02"),
Description: sanitizeCell(s.Description),
Category: s.Category,
}
if has && (!topMover.HasMover || delta > topMover.Delta) {
topMover = TopMover{NameWithOwner: s.NameWithOwner, URL: s.URL, Delta: delta, HasMover: true}
}
}
funcs := template.FuncMap{
"formatDelta": func(d int, has bool) string {
if !has {
return "—"
}
if d > 0 {
return fmt.Sprintf("+%d", d)
}
if d < 0 {
return fmt.Sprintf("%d", d)
}
return "0"
},
"formatStars": func(n int) string {
switch {
case n < 1000:
return fmt.Sprintf("%d", n)
case n < 1_000_000:
return fmt.Sprintf("%.1fk", float64(n)/1000)
default:
return fmt.Sprintf("%.1fM", float64(n)/1_000_000)
}
},
}
tmpl, err := template.New("").Funcs(funcs).ParseFiles(tmplPath)
if err != nil {
return err
}
f, err := os.Create(outPath)
if err != nil {
return err
}
execErr := tmpl.ExecuteTemplate(f, filepath.Base(tmplPath), map[string]any{
"Rows": rows,
"UpdatedAt": timeNow().UTC().Format("2006-01-02 15:04 UTC"),
"Total": len(rows),
"TopMover": topMover,
})
// A failed close on a write path can hide lost data — surface it.
if closeErr := f.Close(); closeErr != nil && execErr == nil {
execErr = closeErr
}
return execErr
}
// sanitizeCell makes a third-party repo description safe to embed in a
// single Markdown table cell. Order matters:
// 1. backslash first, so escaping added by later steps isn't re-escaped
// (also fixes `\|` rendering as a literal backslash + unescaped pipe,
// which broke the table row).
// 2. pipe, so the cell can't inject a table column boundary.
// 3. angle brackets → HTML entities, so raw HTML/script tags in a
// description can't be injected into the rendered README.
// 4. newlines/carriage returns → space, so the cell stays one line.
func sanitizeCell(s string) string {
s = strings.ReplaceAll(s, "\\", "\\\\")
s = strings.ReplaceAll(s, "|", "\\|")
s = strings.ReplaceAll(s, "<", "&lt;")
s = strings.ReplaceAll(s, ">", "&gt;")
s = strings.ReplaceAll(s, "\n", " ")
s = strings.ReplaceAll(s, "\r", " ")
return strings.TrimSpace(s)
}