Cybersecurity-Projects/PROJECTS/intermediate/security-news-scraper/internal/normalize
CarterPerez-dev b41276004f feat(nadezhda): M1 ingestion core (fetch, parse, normalize, dedup)
Concurrent, rate-limited feed ingestion wired end to end: 'scrape' pulls
live security news into the SQLite store, idempotent across re-runs.

- internal/fetch: per-host token-bucket rate limiting, conditional GET
  (ETag/Last-Modified with 304 handling), retry+backoff that honors
  Retry-After and treats timeouts/cancellation as non-retryable, per-source
  deadline, honest User-Agent, 16MB body cap. robots.txt is enforced only via
  Client.Allowed() for the future HTML article-scrape path, not on subscribed
  feed fetches (user-directed retrieval; several publishers blanket-disallow
  generic bots yet serve a public feed).
- internal/parse: gofeed RSS/Atom to a normalized Item; RFC1123Z/RFC822Z/
  RFC3339 time fallbacks (CISA emits RFC822Z, not RFC1123Z as first researched).
- internal/normalize: canonical URL (lowercase scheme+host, drop fragment,
  strip utm_*/gclid/fbclid/ref/mc_cid/mc_eid, drop trailing slash), title
  normalization, goquery HTML strip, sha256 content+title hashes.
- internal/ingest: errgroup fan-out over sources, fail-soft per source, fetch
  -> parse -> normalize -> InsertArticle dedup via store.ErrDuplicate, plus
  fetch_state upsert for conditional GET.
- store: migration 0002 adds articles.title_hash; GetFetchState/UpsertFetchState.
- Trimmed testdata/feeds fixtures (3 items each) back golden parse tests; full
  suite is offline and passes under -race. Proven live: cold run 215 new across
  7 sources, warm run 0 new (304s + dedup).

Ctrl-C aborts scrape cleanly via signal-aware context.
2026-07-05 14:51:57 -04:00
..
normalize.go feat(nadezhda): M1 ingestion core (fetch, parse, normalize, dedup) 2026-07-05 14:51:57 -04:00
normalize_test.go feat(nadezhda): M1 ingestion core (fetch, parse, normalize, dedup) 2026-07-05 14:51:57 -04:00