Cybersecurity-Projects/PROJECTS/intermediate/security-news-scraper/internal/rank
CarterPerez-dev 8713712bf9 feat(nadezhda): M4 ranking + digest + Markdown/JSON export
Turn the aggregated store into a ranked, exportable digest of story clusters.

- internal/rank: pure deterministic Score(Signals, cfg.Rank) — recency
  (exp half-life decay), cvss/10, kev, epss, velocity (cluster size/age), source
  weight, watchlist keyword. Every signal clamped to [0,1]; all weights +
  half_life + velocity_norm from config, no literals. recency/velocity are total
  (guard half_life<1 and norm<=0). Rank() scores each cluster and stable-sorts
  desc, tiebreak freshest. Research 06's two worked examples are the golden-order
  test (A ~0.99 strictly above B ~0.13).
- internal/export: per-cluster (story) digest. Headline = freshest member, distinct
  sorted outlets, CVEs sorted KEV-first then CVSS. Markdown + deterministic JSON.
- store.DigestClusters: 3-query per-cluster aggregation (clusters by since, member
  articles, union of CVE signals) with explicit ORDER BY; per-cluster CVE dedup
  via SELECT DISTINCT.
- digest command: --top/--since/--format md|json/--out. Off the stub list.
- Proven live: scrape -> digest --format md and json render correctly, per-cluster
  (FortiBleed shows 3 outlets). Suite offline + -race.

One read-only audit agent; 0 Crit/High/Med, golden-order math verified; Low/Nit
fixed in-phase (top<=0 count, pure-fn guards, ORDER BY, headline sanitize, 2-cluster
attach test).

Digest ranks per-cluster (Carter's choice), not per-article.
2026-07-05 18:42:57 -04:00
..
rank.go feat(nadezhda): M4 ranking + digest + Markdown/JSON export 2026-07-05 18:42:57 -04:00
rank_test.go feat(nadezhda): M4 ranking + digest + Markdown/JSON export 2026-07-05 18:42:57 -04:00