Reading List
A tiered reading list for a hand-picked set of writers — reads the Swamp Club feed first, then the Wardley-mapping crowd, and merges everything into one priority-ordered digest.
Model:
@magistr/reading-list—fetchreads every enabled source in a single execution (fan-out, one model lock) and writes afeedresource per source plus a mergeddigestordered by tier then recency. A source that fails is recorded withok: falseand its error, and never aborts the run.latestrebuilds a filtered digest from stored feeds without re-fetching (filter by tier, source, recency, or only-new).sourceslists the configured sources and tiers.
Two source kinds are supported. rss handles RSS 2.0 and Atom, unwrapping
CDATA first — Substack, Medium and WordPress wrap titles and authors in
CDATA, which an XML parser otherwise reports as empty. linkedin reads the
JSON-LD @graph embedded in a public LinkedIn profile, covering both short
posts and Pulse articles; it is unauthenticated and therefore fragile by
nature, so it is isolated behind the same per-source error handling.
Re-running fetch is idempotent for firstSeenAt: the previous run's
values are carried forward per article URL, so only genuinely unseen items
are flagged isNew.
Ships with a curated default source set (Swamp Club at tier 1; joapen,
Chris Daniel, Jon Ayre, Adrian Cockcroft and Simon Wardley at tier 2), so
the model works with no configuration. Override sources to read anyone
else.
Resources
2026.08.07.1
Security
- Feed links are now restricted to
http(s)at the parse boundary. BothparseFeedXmlandparseLinkedInProfileaccepted any string as an article URL, so a hostile or compromised feed could emit<link>javascript:alert(1)</link>and have it rendered into an<a href="…">in the Telegram digest. Escaping does not help here — anhref="javascript:…"needs no metacharacter to fire. Links carrying any other scheme (javascript:,data:,file:,mailto:) and relative or protocol-relative links are now dropped, and the check lives in one place rather than being re-derived by each renderer. Found by the new adversarial suite. hostOfno longer returns an empty string for a hostile scheme.new URL("javascript:alert(1)")parses successfully with an empty hostname, so the previous try/catch let""through — falsy, but defined, leaving every caller to remember that""meant failure. It now returnsundefinedfor anything that is nothttp(s)or that has no hostname.
Changed
digestMessage'stelegramModelsend fails loudly when the Telegram model returns no resource. It previously loggedSent digest via <model>unconditionally on the line after theawait, so a send that never happened was indistinguishable from one that did. Underswamp serve,context.runModelwas observed returning without executing the callee — three consecutive green scheduled runs delivered nothing while logging success. A real send writes asentMessageresource, so the returned handles are now required and their absence throws. Prefer a dedicated Telegram workflow step over this argument; a step and a direct method run both work where the in-method call did not.
Added
- Four test suites completing the workspace quality standard, alongside the
existing contract suite:
methods(every method's success and failure paths, withfetchstubbed),adversarial(injection, malformed and gated responses, hostile selectors),coverage(regression locks on previously untested guards), andproperty-invariant-flow(fast-check, with anFC_NUM_RUNSsoak knob). 109 tests total. deno task test:soakfor a high-iteration property run.
- Has README or module doc2/2earned
- README has a code example1/1earned
- README is substantive1/1earned
- Most symbols documented1/1earned
- No slow types (deprecated)1/1earned
- Dependencies pass trust audit2/2earned
- Has description1/1earned
- Platform support declared (or universal)2/2earned
- License declared1/1earned
- Verified public repository2/2earned