Tom MacWright

2025@macwright.com

How to contribute to open source

Aut inveniam viam aut faciam: I shall either find a way or make one.

Another ranking of people on GitHub dropped, and for the odd metric of 'stars from repos where a developer has merged PRs' I rank third, as of this writing, in the United States. Like all rankings, it's mostly lies, statistics, coincidence, and a reflection of GitHub's top-heavy usage.

But it's a nice metric because that is what I've focused on for the last few years: instead of trying to create some popular new framework, I've been trying to contribute more to existing projects.

So, free advice to the new open source contributor: when you hit a bug or a limitation in some project, file an issue and volunteer to fix it if you think you can.

You'll learn a lot from working in lots of projects - how do they set up tests and linter rules, what are their code styles, etc?

Don't use LLMs to do this. Using an LLM especially to write the PR description or anything like that cheats both you and them: you're missing out on the learning and experience, and they will become wary of automated contributions. Don't be lazy.

Is this still an effective way to stay in open source and do good: I think so. So, when you hit a bug, instead of doing a workaround, a patch, or switching tools, try and make a way by fixing it. Treat all bugs like your responsibility because you're an active community member.

Effect notes: PRs, progress, and joys

It's been three months since the last Effect devlog and I'm still incrementally adopting Effect in Val Town.

Things are going well but not spectacularly. My approval rating is a solid 'B' right now.

I'm far from the only or most important Effect user, but I'm bummed that a majority of my annoyances from October & November of 2025 are outstanding: a drizzle bug, a Cron bug, a vitest incompatibility, documentation improvements are all stalled. I write a brief fork that implemented Cron iteration in the reverse direction, Cron.prev, and Kit finished and merged it, but that took three months to get merged and has been stalled without release for another three months. The documentation for Effect.fn.Return got written by a core team member but that PR got closed without merging. I tried writing some docs for stream interop which never got merged or reviewed. I got a minor documentation improvement about generators merged with explicit coordination from the Effect team.

None of that stuff is a dealbreaker and I know from privately chatting with the Effect team that they prefer PRs to be coordinated because there's so much in flight and so much to understand. I think the current scenario, in which people don't know that because it isn't written anywhere, and put in effort into PRs that stall, isn't very good for community vibes.

Probably some of this delay is because Effect v4 has been a major focus of the Effectful team. v4 does seem exciting: a smaller, more unified, faster module is great news. We haven't migrated yet because we use some of the deprecated APIs, like Runtime, and I try to avoid using beta releases in general in production software.

Obligatory LLM discourse: my usual AI tool, Claude Code with Opus 4.6, is decent at using Effect but stumbles in the same places that the documentation is lacking, like that Effect.fn.Return type - it doesn't like to use that. I've used the LLM for roughly half of the refactors to Effect, but recently I've been finding it slower than doing things manually along with ast-grep. There are faster LLMs, but I've found quality and speed to be strongly inversely correlated, and fast models like Minimax tend to get themselves into corners faster.

Effect joys

The Duration and DateTime modules have been really nice for describing times and limits in the application. Soon, Duration will do a lot of the same things that those Effect utilities can do, but it's nice to have them a little early.

