Release Notes
v2.0.2 (The Masthead) - May 25, 2026
Patch release. The headline correctness story is the classifier bug fix; the broader story is the brand redesign that aligns fishwrap.org with its own thesis. No engine API changes; no breaking changes; the v2.0 image contract holds.
🐛 Engine Correctness
- Classifier silently dropped every mixed-case keyword (#28).
editor.py:classify_articlelowercased the article text but compared keywords as-given. Every demo config’s proper-case keyword list was dead code; section routing was carried entirely bySOURCE_SECTIONSand the last-section fallback. - Classifier word-boundary check missed punctuation-adjacent matches (#29).
editor.py:score_keywordsand the parallel keyword-policy loop inscoring.pyusedf" {k} " in f" {text_blob} ", which only treats the literal space character as a boundary. Headlines likeNCIS: Origins,'Bridgerton,' Netflix renews, andThe Pitt. Noah Wyle interviewfailed to match show-name keywords. - Unified fix:
re.search(rf"\b{re.escape(k.lower())}\b", text_blob)applied to botheditor.pyandscoring.py. Lowercases the keyword and uses regex\bword boundaries that match at any\w/\Wtransition. New regression test atscripts/test_classifier_matching.pycovers case-mismatch, colon/comma/period boundaries, apostrophe-bearing keywords (grey's anatomy), embedded-substring negatives, and the parallelscoring.pypolicy boundary.
🧹 Architecture Cleanup
- Alembic infrastructure removed (follow-up to #15). Issue #15 dropped alembic from runtime requirements, but the
alembic/directory, migrations, andalembic.iniwere left in tree. The runtime path (fishwrap/db/repository.py:_initialize_engine) bootstraps viaBase.metadata.create_alland never invokes alembic. The migrations were dead code. The oldscripts/test_schema_integrity.pypointed at a gitignored local SQLite file and could not pass on a fresh clone. Replaced with a self-contained test that creates a tempfile SQLite, runsBase.metadata.create_all, and verifies the expected tables, columns, and foreign keys viasqlalchemy.inspect.
🎨 Brand Redesign
docs/BRAND.mdrewritten wholesale. The Digital Origami direction (origami fish, Deep Ocean Blue + Newsprint Cream + Salmon/Coral, “Enlightened Architect” voice) is retired. The new architecture is one brand with two voice registers under one visual home: REFUSAL (acquisition surfaces, broadsheet declarative) and EDITOR (working surfaces, magazine-warm second-person). Appendix A maps every engine surface to its required register.- Wordmark:
Fishwra¶in Libre Baskerville Bold. The pilcrow takes the place of the closing P; the pilcrow’s morphology is a P with a doubled vertical stem, the medieval scribal mark for a new paragraph. The pilcrow renders in Cardinal Red (#9C1B14), the editor’s pencil; the rest of the letterforms in Refusal Black (#0E0D0C). The substitution is announced typographically and chromatically. - Glyph and favicon: bare pilcrow in Libre Baskerville Bold, Refusal Black on Smoke Cream. Path-only SVG plus PNG raster set (16/32/48/64/96/128/180/192/256/512) plus multi-resolution ICO at site root.
- Type stack: Libre Baskerville + iA Writer Mono. Both SIL OFL, free, self-hosted under
docs/static/fonts/. No paid foundry. No third face. The voice differentiation between registers comes from prose, not from a second body face. - Asset production pipeline:
docs/static/brand/generate-assets.pyproduces wordmark SVG, favicon SVG, PNG raster set, multi-resolution ICO, and Open Graph card from the live fonts viafontToolsand ImageMagick. No generative AI in the pipeline; the brand thesis (the algorithm you can read) is preserved by producing the brand’s own marks through extant, auditable tooling. - Open Graph card: 1200×630 PNG with the wordmark, the masthead phrase (The peasants scroll. The elite read.), a placeholder edition line in iA Writer Mono, and the
fishwrap.orgURL. - Jekyll wire-up:
docs/_includes/head_custom.htmladds favicon links, og:image meta, and twitter:card meta.docs/_config.ymlgoturl: https://fishwrap.orgso absolute URLs resolve in social-share meta.
📚 Documentation
docs/index.mdrewritten. The Stephenson epigraphs from The Diamond Age and Anathem survive as the page’s literary frame; the REFUSAL-register §1 / HYBRID §2 / EDITOR §3 prose is lifted from BRAND.md with home-page warmth. Second-person warmth (“you know who you are”) replaces the bible’s clinical third person.README.mdrewritten in REFUSAL register. The Mailchimp opener (Fishwrap is a Glass-Box news engine. It transforms RSS feeds, Reddit threads, and Hacker News into finite, auditable HTML editions) is gone. Section emojis stripped. The brand bible added to the documentation table.- Engineering blog retired. Five posts and the index (
docs/blog/01_algorithms.mdthrough05_release_engineering.mdanddocs/blog/index.md) deleted. The bulk of the corpus’s off-register prose — Blade Runner / Memento / fish-judging metaphors — lived there. Engineering content lives in the code; the manifesto work the blog tried to do is now in BRAND.md §2. docs/RELEASE_NOTES.mdv1.x and v0.x entries rewritten to strip Digital-Origami-era lyrical voice while preserving every codename, date, and feature.docs/RELEASING.mdrunbook surgery (path bug fixes, anthropomorphism cleanup, em-dash flanker removal).docs/VERSIONING.md,docs/IMAGE_CONTRACT.md,docs/CONFIG_SCHEMA.md,docs/adr/001-release-artifact.mdlight surgery against the Klausner bar.
🧹 Project Ops
CONTRIBUTING.mddeleted. 250 of 250 commits in the project’s lifetime are from a single author. Zero external PRs, issues, forks, stars, or watchers. The file documented a contribution workflow that has not run. The README’s documentation-table reference was also removed. Reintroduce when an external contributor first earns one.
🛠️ Release Engineering
docs/RELEASING.mdstep 3 path bug.cd ~/gemini/fishwrapwas a stale path from a different machine setup. Corrected tocd ~/dev/fishwrap.
🧪 Tests
scripts/test_classifier_matching.py— 10 cases covering #28 and #29 across botheditor.pyandscoring.py.scripts/test_schema_integrity.py— rewritten to test the actual runtime bootstrap path (Base.metadata.create_all) via a tempfile, replacing the prior test that depended on a gitignored local artifact.
v2.0.1 (The Audit Pass) - May 24, 2026
Point release driven by a top-to-bottom code and bandwidth review of the deployed fishwrap.org site. Every change traces to a GitHub issue (#17–#26) so the audit lineage stays visible in git log.
🔒 Security
- Re-enable TLS certificate verification (#20).
utils.fetch_urlwas disablingcheck_hostnameand forcingverify_mode = CERT_NONEon the shared SSL context. Every HTTPS fetch accepted any peer certificate. Restored tossl.create_default_context()defaults. - Parse RSS/Atom via defusedxml (#21).
xml.etree.ElementTreeis documented as vulnerable to entity-expansion attacks; RSS feeds are untrusted by definition. Swapped todefusedxml.ElementTree. Regression testscripts/test_xml_safety.pyfeeds a billion-laughs payload and asserts the fetcher refuses it.
📡 Engine Reliability
- Wire a real logger across the engine (#19). Seven
except: passand silentreturn []/return Nonesites acrossfetcher.py,utils.py,auditor.py,editor.py,printer.py, anddb/repository.pywere swallowing errors against the README’s “Failed feeds are logged, not hidden” claim. Newfishwrap/log.py(stdliblogging, level viaFISHWRAP_LOG_LEVEL, stderr) replaces them. Bareexcept:clauses narrowed to specific exception types soKeyboardInterruptandSystemExitno longer get eaten mid-build. - Configurable fetcher concurrency + per-host rate limit default (#23). Hardcoded
MAX_WORKERS = 30becomes config-drivenFETCH_WORKERS(default 10). NewFETCH_DEFAULT_RATE_LIMIT_SECONDS(default 0.0) provides a global floor for hosts not enumerated inutils.RATE_LIMITS. - Code hygiene sweep (#26). Every
open()that handles text now specifiesencoding='utf-8'explicitly.editor.pyandprinter.pyswitched fromstr.replace-based fragment path math toos.path.join+os.path.dirname. - Delete dead renderer code (#18).
fishwrap/renderers/{html,pdf}.py(412 lines) was unreachable in the production pipeline. The HTML renderer usedrandom.choiceat render time (non-reproducible, manifesto violation) and unsafestr.formaton raw template strings.
🌐 fishwrap.org Bandwidth
- Showrunner image weight: 4.5 MB → ~18 KB on AVIF-capable browsers (#17). Three 1024×1024 PNGs (~4.5 MB total) replaced with AVIF + WebP + JPEG/PNG fallback via
<picture>(logo) and CSSimage-set()(section heroes). Source PNGs preserved instatic/images/sources/.scripts/optimize-theme-images.shregenerates derivatives from sources. - Externalize basic-theme CSS (#22). The basic theme inlined 21 KB of CSS in
layout.htmlplus appended a dark-theme override from a separate file. Consolidated into one externalstatic/css/style.css. Per-page HTML drops from ~24 KB to ~3 KB, and the three demos that share the theme (vanilla/cyber/ai) now hit a single cached file.
📦 Supply Chain
- SLSA build provenance + CycloneDX SBOM on every release (#24). Closes the gap that #5 (A3) left open.
release.ymlnow usesactions/attest-build-provenance@v3to attest the image digest andanchore/sbom-action@v0+actions/attest-sbom@v3to ship a verifiable SBOM.docs/IMAGE_CONTRACT.mddocumentsgh attestation verify.
📝 Documented (Not Implemented)
- #25 — fishwrap.org security response headers. HSTS, CSP,
X-Content-Type-Options,Referrer-Policy,Permissions-Policyare missing. Issue captures the recommended header set and Cloudflare Transform Rules path; no code change was the right vehicle.
🧪 Tests
scripts/test_logging_wiring.py— asserts garbage XML and unparseable Reddit JSON produce WARNING log entries.scripts/test_xml_safety.py— asserts a billion-laughs payload is rejected by defusedxml and logged.
v2.0.0 (The Newsstand) - May 5, 2026
This release marks the transition from “code you clone” to “image you pull.” Fishwrap now ships as a signed OCI image at ghcr.io/maxspevack/fishwrap. Downstream products pull the pinned image, mount a config, and run it.
This is a major version bump per docs/VERSIONING.md’s “fundamental architectural shifts” criterion. The way consumers integrate with fishwrap changes structurally. If you were cloning fishwrap into a vendor/ directory and running its venv install, that path no longer works. Switch to docker pull ghcr.io/maxspevack/fishwrap:2.0 and run via docker run. See docs/IMAGE_CONTRACT.md for the full new integration shape, and Daily Clamour for a worked example.
📦 The Artifact
- OCI image release: every
v*tag push triggers.github/workflows/release.yml, which builds and publishes the image to GHCR. Stable tags publish both the exact tag (:2.0.0) and the floating minor (:2.0); pre-release tags (-rc1,-alpha.1, etc.) publish only the exact tag. - Documented consumer contract:
docs/IMAGE_CONTRACT.mddescribes inputs, outputs, entrypoints, versioning policy, and pinning recommendations. The contract surface is the only thing consumers should depend on.
✨ New CLI Surface
fishwrap-build --config <path>: runs the full pipeline. Replaces the four-steppython -m fishwrap.{fetcher,editor,enhancer,printer}sequence.fishwrap-version: prints the running image’s semver to stdout. The only supported way for downstream consumers to read the version.fishwrap-validate-config <path>: schema-checks a config file in ~100 ms before the pipeline runs. Catches missing keys and wrong types up-front instead of mid-fetch crashes.
🤖 Production CI
- Daily demo refresh:
.github/workflows/demos.ymlruns at 12:00 UTC daily, rebuilds all four reference demos against the published image, validates output size (≥10 KB), and deploys to fishwrap.org viaactions/deploy-pages. Per-vertical isolation: one bad feed does not block other demos.
🏗️ Engine Improvements
- Self-bootstrapping schema: the engine now self-initializes its SQLite schema via
Base.metadata.create_allin_initialize_engine. Ephemeral DBs in CI just work; fresh local clones no longer crash with “no such table: articles.” - Lazy weasyprint: PDF generation is optional.
printer.pylazy-imports weasyprint inside the PDF code path, gated byImportError. The v2.0 image ships without weasyprint and its ~80 MB native dependency stack; PDF returns when a real consumer asks.
🧹 Decommissioned
- The launchd ship pipeline retired:
ship_demos.sh,publish_demo.sh,scripts/refresh_demos.sh, the launchd plist, and themake ship/make publish/make run-clamourMakefile targets all removed. CI replaces them. ROADMAP.mdremoved: roadmap state lives in GitHub Issues and Milestones to prevent drift between the file and reality.
📚 Documentation
docs/CONFIG_SCHEMA.md: every config key the engine recognizes, mirrored from the validator.docs/adr/001-release-artifact.md: internal decision record for the image-based release artifact.docs/RELEASING.md: rewritten as a procedural runbook for cutting future releases.README.md: rewritten around the image-based quick-start (docker run …instead ofmake setup).CONTRIBUTING.md: updated with a Two Audiences framing — engine contributors vs. image consumers.
🛠️ Release Engineering
- Action versions current: release and demo workflows use the latest major versions of
actions/checkout,docker/build-push-action,docker/login-action,docker/metadata-action, anddocker/setup-buildx-action. Node 24-compatible ahead of GitHub’s June 2026 deprecation. - No
:latesttag:flavor: latest=falsesuppresses the auto-generated:latesttag that would otherwise be a footgun for consumers who pin loosely.
v1.3.3 (The Synchronization) - Dec 14, 2025
Maintenance release to synchronize documentation updates and ensure all downstream artifacts build from the latest stable baseline.
- Documentation refinements clarifying the separation of concerns between Engine and Product in deployment documentation.
v1.3.2 (The Chronos Update) - Dec 13, 2025
Added precise publication-time metadata to the template context.
- Publication timestamp: the engine now injects the precise generation time into the template context as
time_str(e.g.,08:00 AM PST). - Forward compatibility: themes updated to conditionally render the timestamp.
v1.3.1 (The Polish) - Dec 13, 2025
Maintenance release covering UI refinement, build stability, and release engineering.
Bug fixes and polish
- Fixed the tab-flashing bug in the transparency modal and enforced high-contrast text colors.
- Updated the score badges to fixed widths for alignment.
- Renamed “Signal Delta” to “Delta” and reordered the Source Efficiency table (Input → Output → Delta) for logical flow.
- Replaced the text-based version string in the footer with a GitHub icon + version badge.
Release engineering
- Introduced
scripts/release.shto automate the version bump, build verification, and tagging process. - The release pipeline now enforces a
make testpass before allowing a release. - Updated
Makefileto suppress legacySyntaxWarningnoise and useinstall_venv.shfor environment setup. - Published
docs/RELEASING.mdas the runbook for shipping new versions.
v1.3.0 (Digital Origami) - Dec 13, 2025
Tabbed Transparency UI, a brand identity that has since been retired, and build hardening against Python 3.14. The brand work this release introduced (the original “Digital Origami” direction) has been superseded by the bible at docs/BRAND.md; see the README and §6 of the bible for the current visual identity.
Engineering
- Tabbed transparency modal separating Vitals (The Funnel), Sources (Efficiency), and The Bubble (Cut-Line).
- Sidebar navigation regression fixed in the demo themes.
Makefileupdated to handle shell environments explicitly (bash);install_venv.shsimplifies dependency installation.- Dependencies cleaned up; the experimental Pydantic refactor was reverted to maintain Python 3.14 compatibility.
Documentation (now superseded)
- Reorganized the engineering documentation into a blog structure. The blog was retired in May 2026; engineering content now lives alongside the code.
- Added the first version of the brand bible. This bible was rewritten in May 2026 and replaced with the current
docs/BRAND.md.
v1.2.0 (The Glass Box) - Dec 12, 2025
v1.2 added an audit log so the reader could check the engine’s working. The previous file-based store (JSON) was replaced with SQLite, and the Auditor module was introduced.
Architecture
- SQLite migration: moved from atomic JSON files to a SQLite backend (
fishwrap.db). Enables long-term history and prevents corruption during concurrent fetches. - The Auditor: new module that runs after every edition, generates a Transparency Report (
transparency.html), and records what the engine considered, scored, and cut. - Funnel metrics: introduced Anti-Feed Protection (yield rate) and Source Efficiency metrics to quantify what the engine filtered out.
Observability
- Console reporting for cut-line stories (what almost made the edition) and drift (when a story was reclassified into a different section).
fw-dbcommand-line tool for managing the newsroom database (status,prune,vacuum).
v1.1.0 (The Parallel Press) - Dec 12, 2025
v1.1 added concurrent fetching and per-domain rate limiting after the unthrottled fetcher tripped Reddit’s 429.
Performance
- Concurrency:
ThreadPoolExecutorfor the Fetcher and Enhancer. Pipeline runtime dropped from ~51s to ~15s (3.5x speedup). - Rate limiting: token-bucket rate limiting introduced after the concurrent fetcher exceeded Reddit’s per-host limit. Per-host rate limits are now configurable via
utils.RATE_LIMITS.
Logic
- v1.1 fixed a regression where the Fetcher overwrote scraped text with raw RSS data on each cycle. Subsequent runs now hit the cache for previously-enhanced articles.
- Deduplication optimized from O(N²) to effective O(N) by pre-filtering candidates with set intersection before running fuzzy matching.
v1.0.0 (The Foundation) - Dec 11, 2025
The initial release.
- Four-stage pipeline: Fetcher → Editor → Enhancer → Printer.
- JSON-file persistence (
articles_db.json). - Static HTML and rudimentary PDF output.
Pre-history
Single-line entries. Pre-v1 work was prototype territory.
- v0.9.0 — 2025-12-10. Added per-vertical configurations: cybersecurity (The Zero Day) and AI research (The Hallucination). Proved the engine was not bound to a single publication.
- v0.3.0 — 2025-12-09. Extracted the engine (
fishwrap) from the consumer publication (dailyclamour.com). Established the architectural separation that makes Fishwrap a substrate, not a product. - v0.2.0 — 2025-12-08. First publication-side visual identity for Daily Clamour, including the bento-grid layout. Engine-side branding came later and was rewritten in May 2026.
- v0.1.0 — 2025-12-07. First commit. A script to fetch, process, and print headlines.