HTML capture: extraction gate, archetype routing, newsletters (Substack + email digests) (#2) - #11
Draft
janikmu wants to merge 9 commits into
Draft
HTML capture: extraction gate, archetype routing, newsletters (Substack + email digests) (#2)#11janikmu wants to merge 9 commits into
janikmu wants to merge 9 commits into
Conversation
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, & 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
marked this pull request as draft
July 23, 2026 15:24
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 toarticle) refusesdigest/indexpages as single artifacts; pre-fetch URL hints for known newsletter landing/email links.bulk-capturegains 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)/api/v1/postsAPI; list-and-pick or--expand-all.raidar newsletter {subscribe,unsubscribe,list,poll}+ a weeklycom.airadar.newsletterslaunchd agent. Cadence-agnostic — "new" is decided by canonical post URL, never URL arithmetic or an assumed schedule.source_url+via).Phase 3 — in-body implementation repos
For a single article, the classifier returns
linked_repos, grounded against the article text;--follow-reposcaptures 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
webextract,substack,subscriptions,email_digest) wired intoinfra/smoke.sh.page_kindcorrectly 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.--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
896a67d"Make launchd agent install reachable after initial setup") that was the branch base and isn't onmainyet — happy to split it out if you'd prefer it in its own PR.source_url/viaprovenance and asubscriptions.jsonregistry beside the config (not a vault entity); no changes to the concept/artifact schema.raidar install-launchdto pick up the new weeklynewslettersagent (raidar healthwarns until then).raidar healthflags a dangling ref (agent-development-frameworks→multica-ai/multica) in the vault — worth a separate cleanup.🤖 Generated with Claude Code