We have a lot of Drizzle queries that try to fetch one record, so .limit(1) and then .pipe(Effect.map(r => r.at(0)) and I recently created a nice little dual method as a helper. This was not especially easy to write!

/**
 * For the many many database queries where we want to take the first
 * element and return NotFoundError if it is not found.
 *
 * @example
 * db.query(...)
 *  .pipe(takeFirst('Project not found'));
 */
export const takeFirst = dual<
  (
    that: string
  ) => <T, Error, Requirements>(
    self: Effect.Effect<T[], Error, Requirements>
  ) => Effect.Effect<NonNullable<T>, Error | NotFoundError, Requirements>,
  <T, Error, Requirements>(
    self: Effect.Effect<T[], Error, Requirements>,
    that: string
  ) => Effect.Effect<NonNullable<T>, Error | NotFoundError, Requirements>
>(2, (self, message = "Not found") => {
  return Effect.flatMap(self, (value) => {
    const first = value.at(0);
    if (first !== undefined && first !== null) {
      return Effect.succeed(first);
    }
    return Effect.fail(new NotFoundError({ message }));
  });
});

The more methods get ported to Effect the more I can use Effect.gen or Effect.fn to combine them, which is nice - feels like a tipping point in many ways. This is something that I have noticed that LLMs are hesitant to do: they're pretty single-minded when working on a task and will happy let two Effect.runPromise statements sit on consecutive lines when they could be combined.

The friction of Effect at boundaries is still there: Val Town uses Fastify, tRPC, React Router, etc., and we have a lot of existing code, so we aren't achieving the brilliant purity that Effect docs insist upon. Tests still don't use @effect/vitest because of its missing feature and lack of vitest 4 support, so there's a lot of unwrapping Effects there too.

Overall: it rolls on, and I'm starting to slowly introduce Effect to the tough core of the application, the part that actually runs vals, and has many complex asynchronous flows. That is going fairly well: the last push was to replace our homemade implementation of Promise.withResolvers() that also had a Bun-inspired .peek() method to synchronously get a Promise value if there is one. Effect's Deferred was a drop-in replacement for that problem area.

Placemark & OSS Changelog

What's new with Placemark and open source recently:

Placemark

  • The features table in Placemark now lets you sort by different columns in ascending or descending order.
  • The features table had resizable columns, which broke. I fixed that, and you can resize columns again.
  • I've created a documentation website for Placemark, recreating some of what existed with Webflow but with Astro Starlight.

tokml

  • I've adopted changesets for release automation, like I've been doing on all my projects. The combination of changesets with a release action that publishes to NPM using OIDC is really nice, and makes maintenance and updates easier.
  • I dropped the UMD build, making the NPM package roughly 30% smaller.

simple-statistics

  • Quantile computations now match what R does. Thanks zhengshui for the fix.
  • Also, adopted changesets, like the rest of the projects.

I've been trying to 'fix what I find' in projects - small fixes in stuff like fastify-sensible and 11ty-vento. Contributing to existing projects in small ways feels good, I think it would be nice if the average longevity of projects were higher, and small contributions are what keep me interested in my older projects, too.

ROOTS - Return Old Online Things to your own Site

Lisa Charlotte Muth just coined ROOTS: "Return Old Online Things to your own Site." She's collecting all her old content and putting it all on her site.

Good idea! I share all the sentiments in that post, and hope to do the same, and also do some manual review of my old posts to fix some bitrotted iframe embeds. Inlining everything would be really nice - I want some self-hostable local playground element for interactive code examples.

Reactionary AI Centrism

The current wave of AI discourse is what I'd call "Radical AI Centrism." The gist is:

  • "Good" can mean "morally or ethically good" or "effective and able to do what it says it does."
  • Many critics of AI have made arguments about both: that AI is both a 'glorified spellcheck that doesn't work very well' (not good at what it does) and 'increasing electricity prices, destroying jobs, etc.' (not good in a moral sense)
  • In the last five months or so, AI models have gotten better at what they do, according to most quantitative and qualitative measures.
  • This has made some past critiques of AI inaccurate, and some critics have been slow to adjust their rhetoric for the new reality of AI models that can write passable code.
  • Therefore, a new space for criticism has opened up: a "backlash to the backlash" in which AI critics are critiqued for insufficiently appreciating how AI is 'good at what it does.'
  • This realm of argument is useful strategically because:
    • It is narrowly true.
    • It satisfies the reactionary centrist impulse to avoid straightforward descriptions of 'bad things' and to instead find complex, sophisticated alternative takes.
    • For many people, it's more fun to argue with people in their own circle than to critique large corporations.
    • It shifts the argument away from the moral, ethical, and political realm (moral good) and toward the realm of technology and effectiveness (good at what it does).

Media diet

I've been tweaking my media diet recently for three goals:

  1. I don't want to be exposed to advertising.
  2. I'd like to avoid some big companies.
  3. I want more honest news.

I already pay for a few big newspapers and use uBlock with my browser Helium, but I still get ads on the mobile versions of news apps like the New York Times and Bloomberg. Considering how much these subscriptions cost, I think that's pretty silly.

And it's very clear that those papers haven't been writing honest coverage of some important world events.

So, here's what I'm listening/reading/watching more often:

  • NPR Plus is $96/yr and listening to 1A and Consider This cover most of my audio news needs.
  • The Guardian is $90/yr and is ad-free for subscribers.
  • News As Facts is good for global headlines and exclusively links to Wikipedia for terms, which is where I go anyway.
  • Nebula is not quite a YouTube alternative, but it has a few major creators who I follow, like Adam Neely, and lets me watch their videos without ads for a relatively cheap subscription ($30/yr).

What I haven't figured out

Some technical things that I feel like I've never really figured out, that I'm trying to figure out:

How much should applications fail-fast and how much should they tolerate failure?

Failing fast feels right and I've implemented it in a lot of places - using envsafe or something similar is a necessity on any project I work on, for example: if an application isn't properly configured, it should fail at startup instead of limping along.

But should applications tolerate failed database queries in an elegant way? What about failed external services?

I think one clear line is that an application shouldn't allow internal inconsistency. For example, if you have some function that's being called with an incorrect argument type, you update the callers instead of making the function more flexible. This probably isn't the case when companies grow in size because eventually you can't tell a whole team to just update all their code when an API changes.

But the line keeps moving: in particular, I think the last two years has shown me that it's useful to have a system that can fail partially, and that every single external service will fail at some point, and you should have a plan for those things, whether it's tolerating failure or doing retries.

How should logs work?

Every application that I've worked on eventually just generates several 'flavors' of log messages out of stdout and stderr and logs stop being useful because they're filled with 'junk' like request logs.

I've tried structured JSON logs with pino, tried tslog, Betterstack, Axiom, and never got it. We've never had a team member that really got value out of logs. I've never really gotten value out of logs. I often wonder if servers should emit logs at all, and instead we should just do telemetry and metrics?

Changing my mind

I've changed my mind about a lot of stuff recently:

  • I have been using Claude Code a bunch. I feel deeply conflicted about it: it isn't satisfying to use, it has all these ethical problems, but it works really well. Only the highest-level models work well for writing code: opencode is much nicer and less glitchy than Claude Code, but it uses Grok by default which is a lot worse. I'm only using it on work stuff. Partly the watershed moment was the Claude Code form factor: in-editor AI suggestions are annoying and disruptive, whereas the CLI editor-lite direction is a lot better. Note: please don't recommend another coding CLI to me, I've seen them all and despite using them, I am existentially tired by the LLM discourse.
  • I switched from Alfred to Raycast. The biggest reason was that I want to run less software, and Raycast replaces Session and Rectangle for me. I've also been impressed with 'Raycast-the-platform' - its Strava integration shows the last 4 weeks' mileage in my menu bar. Not tied directly to Raycast as a quick switcher, but nifty. To rewind, every time I tried Raycast in the past I was frustrated about its 'quick switching' abilities - typing a few characters of an application's name would be less reliable than with Alfred, presumably because Raycast was doing fancier string matching. But now I use LeaderKey for switching and that's not an issue.
  • I've been adopting Effect at work, after swearing it off for a year. I think my qualms with it are still pretty true (the team does not care enough about documentation and it can be difficult to use) but there are also a lot of benefits - a big ecosystem that's pretty mature, and the opportunity to remove other NPM modules because Effect includes nice implementations of things like DateTime, Duration, and Cron types.
  • I switched this blog to use 11ty after being a holdout using Jekyll for roughly 14 years. Jekyll kept encountering problems building the site. 11ty is fine and the migration was mostly smooth. I still have some incremental work to do with it. The site builds slightly faster but that wasn't a real motivation for the switch, it was more about switching to a static site builder that is actively maintained. Plus I had a bunch of post-optimization involved with the site that was already written in JavaScript, and using 11ty let me integrate that with the site build process.

Graph layout

Honestly, I've been running a little low on passion for side-projects and true deep dives lately. Lots of reasons, most of which you can probably guess if you also live in the US or work in tech.

But I'm still pretty obsessed with graph layout. Basically I love d3-force - force-directed layout, but I think that it's used everywhere and isn't the right choice for everything. And graph layout is catnip to computer scientists so there's a ton of cool research being written about alternative algorithms.

Image

I think this graph could be better. That's a real load-bearing could though: Mike's work on d3 is big for a reason: he focused on and solved a lot of really hard problems that others were scared of.

Image

But on the New York subway, I see their subway maps and I am transfixed. Hand-drawn charts look different than what a computer can generate. Old charts are just amazing.

There are a bunch of things about beautiful charts that I appreciate:

  • Orthogonal or semi-orthogonal layouts: all of the edges are horizontal, vertical, or in some cases, diagonal. Subway maps have diagonals and I think it looks amazing. As far as I can tell so far though, most chart implementations are either freeform or orthogonal.'
  • Edge bundling. See circuit diagrams. If there are a bunch of edges following similar routes, they should line up and potentially be combined into one. Here's an example of edge bundling with a circular layout.
  • Good nodes. For example, subway charts don't have all of the subway lines pinching together for each station, but instead they show the station as kind of a horizontal joining line.
  • Symmetry. It's human, but it's pretty nice to see charts that prioritize making things symmetrical, even if that's not optimal in another direction.

So I've been reading some papers.

I liked A Walk On The Wild Side: a Shape-First Methodology for Orthogonal Drawings. Some of the takeways:

  • Most orthogonal drawing is based on the Topology-Shape-Metrics paradigm, which tries hard to avoid links crossing over each other.
  • Instead, they care more about minimizing bends in the links, which they think looks better and matters more than crossings.
  • They do so with a SAT solver (oooh!) so they can defer some of the really hard algorithmic work to that.

But so far my favorite is HOLA: Human-Like Orthogonal Network Layout. The results look spectacular. The gist is:

  • Instead of optimizing for something they think is nice, they tested what people would do if they laid out graphs themselves.
  • They came up with five criteria for good graphs (trees placed outside, aesthetic bend points, compactness, grid-like node placement, symmetry - though I recommend just looking at the paper because there are good illustrations for these).
  • I haven't read through the implementation but I'm guessing it's pretty complicated to achieve all of these goals. I think the main implementation is in libdialect. There's a JavaScript port of sorts called cola.js, which has a gridified example but I don't think that's actually HOLA. Also there's a python implementation of HOLA, but that relies on the C++ adaptograms implementation so I don't know if it would be any easier to port.
  • Bracketing the feelings involved, potentially a TypeScript port of libdialect is possible via LLMs. I suspect that the lack of real integers and the different performance profile might be tricky, though.

Sidenote: when reading through the ogdf documentation I saw that they use earcut, made by Mapbox! Cool to see that foundational work like that is so widely adopted, and liberal open source licensing makes it possible.

HOLA was written in 2015, so I went looking for more recent work, and found Praline which has a Java implementation by the authors.

And then A Simple Pipeline for Orthogonal Graph Drawing, which cites PRALINE and HOLA as examples and has really nice output. I was hopeful that the 'simple' in that paper meant that it was simple to implement, which… not sure. There's a Scala implementation.

It's amazing to me that some of this really cutting-edge work sits in repositories on GitHub with 6 stars (one of them mine) when they represent so much real thought and effort. Of course the real product is the paper, and the real reward is PhDs, tenure, respect of their peers, and citations. But still!

Then the same authors as "A Simple Pipeline" - Tim Hegemann and Alexander Wolff - wrote Storylines with a Protagonist which as an online demo which implements a lot of the nice parts of subway-map drawing!


I'm having fun following along with fancy graph-drawing algorithms! Some questions that I am looking to answer next:

  • If I use an LLM to translate Scala to TypeScript and popularize one of these, am I the baddie? It feels kind of bad to think about it.
  • Are force-directed graphs successful because they're fast and general and that puts a ceiling on these nicer but potentially slower and less general alternatives?
  • Do graph drawing algorithms with eight-directional links (diagonals) exist? I can't figure out what to search for and I haven't found any evidence that this is supported yet.

Misc engineering truisms

  • Data structures are the foundation of programming
    • IDs are identifiers. Names are not identifiers. Do not use names as identifiers.
    • The fewer databases you use the better. Consistency between datasets is hard, and it's painful to make requests against multiple datasources and handle multiple kinds of failure. Postgres goes a long way.
    • Either both compute and databases are in the same region, or both are geographically distributed. Having a big hop between a server and its database is a recipe for bad performance.
    • Network locality is really important. Put things close to each other, in the same region or datacenter if you can.
    • It is much more common for applications to be slow because of slow database queries than it is for them to be slow because of inefficient algorithms in your code. If you're good at knowing how indexes and queries work, it'll help you a lot in your career.
    • Similarly, it is more common for applications to be slow because of bad algorithms than bad programming languages. 'Slow' languages like Ruby are fast enough for most things if code is written intelligently.
  • Scale is hard to anticipate
    • Everything everywhere should have a limit from day one. Any unlimited text input is an arbitrary storage mechanism. Any async task without a timeout will run forever eventually.
  • The internet is an adversarial medium
    • All websites with user-generated content are vectors for SEO farming, phishing, and other malevolent behavior. Every company that hosts one will have to do content moderation.
    • All websites with a not-at-cost computing component will be used for crypto mining. Every company that provides this has to fight it. See: GitHub Actions, even that was used for crypto-mining.
  • Postgres stuff
    • Use TEXT for all text stuff, and citext for case-insensitive text. There is no advantage to char or varchar, avoid them.
    • Don't store binary data as base64'ed TEXT or hex TEXT or whatever, store it as binary data. bytea is good.
    • Basically every table should have a created_at column that defaults to NOW(). You'll need it eventually.
  • Misc lessons learned
    • API tokens should be prefixed or identifiable so they can be identified by security scanners. Don't use UUIDs as api tokens. Something like servicename_base58-check-encoded-random-bytes is good.
  • Speed of iteration is really important
    • Deploys, CI, and release should all be automated as much as possible, but no more than that.
  • Interfaces
    • Most of the time, power and simplicity are a direct tradeoff: powerful interfaces are complex, simple interfaces are restrictive. Aiming to create something powerful and simple without a plan for how you'll achieve that is going to fail. Getting more power without complexity is the hardest and most worthwhile activity.
    • Most "intuition" is really "familiarity." There are popular interfaces that are hard to learn and look weird, but are so commonplace that people are used to them and consider them friendly. There are friendly interfaces that are so rare that people consider them intimidating.
  • Tests
    • What tests are testing for can be wrong, and you'll end up enforcing incorrect logic for the long term. Making tests readable and then re-reading them from time to time is a good counterweight.
    • Test coverage is wonderful if it's possible, but there are many applications where really you can't get full test coverage with good ROI.
  • Abstractions that are usually worth it.
    • Result/Either types are worth their weight most of the time, if you're in JavaScript. It makes more sense to build with them from the start rather than putting them in later.
    • An 'integrations/' directory where you instantiate SDKs for external services is usually good in the long run.
    • Validating all your environment variables at startup is essential - in JavaScript, envsafe, Effect, zod are all good options for this. It is painful to crash after deployment because some function relied on an env var that was missing.

Archive

The module pattern really isn't needed anymore How are we supposed to do tooltips now? Luxury of simplicity Increasingly miffed about the state of React releases I wish there was a better default for database IDs codemirror-continue How could you make a scalable online geospatial editor? Chrome Devtools protip: Emulate a focused page How to set headers on objects in R2 using rclone What editors do things use? On Web Components Replay.web is cool Don't use marked Headlamps are better flashlights Using Just CSS Roundup Code-folding JSX elements in CodeMirror Incentives Running motivation hacks Remix notes About Placemark.io Web pages and video games The S&P 500 is largely a historical artifact Hooking up search results from Astro Starlight in other sites React is old Notes on using Linear Hawbuck wallets Takeaway from using CO₂ monitors: run the exhaust fan A day using zed Charitable trusts Migrating a Remix site to Vite Car privacy Where are the public geospatial companies? Previous and next links for my daily note in Obsidian Some new Browser APIs Remix fetcher and action gripes How I use Obsidian APIs and applications Read your lockfiles Searching for a browser Bookish is no longer Would LLMs democratizing coding be a pyrrhic victory? TIL about TypeScript and TSX TIL about requestSubmit Blogging under AI Linting a whole project in neovim, more advanced search with telescope Smart, kind, and eventual Running a quick Linux container What are taxes for Syncing iTerm light and dark mode with neovim Crypto's missing plateau of productivity The good NYC cycling paths The unspoken rules of React hooks Wanting to build a trip planner like Embark Bikeshare data notes If you use a domain for something popular, it will get squatted if you don't renew it Reddit is my Web Components reference point Thoughts on Arc Knip: good software for cleaning up TypeScript tech debt Searching for the perfect neovim setup Is there really a way to push back on the complexity of the web? TIL: Be careful with Postgres cascades On not using copilot A warning about tiktoken, BPE, and OpenAI models I want brands Maximization and buying stuff Travel internet notes Companies produce trash / people want trash Limits How is Filecoin supposed to work? There should be a neovim business Mimestream TIL: Vitest assert Surprising embeddings TIL with neverthrow An ast-grep rule to ban bad HTML nesting in React An ast-grep rule requiring you to await vitest expectations Talking about Placemark on the Software Sessions Podcast The web is already multiplayer Building an NPM module All hat, no cowboy Fastify decorateReply types End of Twitter Vitest with async fixtures and it.for/it.each Markwhen and Meridiem are great ThinkUp Tribute Everyone is new here LeaderKey.app is a very good launcher Blog micro-optimization NYC Primaries June 24 - don't rank Cuomo, vote for everyone else Putting an untrusted layer of chatbot AI between you and the internet is an obvious disaster waiting to happen The election Reverse timers Placemark updates Dependency thoughts Onyx Boox Go 7 as Instapaper single-tasker The confident mind works for running Placemark updates: Vite! Observable Notebooks 2.0 Personal canon Monad annoyance Using super Why D3('s DOM methods) are so verbose (another angle) On Interrobang with Dave DeGraw Revisiting "Rust is a hard way to make a web API" Pictures are famous for their humanness, and not for their pictureness First-run with agent skills from Anthropic What if people don't want to create things Fewer people should run marathons LLMs pivot to the aesthetics of thinking Effect notes: caching Hallucination city Placemark Sunday Effect notes: runtimes and logging Effect notes: flow and cancellation Effect notes: fn Effect notes: streams and such Sentry's distributed tracing causes missing parent spans in Honeycomb Maybe rich parsers are the way to introduce rich types Effect notes: tRPC Misc engineering truisms Graph layout Changing my mind What I haven't figured out Media diet Reactionary AI Centrism ROOTS - Return Old Online Things to your own Site Placemark & OSS Changelog Effect notes: PRs, progress, and joys How to contribute to open source