{
    "componentChunkName": "component---src-pages-blog-mdx-slug-js",
    "path": "/blog/2026-07-19-misadventures-in-ruby-gem-development/",
    "result": {"data":{"mdx":{"frontmatter":{"title":"Misadventures in Ruby Gem Development","tags":["Ruby","C++","gems","ABI","open source"],"categories":[],"date":"July 19, 2026","image":null,"imageAlt":null},"id":"39d66bbb-72ff-5c7b-bb84-aef83f8b7400","body":"var _excluded = [\"components\"];\nfunction _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }\nfunction _objectWithoutProperties(e, t) { if (null == e) return {}; var o, r, i = _objectWithoutPropertiesLoose(e, t); if (Object.getOwnPropertySymbols) { var n = Object.getOwnPropertySymbols(e); for (r = 0; r < n.length; r++) o = n[r], -1 === t.indexOf(o) && {}.propertyIsEnumerable.call(e, o) && (i[o] = e[o]); } return i; }\nfunction _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (-1 !== e.indexOf(n)) continue; t[n] = r[n]; } return t; }\n/* @jsxRuntime classic */\n/* @jsx mdx */\n\nvar _frontmatter = {\n  \"title\": \"Misadventures in Ruby Gem Development\",\n  \"date\": \"2026-07-19T00:00:00.000Z\",\n  \"tags\": [\"Ruby\", \"C++\", \"gems\", \"ABI\", \"open source\"],\n  \"categories\": [],\n  \"draft\": false\n};\nvar layoutProps = {\n  _frontmatter: _frontmatter\n};\nvar MDXLayout = \"wrapper\";\nreturn function MDXContent(_ref) {\n  var components = _ref.components,\n    props = _objectWithoutProperties(_ref, _excluded);\n  return mdx(MDXLayout, _extends({}, layoutProps, props, {\n    components: components,\n    mdxType: \"MDXLayout\"\n  }), mdx(\"p\", null, \"In \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"/blog/2026-07-05-pico-phone-first-gem/\"\n  }, \"a previous post\"), \", I wrote about shipping my first Ruby gem: pico_phone, a Ruby extension wrapping Google's C++ \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://github.com/google/libphonenumber\"\n  }, \"libphonenumber\"), \" library.\"), mdx(\"p\", null, \"By pico_phone \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"0.6.1\"), \" I felt like I was in the coasting part of maintaining a gem. Features were in, the native binaries were building in CI, and I was just shipping small releases as I found things to add. Then I went looking for a completely unrelated fix and ended up finding out that \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"every native gem I had ever published was broken\"), \". Not \\\"broken on some machines.\\\" Broken for everyone, on every platform, since the very first one.\"), mdx(\"div\", {\n    \"style\": {\n      \"textAlign\": \"center\"\n    }\n  }, \"\\n  \", mdx(\"img\", {\n    parentName: \"div\",\n    \"src\": \"/misadventures-in-ruby-gem-development/this-is-fine.gif\",\n    \"alt\": \"This is fine dog sitting at a table in a room that's on fire\",\n    \"style\": {\n      \"maxWidth\": \"100%\"\n    }\n  })), mdx(\"hr\", null), mdx(\"h2\", null, \"It all started with a badge\"), mdx(\"p\", null, \"It started with something dumb: the RubyGems version badge in the README was showing an old version number. I chased that down and it turned out to be a stale GitHub camo cache, badge.fury.io's SVG URL doesn't change as the gem version changes, so GitHub's image proxy was just serving whatever it had cached from before. A cache-busting query param fixed it in five minutes.\"), mdx(\"p\", null, \"But fixing it meant opening the README, and opening the README meant noticing the CI matrix only tested Ruby up to 3.4. Ruby 4.0 had been out for a while at that point (it shipped on Ruby's 30th anniversary, skipping 3.5 entirely and going straight to 4.0 as a branding move, not a signal of some huge breaking change). I'd never once tested pico_phone against it, probably because I didn't have Ruby 4 installed in my machine. Seemed like an easy thing to knock out while I was already in there.\"), mdx(\"p\", null, \"Only it was not an easy thing to knock out.\"), mdx(\"h2\", null, \"Adding Ruby 4.0 Felt Like the Easy Part\"), mdx(\"p\", null, \"pico_phone ships a C++ extension built with \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://github.com/ruby-rice/rice\"\n  }, \"Rice\"), \", a header-only C++/Ruby binding library. The version I had locked, 4.5.0, predated Rice's own Ruby 4.0 support, which landed in 4.10.0. So step one was bumping Rice, then adding \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"4.0\"), \" to the CI matrix, then confirming the extension still built and the full spec suite still passed across every platform and build path I ship: arm64-darwin, x86_64-linux, aarch64-linux, both the dynamic build and the vendored static one. All green. I shipped it as its own PR and felt done.\"), mdx(\"p\", null, \"But I was not done. I just hadn't found the actual problem yet.\"), mdx(\"h2\", null, \"Wait, Do the Precompiled Gems Even Work on Ruby 4?\"), mdx(\"p\", null, \"Sitting there after the CI PR merged, I started wondering something that felt almost too basic to ask: had I actually verified that the precompiled binaries work for people on different Ruby versions, or was I just trusting Ruby's documented ABI promise and assuming it held? The gem was new enough that I didn't have any user reports to lean on either way, nobody had installed it on enough different setups yet for a problem to surface on its own. It felt like a slightly silly question given how early it was, but I couldn't stop thinking about it.\"), mdx(\"p\", null, \"My release workflow built exactly one binary per platform, always with Ruby 3.3, and shipped that same binary to anyone on Ruby 3.1 through 3.4. I'd never once tried to load that binary under a Ruby version it wasn't built with. So I built the extension under 3.3, the same way \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"release.yml\"), \" did, and tried to load it under Ruby 4.0:\"), mdx(\"div\", {\n    \"className\": \"gatsby-highlight\",\n    \"data-language\": \"text\"\n  }, mdx(\"pre\", {\n    parentName: \"div\",\n    \"className\": \"language-text\"\n  }, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-text\"\n  }, \"LoadError: linked to incompatible .../ruby/3.3.12/lib/libruby.3.3.dylib - pico_phone.bundle\"))), mdx(\"p\", null, \"Reasonable enough on its face. I figured this was just \\\"the ABI promise doesn't extend across the Ruby 4.0 boundary,\\\" which is documented and expected for a major bump. My plan was to build one binary per major series, 3.x and 4.x, each compiled against the oldest minor in that series, and pick the right one at require time. Clean fix, proportionate to the problem.\"), mdx(\"p\", null, \"Except it turned out not to be the actual bug at all.\"), mdx(\"h2\", null, \"The Real Bug Had Nothing to Do With Ruby 4\"), mdx(\"p\", null, \"While I was validating that plan, I tried something that should have been a non-issue: loading a binary built under Ruby 3.1 on Ruby 3.3. Same major series. Ruby's own ABI promise says that should just work.\"), mdx(\"div\", {\n    \"className\": \"gatsby-highlight\",\n    \"data-language\": \"text\"\n  }, mdx(\"pre\", {\n    parentName: \"div\",\n    \"className\": \"language-text\"\n  }, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-text\"\n  }, \"LoadError: linked to incompatible .../ruby/3.1.7/lib/libruby.3.1.dylib\"))), mdx(\"p\", null, \"It didn't. Same failure, same major series, no Ruby 4 involved at all. That's the moment I knew I'd been solving the wrong problem.\"), mdx(\"p\", null, \"I pulled up \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"otool -L\"), \" on the compiled extension to see what it actually linked against, and there it was: an absolute, machine-specific path to \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"libruby\"), \". Turns out Ruby builds configured with \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"--enable-shared\"), \", which is what \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"ruby/setup-ruby\"), \"'s prebuilt binaries use, what the official Docker Ruby images use, and what \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"mise\"), \"/\", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"ruby-build\"), \" default to, make \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"mkmf\"), \" hard-link every compiled extension against a build-specific \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"libruby.so\"), \" or \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \".dylib\"), \" through a Makefile variable called \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"LIBRUBYARG_SHARED\"), \". This is on top of \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"-Wl,-undefined,dynamic_lookup\"), \", which is already sufficient on its own for resolving Ruby symbols at load time. On macOS that dependency bakes in as a literal absolute path, something like \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"/Users/me/.local/share/mise/installs/ruby/3.3.12/lib/libruby.3.3.dylib\"), \", or wherever \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"ruby/setup-ruby\"), \" happened to install to on that day's CI runner. A path that cannot exist on any machine except the one that compiled the extension.\"), mdx(\"p\", null, \"I wanted to know how far this actually went, so I stopped testing local builds and downloaded the real, currently published \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"pico_phone-0.6.1-arm64-darwin.gem\"), \" straight from rubygems.org. Tried to load it on a Ruby it wasn't built with. Failed. Tried it on the exact Ruby version it was supposedly built for, just freshly installed somewhere else. Also failed, because the absolute-path dependency doesn't care about Ruby version at all, it only cares about the exact build machine and the exact path that machine used.\"), mdx(\"p\", null, \"Every precompiled native gem pico_phone has ever published has been broken, for every user, on every platform, regardless of Ruby version, going back to when native gem builds were first introduced at \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"0.2.0\"), \". Nobody's \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"gem install pico_phone\"), \" on a native platform ever actually used the precompiled binary, because RubyGems and Bundler prefer a matching native gem over the source gem by default. This had probably been silently causing install failures the entire time, and it's exactly the kind of bug nobody files an issue about, because it looks identical to a flaky network install and most people just retry or fall back without a second thought.\"), mdx(\"div\", {\n    \"style\": {\n      \"textAlign\": \"center\"\n    }\n  }, \"\\n  \", mdx(\"img\", {\n    parentName: \"div\",\n    \"src\": \"/misadventures-in-ruby-gem-development/oh-no.gif\",\n    \"alt\": \"Shocked, wide-eyed \\\"oh no\\\" reaction\",\n    \"style\": {\n      \"maxWidth\": \"100%\"\n    }\n  })), mdx(\"p\", null, \"The fix itself was small once I found it: strip \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"$LIBRUBYARG_SHARED\"), \" and \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"$LIBRUBYARG\"), \" to empty strings in \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"extconf.rb\"), \" right before \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"create_makefile\"), \". The \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"dynamic_lookup\"), \" flag alone is enough. That hard dependency was pure liability that had been sitting there since day one.\"), mdx(\"h2\", null, \"A Second Bug Hiding Behind the First\"), mdx(\"p\", null, \"With the linking fix in place, the Ruby-3.1-built binary now loaded fine under 3.3, 3.4, and 4.0. Loading isn't the same as working, though:\"), mdx(\"div\", {\n    \"className\": \"gatsby-highlight\",\n    \"data-language\": \"ruby\"\n  }, mdx(\"pre\", {\n    parentName: \"div\",\n    \"className\": \"language-ruby\"\n  }, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-ruby\"\n  }, mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token comment\"\n  }, \"# built under Ruby 3.1, run under Ruby 3.3:\"), \"\\nPicoPhone\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token double-colon punctuation\"\n  }, \"::\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token class-name\"\n  }, \"PhoneNumber\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \".\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token keyword\"\n  }, \"new\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \"(\"), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token string-literal\"\n  }, mdx(\"span\", {\n    parentName: \"span\",\n    \"className\": \"token string\"\n  }, \"\\\"4155552671\\\"\")), mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token punctuation\"\n  }, \")\"), \"\\n\", mdx(\"span\", {\n    parentName: \"code\",\n    \"className\": \"token comment\"\n  }, \"# => TypeError: no implicit conversion of nil into String\")))), mdx(\"p\", null, \"This one is a nastier class of bug than a \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"LoadError\"), \". A \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"LoadError\"), \" fails loud and immediate. This fails quietly, deep inside normal usage, and in a slightly different shape it could just as easily produce a wrong validation result instead of a crash.\"), mdx(\"p\", null, \"My working theory, and I still think it's right: Rice is header-only as of version 4. Some of Ruby's C API gets exposed through inline macros and functions that compile directly into the extension using whatever Ruby minor's headers happened to be present at build time, which bakes that minor's internal struct layout straight into the compiled binary. The \\\"stable across a major series\\\" promise Ruby makes covers the plain function-call C API. It was never a promise about inlined, header-level access patterns staying compatible too. A header-only binding library is exactly the kind of dependency most likely to lean on the part of the API that promise doesn't cover.\"), mdx(\"p\", null, \"That killed the \\\"one binary per major series\\\" plan entirely, separate from the linking fix. I went back to one binary per Ruby minor actually tested in CI, 3.1 through 3.4 and 4.0, selected by exact match only at require time, no cross-minor fallback. Build Ruby always equals run Ruby, by construction, so this whole class of bug can't happen. It's the same approach nokogiri and grpc already take for their native extensions. I'd always assumed that was extra caution on their part. Now I understand it's load-bearing.\"), mdx(\"h2\", null, \"Testing the Fix for Real\"), mdx(\"p\", null, \"None of the above got taken on faith at any point, and that discipline is the only reason I found the full scope of it instead of just patching the symptom in front of me:\"), mdx(\"ul\", null, mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"I didn't stop at \\\"the fix builds.\\\" I checked that a binary built on one Ruby actually loads \", mdx(\"em\", {\n    parentName: \"li\"\n  }, \"and produces correct output\"), \" on a genuinely different Ruby, installed from a real packaged \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \".gem\"), \" via \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"gem install --local\"), \", not just required directly out of a build directory.\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"I didn't assume the shipped gem was fine because it had been out for weeks with no complaints. I downloaded the real artifact from rubygems.org and tried to break it, which is the only reason I found out the bug covered every version back to \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"0.2.0\"), \" instead of just assuming it started with 0.6.x.\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"I didn't trust that the fix would work in CI just because it worked locally. I added a \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"workflow_dispatch\"), \" trigger specifically so I could run the real \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"release.yml\"), \" native-build jobs on the branch before merging, then downloaded those actual build artifacts and exercised them (parse, validate, geocode, carrier lookup, timezone lookup, not just \\\"did require succeed\\\") across all three platforms through Docker before I'd call the PR mergeable.\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"I hit a false alarm along the way. Linux verification first failed with \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"libicui18n.so.74: cannot open shared object file\"), \", which looked like a fresh bug. It wasn't. \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"extconf.rb\"), \"'s dynamic ICU linking on Linux assumes an Ubuntu 24.04 runtime, and the Docker Ruby images I was using for the quick local test are Debian-based with a different ICU version. Rebuilding the test against an actual \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"ubuntu:24.04\"), \" base image with \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"libicu74\"), \" installed passed cleanly. Worth remembering: not every failure you hit while verifying a fix was caused by the fix. Isolate before you assume.\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"I also caught a bug in the fix's own loader logic before it shipped. An early draft picked buckets by \\\"closest minor at or below the running minor,\\\" which would have silently excluded the whole bucket for anyone running a minor \", mdx(\"em\", {\n    parentName: \"li\"\n  }, \"older\"), \" than what got built, say Ruby 3.1 with only a 3.4-built bucket available. That only surfaced because I explicitly asked what happens to someone on an older Ruby than what I build for, not something the happy-path tests would ever have caught.\")), mdx(\"h2\", null, \"Yanking Every Broken Gem I'd Ever Shipped\"), mdx(\"div\", {\n    \"style\": {\n      \"textAlign\": \"center\"\n    }\n  }, \"\\n  \", mdx(\"img\", {\n    parentName: \"div\",\n    \"src\": \"/misadventures-in-ruby-gem-development/nope.gif\",\n    \"alt\": \"A person hits a big red NOPE button that fires a ray wiping everything out\",\n    \"style\": {\n      \"maxWidth\": \"100%\"\n    }\n  })), mdx(\"p\", null, \"Once the fix was live as \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"0.6.2\"), \", the obvious next question was what to do about every prior release. RubyGems doesn't let you overwrite a published version-and-platform combination, by design, so nobody can silently swap a version out from under people who already trust it. There was never a way to fix \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"0.6.1\"), \"'s native gem in place. The only path forward was a new version number.\"), mdx(\"p\", null, \"So I yanked the broken native platform variants, arm64-darwin, x86_64-linux, aarch64-linux, across every affected version: \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"0.2.0\"), \" through \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"0.6.1\"), \", 23 version-and-platform combinations in total. I left each version's plain \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"ruby\"), \" source gem untouched. Yanking a version-and-platform doesn't remove the whole version; \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"gem install pico_phone -v 0.6.1\"), \" still works after the yank, it just falls back to compiling from source instead of grabbing a binary that can't load. \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"0.1.0\"), \" was unaffected since native gems didn't exist yet at that version.\"), mdx(\"h2\", null, \"What I'd Tell Past Me\"), mdx(\"p\", null, \"The badge fix took five minutes. What it led to took a few hours and involved yanking every native gem I'd published since day one, all of them in good faith, believing they worked. A stale badge cache turning into that is a ridiculous size mismatch, and it's honestly the part that sticks with me the most.\"), mdx(\"p\", null, \"But strip away the C++ and the ABI details and what's actually underneath all of it is the oldest lesson in the book: it worked on my machine. Every developer hears that line as a joke at some point, usually about someone else's bug. Mine was the literal version of it, not the metaphor. The binary didn't just happen to work in an environment similar to my laptop, it was hard-linked to a path that could only ever exist on my laptop. I'd built something that was never going to work anywhere else, and the reason nobody noticed (besided the fact that this gem is so new it probably doesn't have many adopters yet) is that \\\"gem install\\\" quietly falling back to compiling from source looks exactly like success.\"), mdx(\"p\", null, \"If you ship native gems and you've only ever built and tested on the machines that also compiled the binary, it's worth running \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"otool -L\"), \" or \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"ldd\"), \" on the actual compiled extension and looking at what it links against. Not what you assume it links against. What it actually does. \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"-Wl,-undefined,dynamic_lookup\"), \" is already doing the job you think \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"LIBRUBYARG_SHARED\"), \" is doing, and that second one is just quietly baking your build machine's filesystem into a binary you're about to hand to strangers, which is \\\"it works on my machine\\\" turned into a permanent, shippable artifact.\"), mdx(\"p\", null, \"pico_phone \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"0.6.2\"), \" is out now with real per-minor native gems and no absolute paths anywhere in them. \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"gem install pico_phone\"), \" should actually mean what it says this time, on a machine that isn't mine.\"));\n}\n;\nMDXContent.isMDXComponent = true;","timeToRead":8,"slug":"2026-07-19-misadventures-in-ruby-gem-development/"},"allMdx":{"edges":[{"next":{"slug":"2013-03-17-silicon-valley-devfest/","frontmatter":{"title":"Silicon Valley DevFest"},"id":"edcdad98-0f1c-5f08-b9bf-ef2584a6784d"},"previous":null,"node":{"id":"3378703f-653c-58fd-a11e-7adc675fb79e","frontmatter":{"title":"Makings of a Pythonista"},"slug":"2012-11-21-makings-of-a-pythonista"}},{"next":{"slug":"2015-06-14-in-search-of-a-faster-query","frontmatter":{"title":"In search of a faster query"},"id":"c6497654-5ee0-5b14-b735-e08bbbc6a4bf"},"previous":{"frontmatter":{"title":"Makings of a Pythonista"},"slug":"2012-11-21-makings-of-a-pythonista"},"node":{"id":"edcdad98-0f1c-5f08-b9bf-ef2584a6784d","frontmatter":{"title":"Silicon Valley DevFest"},"slug":"2013-03-17-silicon-valley-devfest/"}},{"next":{"slug":"2015-11-01-diary-of-a-dev-joys-of-building","frontmatter":{"title":"Diary of a Junior Dev: The joys of building"},"id":"ca6c84a6-3aea-5278-bf26-267d43acb227"},"previous":{"frontmatter":{"title":"Silicon Valley DevFest"},"slug":"2013-03-17-silicon-valley-devfest/"},"node":{"id":"c6497654-5ee0-5b14-b735-e08bbbc6a4bf","frontmatter":{"title":"In search of a faster query"},"slug":"2015-06-14-in-search-of-a-faster-query"}},{"next":{"slug":"2016-12-24-working-with-d3-based-chart-library/","frontmatter":{"title":"TIL: Working with a D3 based chart library"},"id":"776dcc9f-2ac4-511a-a9c6-982c226e0478"},"previous":{"frontmatter":{"title":"In search of a faster query"},"slug":"2015-06-14-in-search-of-a-faster-query"},"node":{"id":"ca6c84a6-3aea-5278-bf26-267d43acb227","frontmatter":{"title":"Diary of a Junior Dev: The joys of building"},"slug":"2015-11-01-diary-of-a-dev-joys-of-building"}},{"next":{"slug":"2017-03-15-react-conf-2017/","frontmatter":{"title":"React Conf 2017"},"id":"97c0196c-f0cd-5d6a-b2b9-fd3c6f1f1baa"},"previous":{"frontmatter":{"title":"Diary of a Junior Dev: The joys of building"},"slug":"2015-11-01-diary-of-a-dev-joys-of-building"},"node":{"id":"776dcc9f-2ac4-511a-a9c6-982c226e0478","frontmatter":{"title":"TIL: Working with a D3 based chart library"},"slug":"2016-12-24-working-with-d3-based-chart-library/"}},{"next":{"slug":"2017-04-12-the-initial-state-gotcha","frontmatter":{"title":"The Initial State Gotcha"},"id":"82041731-cb81-571d-958c-00f57d99b927"},"previous":{"frontmatter":{"title":"TIL: Working with a D3 based chart library"},"slug":"2016-12-24-working-with-d3-based-chart-library/"},"node":{"id":"97c0196c-f0cd-5d6a-b2b9-fd3c6f1f1baa","frontmatter":{"title":"React Conf 2017"},"slug":"2017-03-15-react-conf-2017/"}},{"next":{"slug":"2017-05-13-blue-socks-at-orange/","frontmatter":{"title":"Blue Socks Spotted at Orange"},"id":"893cf729-88c0-5706-bcb8-d14f75a30f2b"},"previous":{"frontmatter":{"title":"React Conf 2017"},"slug":"2017-03-15-react-conf-2017/"},"node":{"id":"82041731-cb81-571d-958c-00f57d99b927","frontmatter":{"title":"The Initial State Gotcha"},"slug":"2017-04-12-the-initial-state-gotcha"}},{"next":{"slug":"once-upon-a-blog/","frontmatter":{"title":"Once upon a Blog"},"id":"db5caf55-934b-5de1-aa6b-5a2078e4f869"},"previous":{"frontmatter":{"title":"The Initial State Gotcha"},"slug":"2017-04-12-the-initial-state-gotcha"},"node":{"id":"893cf729-88c0-5706-bcb8-d14f75a30f2b","frontmatter":{"title":"Blue Socks Spotted at Orange"},"slug":"2017-05-13-blue-socks-at-orange/"}},{"next":{"slug":"story-of-a-laptop-upgrade","frontmatter":{"title":"Story of a laptop upgrade"},"id":"91dabe95-0227-5d01-b1d1-f10959c5d43a"},"previous":{"frontmatter":{"title":"Blue Socks Spotted at Orange"},"slug":"2017-05-13-blue-socks-at-orange/"},"node":{"id":"db5caf55-934b-5de1-aa6b-5a2078e4f869","frontmatter":{"title":"Once upon a Blog"},"slug":"once-upon-a-blog/"}},{"next":{"slug":"the-code-we-write-today/","frontmatter":{"title":"The code we write today"},"id":"08584458-be98-53c2-8cf3-9fa51060e04b"},"previous":{"frontmatter":{"title":"Once upon a Blog"},"slug":"once-upon-a-blog/"},"node":{"id":"91dabe95-0227-5d01-b1d1-f10959c5d43a","frontmatter":{"title":"Story of a laptop upgrade"},"slug":"story-of-a-laptop-upgrade"}},{"next":{"slug":"rails-on-docker/","frontmatter":{"title":"Rails on Docker"},"id":"5e92911a-24f7-562d-8ecd-9a7391960489"},"previous":{"frontmatter":{"title":"Story of a laptop upgrade"},"slug":"story-of-a-laptop-upgrade"},"node":{"id":"08584458-be98-53c2-8cf3-9fa51060e04b","frontmatter":{"title":"The code we write today"},"slug":"the-code-we-write-today/"}},{"next":{"slug":"rails-on-docker-part-two/","frontmatter":{"title":"Rails on Docker Part Two"},"id":"534e725d-71be-5e6f-ace7-75db51185701"},"previous":{"frontmatter":{"title":"The code we write today"},"slug":"the-code-we-write-today/"},"node":{"id":"5e92911a-24f7-562d-8ecd-9a7391960489","frontmatter":{"title":"Rails on Docker"},"slug":"rails-on-docker/"}},{"next":{"slug":"super-powers-for-active-record-relation","frontmatter":{"title":"Super Powers for ActiveRecord::Relation"},"id":"3d5d8b08-cf43-57be-8c43-aad95e2dfd06"},"previous":{"frontmatter":{"title":"Rails on Docker"},"slug":"rails-on-docker/"},"node":{"id":"534e725d-71be-5e6f-ace7-75db51185701","frontmatter":{"title":"Rails on Docker Part Two"},"slug":"rails-on-docker-part-two/"}},{"next":{"slug":"2026-05-30-outsmarting-github-copilot","frontmatter":{"title":"I Tried to Outsmart Copilot. It Made Me a Better Developer."},"id":"8bc48193-2ba7-5463-9edb-0f5e3a910bb1"},"previous":{"frontmatter":{"title":"Rails on Docker Part Two"},"slug":"rails-on-docker-part-two/"},"node":{"id":"3d5d8b08-cf43-57be-8c43-aad95e2dfd06","frontmatter":{"title":"Super Powers for ActiveRecord::Relation"},"slug":"super-powers-for-active-record-relation"}},{"next":{"slug":"2026-06-06-first-steps-with-mistral","frontmatter":{"title":"I Started a Mistral Tutorial. It Ended with My Voice Speaking French."},"id":"cb7d26e3-c28a-5c0a-a659-83f90d04bd15"},"previous":{"frontmatter":{"title":"Super Powers for ActiveRecord::Relation"},"slug":"super-powers-for-active-record-relation"},"node":{"id":"8bc48193-2ba7-5463-9edb-0f5e3a910bb1","frontmatter":{"title":"I Tried to Outsmart Copilot. It Made Me a Better Developer."},"slug":"2026-05-30-outsmarting-github-copilot"}},{"next":{"slug":"2026-06-12-building-the-subtitle-tool/","frontmatter":{"title":"Building a Live Subtitle Tool With Mistral"},"id":"fb5e6f8e-7da3-506b-b50c-869e8bdde883"},"previous":{"frontmatter":{"title":"I Tried to Outsmart Copilot. It Made Me a Better Developer."},"slug":"2026-05-30-outsmarting-github-copilot"},"node":{"id":"cb7d26e3-c28a-5c0a-a659-83f90d04bd15","frontmatter":{"title":"I Started a Mistral Tutorial. It Ended with My Voice Speaking French."},"slug":"2026-06-06-first-steps-with-mistral"}},{"next":{"slug":"2026-06-19-building-a-voice-assistant/","frontmatter":{"title":"Teaching a Voice Assistant to Speak Spanish Like Me"},"id":"182b1abf-7702-5cb7-8de9-ba26e74b811f"},"previous":{"frontmatter":{"title":"I Started a Mistral Tutorial. It Ended with My Voice Speaking French."},"slug":"2026-06-06-first-steps-with-mistral"},"node":{"id":"fb5e6f8e-7da3-506b-b50c-869e8bdde883","frontmatter":{"title":"Building a Live Subtitle Tool With Mistral"},"slug":"2026-06-12-building-the-subtitle-tool/"}},{"next":{"slug":"2026-06-26-knitting-assistant-pattern-ingestion/","frontmatter":{"title":"Building a Knitting Pattern Assistant, Part 1: Getting the PDF In"},"id":"85983a41-a504-57c0-8385-5f6327fb821c"},"previous":{"frontmatter":{"title":"Building a Live Subtitle Tool With Mistral"},"slug":"2026-06-12-building-the-subtitle-tool/"},"node":{"id":"182b1abf-7702-5cb7-8de9-ba26e74b811f","frontmatter":{"title":"Teaching a Voice Assistant to Speak Spanish Like Me"},"slug":"2026-06-19-building-a-voice-assistant/"}},{"next":{"slug":"2026-07-05-pico-phone-first-gem","frontmatter":{"title":"I Just Published My First Ruby Gem!"},"id":"658785b5-44c0-5ef2-927a-cc321bf54f48"},"previous":{"frontmatter":{"title":"Teaching a Voice Assistant to Speak Spanish Like Me"},"slug":"2026-06-19-building-a-voice-assistant/"},"node":{"id":"85983a41-a504-57c0-8385-5f6327fb821c","frontmatter":{"title":"Building a Knitting Pattern Assistant, Part 1: Getting the PDF In"},"slug":"2026-06-26-knitting-assistant-pattern-ingestion/"}},{"next":{"slug":"2026-07-10-knitting-assistant-search-and-chat/","frontmatter":{"title":"Building a Knitting Pattern Assistant, Part 2: From Searchable Library to Grounded Chat"},"id":"300de738-faf9-53bb-8ce4-d2a80544605c"},"previous":{"frontmatter":{"title":"Building a Knitting Pattern Assistant, Part 1: Getting the PDF In"},"slug":"2026-06-26-knitting-assistant-pattern-ingestion/"},"node":{"id":"658785b5-44c0-5ef2-927a-cc321bf54f48","frontmatter":{"title":"I Just Published My First Ruby Gem!"},"slug":"2026-07-05-pico-phone-first-gem"}},{"next":{"slug":"2026-07-19-misadventures-in-ruby-gem-development/","frontmatter":{"title":"Misadventures in Ruby Gem Development"},"id":"39d66bbb-72ff-5c7b-bb84-aef83f8b7400"},"previous":{"frontmatter":{"title":"I Just Published My First Ruby Gem!"},"slug":"2026-07-05-pico-phone-first-gem"},"node":{"id":"300de738-faf9-53bb-8ce4-d2a80544605c","frontmatter":{"title":"Building a Knitting Pattern Assistant, Part 2: From Searchable Library to Grounded Chat"},"slug":"2026-07-10-knitting-assistant-search-and-chat/"}},{"next":null,"previous":{"frontmatter":{"title":"Building a Knitting Pattern Assistant, Part 2: From Searchable Library to Grounded Chat"},"slug":"2026-07-10-knitting-assistant-search-and-chat/"},"node":{"id":"39d66bbb-72ff-5c7b-bb84-aef83f8b7400","frontmatter":{"title":"Misadventures in Ruby Gem Development"},"slug":"2026-07-19-misadventures-in-ruby-gem-development/"}}]}},"pageContext":{"id":"39d66bbb-72ff-5c7b-bb84-aef83f8b7400","slug":"2026-07-19-misadventures-in-ruby-gem-development/","__params":{"slug":"2026-07-19-misadventures-in-ruby-gem-development"}}},
    "staticQueryHashes": ["2201243204"]}