Skip to main content

Reading List

@magistr/reading-listv2026.08.19.1· 5d agoMODELS
01README

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-listfetch reads every enabled source in a single execution (fan-out, one model lock) and writes a feed resource per source plus a merged digest ordered by tier then recency. A source that fails is recorded with ok: false and its error, and never aborts the run. latest rebuilds a filtered digest from stored feeds without re-fetching (filter by tier, source, recency, or only-new). sources lists 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.

02Models1
@magistr/reading-listv2026.08.19.1extensions/models/reading_list.ts
fn fetch()
Read every enabled source in one execution and write per-source
fn latest()
Rebuild a digest from already-stored feeds without re-fetching.
fn digestMessage()
Fetch all sources and compose a Telegram-ready HTML digest of posts
fn markRead()
Mark articles as read — persisted by URL so it survives re-fetches.
fn markUnread()
Remove articles from the read set. Same selectors as markRead;
fn discover()
Find candidate authors from links already saved in another model's
fn sources()
List the configured sources and their tiers without fetching.

Resources

feed(infinite)— Articles from one configured source
digest(infinite)— Merged reading list across all sources, tier-ordered
candidate(infinite)— A host discovered from saved links, with its feed if found
read(infinite)— Persisted read state — the set of article URLs marked read
message(infinite)— A rendered Telegram-ready digest message
03Previous Versions1
2026.08.07.1

2026.08.07.1

Security

  • Feed links are now restricted to http(s) at the parse boundary. Both parseFeedXml and parseLinkedInProfile accepted 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 — an href="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.
  • hostOf no 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 returns undefined for anything that is not http(s) or that has no hostname.

Changed

  • digestMessage's telegramModel send fails loudly when the Telegram model returns no resource. It previously logged Sent digest via <model> unconditionally on the line after the await, so a send that never happened was indistinguishable from one that did. Under swamp serve, context.runModel was observed returning without executing the callee — three consecutive green scheduled runs delivered nothing while logging success. A real send writes a sentMessage resource, 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, with fetch stubbed), adversarial (injection, malformed and gated responses, hostile selectors), coverage (regression locks on previously untested guards), and property-invariant-flow (fast-check, with an FC_NUM_RUNS soak knob). 109 tests total.
  • deno task test:soak for a high-iteration property run.
04Stats
A
100 / 100
Downloads
0
Archive size
138.8 KB
  • 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
05Platforms
06Labels