Portfolio

Pro bono work

GoodGesture

  • Firebase
  • Expo
  • React Native
  • JavaScript
  • Python

GoodGesture is a mutual-aid app connecting people with needs to people with resources. I joined with no prior experience in React Native, Expo, Firebase, or mobile development, and ramped up quickly to become lead developer. As a volunteer project, our contributors come and go with varying skill levels and availability, so a big part of my role is making sure tasks are approachable enough that people can pick them up, contribute meaningfully, and grow as developers along the way.

As lead developer, I've focused on making the codebase easier for a variety of contributors to work in safely. I built shared UI abstractions so screens stay visually consistent even as different people build different features, and created components that handle tricky cross-cutting concerns — like content sitting behind the status bar, or the on-screen keyboard opening — once, correctly, instead of leaving each contributor to solve them ad hoc. I also migrated the mobile app onto the web app's backend, and built a cross-service abstraction that reliably uploads photos to one service while keeping links in another from ever going stale. Alongside this, I optimized our CI pipeline to cut turnaround times, so contributors get faster feedback on their changes.

My open source projects

RepoYear

  • React
  • TypeScript
  • Rust

RepoYear loads the last year of a user’s contributions from GitHub’s GraphQL endpoint and displays them as a color-coded heat map. It can be set up to load contributions from a server to be displayed publicly (demo), or it can display a visitor’s own contributions (demo).

I built it to get hands-on with React, TypeScript, and Claude Code — I use Claude Code to get oriented in unfamiliar code faster, not as a substitute for writing and understanding it myself — and to try Rust’s Dropshot framework, written by Oxide Computer, who I was applying to at the time. Dropshot generates an OpenAPI spec from the server code, giving the client and server a compile-time type contract instead of trusting both sides to agree on the API shape by convention.

In practice, RepoYear’s API surface turned out too small for that to pay off, so I’m planning to rewrite the backend in TypeScript to share code directly between client and server and simplify the stack. I hit the same tradeoff writing a data-download script for the static demo: doing it in Rust meant duplicating logic that already existed in TypeScript, and compiling to WASM produced a much larger client bundle than just writing it in TypeScript to begin with.

claude-pr-review

  • GitHub Actions
  • Bash
  • TypeScript

I wrote this GitHub action to give Claude enough context to review and re-review pull requests well. Anthropic’s official action doesn’t tell Claude whether it has already reviewed a PR, so it repeats itself and misses discussion on the PR. My action uses custom PR rendering code (gh-pr-render) to give Claude the full discussion — inline comments, replies, and the diff — so it can pick up where it left off.

On every push, the action decides whether Claude needs to re-review at all: it caches the PR’s diff with line numbers stripped and compares it to the new diff, so a rebase that doesn’t touch the PR’s actual changes doesn’t trigger a wasted review. When something has changed, Claude gets a diff-of-diffs showing exactly what’s new. This trades some safety for practicality — skipping a review saves CI time and tokens.

It also batches Claude’s inline comments into a single grouped GitHub review, fixing a rough edge in Anthropic’s own action, where five comments show up as five separate reviews. Comments post through a small bundled CLI tool run via Bash rather than an MCP server — GitHub’s real bot token only reaches subprocesses that inherit the action’s environment, which a Bash call does and an MCP server doesn’t, so Bash lets Claude post as claude[bot] instead of generic github-actions[bot].

yanki

  • Python
  • async
  • JavaScript

Yanki is a Python command line tool made to simplify creating thousands of video flashcards from YouTube videos.

I initially wrote Yanki to help me study for an American Sign Language class by making it easier to produce flashcards for the popular Anki app. Naturally I wanted to share the flashcards with my classmates, but many of them had trouble using the Anki app. To give them access, I updated Yanki to produce static HTML that can be hosted on any web server (demo).

Internally, Yanki includes a simple parser for deck definition files, and uses ffmpeg to trim, crop, and/or slow video. Since ffmpeg is primarily single threaded when encoding media, Yanki uses Python’s asyncio to run multiple instance in parallel and significantly cut down the time needed to process thousands of flashcards.

git-status-vars

  • Rust
✔ better-timeout ○ ↑1 ~/projects/git-status-vars 8:58:24 PM ❯ eval $(time git-status-vars) 0.02s elapsed 80% CPU ✔ better-timeout ○ ↑1 ~/projects/git-status-vars 8:58:31 PM 0.1s ❯ echo $head_ref1_short ↑$head_ahead unstaged: $unstaged_count better-timeout ↑1 unstaged: 3

I configure my shell prompt to display information about the current git repository — the branch name, whether there are uncommitted changes, and so on. Loading that information with multiple calls to git added a noticeable delay after every command, including trivial ones like cd. Each git invocation has to start a new process and re-open the repository from scratch; git-status-vars opens the repo once with libgit2 and reads everything it needs directly, which in my own benchmarks makes it roughly 3x faster (about 8ms vs. 25ms for the equivalent git calls, though this varies a lot with repo size and disk speed). Since it runs on every prompt, a hang would be worse than missing information, so it times out after half a second and prints whatever it managed to gather rather than blocking the shell. The output can be evaled and the variables used directly in a prompt.

htmlize

  • Rust

Htmlize is a Rust crate that correctly encodes and decodes HTML text.

assert!(htmlize::escape_attribute("1 & 2 < 3") == "1 &amp; 2 &lt; 3"); assert!(htmlize::unescape("3 &times 4 &gt; 10") == "3 × 4 > 10");

Encoding entities is easy and fast, but decoding entities correctly has two major edge cases: not all entities have to end with a semicolon, e.g. &copy (©), and some entities contain other entities as a prefix, e.g. &copysr; (℗).

After significant experimentation, I chose two fast decode algorithms. They produce identical results, but switching between them trades faster compile times (with a perfect hash) for faster run times (with a massive match tree generated by my crate matchgen).

matchgen

  • Rust

Matchgen grew out of htmlize, where decoding HTML entities means matching the longest valid entity name out of 2,231 possibilities. A match tree that consumes the input byte by byte is much faster than testing candidates against a HashMap, since the compiler turns it directly into a decision tree instead of hashing and comparing whole strings — but writing one by hand isn’t realistic at this scale: htmlize’s tree comes out to 5,087 lines of Rust, even with a manual optimization that collapses two-branch matches into direct byte-slice patterns. Matchgen generates that tree from a simple list of byte strings and their replacements, so the source of truth stays small and readable while the generated code stays fast.

matchgen::TreeMatcher::new("pub fn decode", "u8") .doc("Decode basic HTML entities.") .add(b"&amp;", "b'&'") .add(b"&lt;", "b'<'") .add(b"&gt;", "b'>'") .add(b"&quot;", "b'\"'") .write_to_out_dir("decoder.rs")?;

read-doc

  • Rust

read-doc::module!("path.rs") reads documentation from another module. It’s useful for splitting code and documentation up into private logical submodules, then re-exporting everything in a public parent module.

//! # Overall module documentation #![doc = read_doc::module!("submodule1.rs", "submodule2.rs")] mod submodule1; mod submodule2; pub use submodule1::*; pub use submodule2::*;

puppet-golang

  • Puppet
  • Ruby

Simple yet flexible Go installations with Puppet.

class { 'golang': ensure => latest, }

armature

  • Ruby

Armature deploys Puppet environments and modules — one environment per branch of a control repo, with each environment’s modules installed per its Puppetfile. I built it as a much faster, much narrower alternative to r10k. It’s no longer actively maintained; r10k is fast enough for most people’s needs today.

❯ armature deploy-branches my-puppet-code.git '*'