Skip to content

HTML capture: extraction gate, archetype routing, newsletters (Substack + email digests) (#2) - #11

Draft
janikmu wants to merge 9 commits into
mainfrom
feat/issue-2-html-capture
Draft

HTML capture: extraction gate, archetype routing, newsletters (Substack + email digests) (#2)#11
janikmu wants to merge 9 commits into
mainfrom
feat/issue-2-html-capture

Conversation

@janikmu

@janikmu janikmu commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Closes the implementation half of #2 ("Decide on and test HTML capture expected outcomes"). The analysis + agreed roadmap live in the issue comments; this PR builds Phases 0–3 plus newsletter subscriptions and HTML-email digest ingest.

Why

Capturing the open web faithfully is the hard part of #2. Running the example URLs through raidar's real extractor showed two structural risks: capture would silently fabricate junk artifacts from login walls / JS-only pages / dead newsletter links, and blanket "bulk-capture every GitHub link" would ingest newsletter self-promo footers as signal. Priority 1 was newsletters — which turned out to be the least faithfully-capturable category until handled deliberately. Guiding doctrine throughout: a skipped capture beats a polluted knowledge base.

What changed

Phase 0 — do-no-harm extraction gate (lib/webextract.py)
Centralises fetch + a confidence gate; capture now refuses an unusable extraction (login wall, JS SPA, dead link, too-thin) with an actionable hint instead of classifying nav cruft or the bare URL.

Phase 1 — archetype routing + bulk-capture gate
New LLM page_kind (biased to article) refuses digest/index pages as single artifacts; pre-fetch URL hints for known newsletter landing/email links. bulk-capture gains an awesome-list shape gate (repo count + owner spread + diversity) so it stops eating newsletter footers and incidental partner links.

Phase 2 — Substack newsletters (lib/substack.py, lib/subscriptions.py, jobs/newsletter.py)

  • Manual: a pasted Substack landing/archive URL auto-expands to per-post permalinks via the unauthenticated /api/v1/posts API; list-and-pick or --expand-all.
  • Recurring: raidar newsletter {subscribe,unsubscribe,list,poll} + a weekly com.airadar.newsletters launchd agent. Cadence-agnostic — "new" is decided by canonical post URL, never URL arithmetic or an assumed schedule.
  • Captured items carry provenance (source_url + via).

Phase 3 — in-body implementation repos
For a single article, the classifier returns linked_repos, grounded against the article text; --follow-repos captures them as siblings under the same concept (e.g. ghuntley.com/ralph → repomirror). Default is to suggest, not auto-ingest.

Mode C — HTML-email digest ingest (lib/email_digest.py)
Newsletters like AlphaSignal arrive as HTML email (web archive is auth/bot-walled). raidar newsletter ingest <file.eml> parses the MIME, LLM-triages links to drop ads/sponsors + chrome before resolving anything (sponsor trackers are never clicked), follows the click-tracking redirects to real destinations, auto-captures the GitHub repos (with provenance), and prints a review list of everything else (HF models, articles) with ready-to-run capture commands.

Verification

  • Offline unit smoke tests for every new lib (webextract, substack, subscriptions, email_digest) wired into infra/smoke.sh.
  • Live-verified against the Decide on and test HTML capture expected outcomes #2 example URLs: pollution cases (LinkedIn, AlphaSignal, Substack index) now refused; page_kind correctly labels ethical.institute→digest, ghuntley→article, refactoring.fm/s/essays→index; the bulk gate refuses the 6-link newsletter and passes a 40-repo list.
  • Sandbox-verified end to end (isolated vault): Substack expand + provenance, subscription baseline + poll new-edition detection, ghuntley --follow-repos (repo lands in the same concept with signal tracking), and email ingest (triage 4 content / 1 ad / 1 chrome; sponsor never resolved; repos route to capture, HF model + news article to the review list).

Reviewer notes

  • Bundles one pre-existing commit (896a67d "Make launchd agent install reachable after initial setup") that was the branch base and isn't on main yet — happy to split it out if you'd prefer it in its own PR.
  • Deferred to their own issues (as agreed on Decide on and test HTML capture expected outcomes #2): Phase 4 (general non-Substack digest decomposition, LinkedIn), Phase 5 (non-GitHub artifact types & signals — HF/Ollama/Kaggle/arXiv). Phase 5 is what would let email ingest auto-capture the models it currently review-lists.
  • Data model: adds source_url/via provenance and a subscriptions.json registry beside the config (not a vault entity); no changes to the concept/artifact schema.
  • After merge, run raidar install-launchd to pick up the new weekly newsletters agent (raidar health warns until then).
  • Unrelated pre-existing finding: raidar health flags a dangling ref (agent-development-frameworksmultica-ai/multica) in the vault — worth a separate cleanup.

🤖 Generated with Claude Code

janikmu and others added 9 commits June 29, 2026 19:30
Web URLs that yield no usable text (login walls, JS-only SPAs, dead
newsletter links) were silently classified into junk artifacts: capture
fell back to feeding the bare URL string / nav cruft to the LLM. Centralise
fetch + a confidence gate in lib/webextract and make capture refuse a
low-confidence extraction with an actionable message instead of polluting
the vault.

Verified against the issue #2 examples: refuses the newsletter pollution
cases (LinkedIn too-short, AlphaSignal fetch-fail, Substack index JS-wall)
while passing genuine articles. Digest-shape pages (ethical.institute) still
pass — that is Phase 1's archetype-routing job, not an extraction failure.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…Phase 1)

capture no longer writes a junk artifact for multi-item pages:
- pre-fetch URL hints for known newsletter landing/index/email links
  (LinkedIn, AlphaSignal, Substack archive) that point at the per-article URL;
- a new LLM page_kind field (article|digest|index|other), biased toward
  'article', that refuses web 'digest'/'index' pages after classification.
  Verified live: ethical.institute -> digest, ghuntley/ralph -> article,
  anthropic opus -> article, refactoring.fm/s/essays -> index.

bulk-capture gains an awesome-list shape gate (repo count + owner spread +
diversity, config-driven via thresholds.bulk_*) so it stops ingesting
newsletter publisher footers and incidental partner links; --force overrides.
Verified: the 6-link ethical.institute newsletter and the 1-link Gemma
announcement are refused, a 40-repo diverse list passes, a single-org dump
is refused.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Manual capture of a Substack landing/archive URL now discovers the per-article
permalinks via the unauthenticated /api/v1/posts API (works for *.substack.com
and custom domains like refactoring.fm) and either lists them (default, so the
user/agent picks) or captures them all (--expand-all). A per-post /p/<slug> URL
and ordinary articles on arbitrary domains are never expanded.

Captured items now carry provenance: source_url (the page captured) and via
(the newsletter that surfaced it). Verified live against refactoring.fm in a
sandbox vault: expansion lists /p/ permalinks, --expand-all captures with both
provenance fields written.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mode B: `raidar newsletter {subscribe,unsubscribe,list,poll}`. subscribe
registers a Substack publication (custom domains included) and baselines its
current posts so only NEW editions are captured later; --catch-up N also grabs
the latest N now. poll is the recurring entry point — it captures every post not
yet seen across subscriptions, stamping `via` provenance on each.

Cadence- and form-agnostic by design: "new" is decided by canonical post URL
(never URL arithmetic or an assumed schedule), so weekly/monthly/irregular
newsletters are all handled by one fixed-clock poll. The registry is a small
JSON list beside the active config (tool state, not a vault concept/artifact)
with a capped seen-set. Only Substack is pollable today; other newsletter forms
are refused at subscribe time rather than stored as something we can't honor.

Verified in a sandbox vault: --catch-up captures the latest and baselines the
window; poll reports 0 new when caught up and exactly the new edition when one
appears.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a com.airadar.newsletters launchd agent that runs `raidar newsletter poll`
Sundays at 19:00 — before enrich (20:00) and digest (21:00) — so new editions
land in the vault ahead of the weekly passes. One fixed schedule suffices for
all newsletter cadences since poll captures whatever is unseen.

install_launchd.sh now maps each plist label to its subcommand (handling the
two-token `newsletter poll`) and templates all three agents; launchd.py and the
health launchd check pick up the new agent automatically, so `raidar health`
nudges existing installs to reinstall and gain it. Verified: the generated plist
validates and its ProgramArguments are three separate strings under bash.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A post that introduces a concept often links its reference implementation
(ghuntley.com/ralph -> repomirrorhq/repomirror). The classifier now returns
linked_repos for articles; _capture_one grounds them against the article text
(dropping hallucinated/off-page links, capped at 3) and, with --follow-repos,
captures each as a sibling repo artifact with `via` provenance. Default is to
SUGGEST, not auto-ingest, per the do-no-harm stance.

Verified live: ralph -> linked_repos=[repomirrorhq/repomirror] (Gemma's
incidental NVIDIA-NeMo link correctly returns []); --follow-repos captures the
repo into the SAME concept (post introduces / repo implements) with signal
tracking; default run only prints the suggestion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
README: four jobs (incl. weekly newsletter poll), new lib/job modules
(webextract, substack, subscriptions, newsletter), third launchd agent.
SKILL.md: capture refusal behavior, Substack expansion, --follow-repos, and
the `raidar newsletter` subscription commands.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Set no_args_is_help=True on the newsletter Typer group so invoking it without a
subcommand lists the available commands instead of erroring with "Missing
command" — matching `raidar search` and the root CLI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Many newsletters (AlphaSignal, Beehiiv/Mailchimp senders) arrive as HTML email,
not a fetchable page — the web archive is auth/bot-walled, so the email itself is
the reliable surface. `raidar newsletter ingest <file.eml>` (or stdin) parses the
MIME, LLM-triages the links to drop ads/sponsors and chrome BEFORE resolving
anything (so sponsor trackers are never clicked), follows the click-tracking
redirects to real destinations, then auto-captures the GitHub repos (with `via`
provenance) and prints a review list of everything else (HF models, articles)
with ready-to-run capture commands — never auto-writing artifact types raidar
cannot yet model (do-no-harm).

lib/email_digest.py handles MIME parsing, anchor/href extraction (tracker-dup
collapse, &amp; unescaping, chrome filtering) and redirect resolution.

Verified against a real AlphaSignal issue (--dry-run): triage = 4 content / 1 ad
/ 1 chrome, with the Lambda sponsor link never resolved; spec-kit + LangBot route
to GitHub capture; the Nemotron HF model and the Anthropic news article go to the
review list with their resolved URLs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@janikmu
janikmu marked this pull request as draft July 23, 2026 15:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant