<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://collinsmuriuki.xyz/feed.xml" rel="self" type="application/atom+xml" /><link href="https://collinsmuriuki.xyz/" rel="alternate" type="text/html" /><updated>2026-05-19T16:35:41+03:00</updated><id>https://collinsmuriuki.xyz/feed.xml</id><title type="html">Collins Muriuki</title><subtitle>Understanding the universe and explaining it to a three year old.</subtitle><entry><title type="html">How the odnelazm-ingest pipeline works</title><link href="https://collinsmuriuki.xyz/how-the-odnelazm-ingest-pipeline-works/" rel="alternate" type="text/html" title="How the odnelazm-ingest pipeline works" /><published>2026-05-19T12:00:00+03:00</published><updated>2026-05-19T12:00:00+03:00</updated><id>https://collinsmuriuki.xyz/how-the-odnelazm-ingest-pipeline-works</id><content type="html" xml:base="https://collinsmuriuki.xyz/how-the-odnelazm-ingest-pipeline-works/"><![CDATA[<p><a href="https://bunge-hub.mwananchi.tech">Bunge Hub</a> is currently in preview. It exposes Kenya’s parliamentary record as structured, searchable data; every bill, debate, and member contribution from the 13th Parliament. This post walks through the pipeline that builds and maintains the database behind it.</p>

<p>The pipeline lives in <a href="https://github.com/mwananchi-tech/odnelazm/tree/main/crates/odnelazm-ingest"><code class="language-plaintext highlighter-rouge">odnelazm-ingest</code></a>, a Rust crate that handles scraping, extraction, storage, and AI enrichment.</p>

<h2 id="the-source">The source</h2>

<p>Kenya’s Hansard is published through mzalendo.com as structured HTML. Each sitting is a flat document of sections, subsections, and contributions with speaker names. The <a href="https://github.com/mwananchi-tech/odnelazm"><code class="language-plaintext highlighter-rouge">odnelazm</code></a> core library parses these into typed Rust structs. <code class="language-plaintext highlighter-rouge">odnelazm-ingest</code> takes those structs and writes them to PostgreSQL.</p>

<h2 id="the-pipeline">The pipeline</h2>

<p>Everything runs through <code class="language-plaintext highlighter-rouge">IngestPipeline</code>, which is generic over any <code class="language-plaintext highlighter-rouge">DataStore</code> implementation. It holds a scraper, an optional embedder, an optional summarizer, and an optional metrics sink.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">pub</span> <span class="k">struct</span> <span class="n">IngestPipeline</span><span class="o">&lt;</span><span class="n">S</span><span class="p">:</span> <span class="n">DataStore</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="n">scraper</span><span class="p">:</span> <span class="n">HansardScraper</span><span class="p">,</span>
    <span class="n">store</span><span class="p">:</span> <span class="n">S</span><span class="p">,</span>
    <span class="n">embedder</span><span class="p">:</span> <span class="nb">Option</span><span class="o">&lt;</span><span class="nb">Arc</span><span class="o">&lt;</span><span class="k">dyn</span> <span class="n">Embedder</span><span class="o">&gt;&gt;</span><span class="p">,</span>
    <span class="k">pub</span> <span class="n">summarizer</span><span class="p">:</span> <span class="nb">Option</span><span class="o">&lt;</span><span class="nb">Arc</span><span class="o">&lt;</span><span class="k">dyn</span> <span class="n">Summarizer</span><span class="o">&gt;&gt;</span><span class="p">,</span>
    <span class="k">pub</span> <span class="n">metrics</span><span class="p">:</span> <span class="nb">Option</span><span class="o">&lt;</span><span class="nb">Arc</span><span class="o">&lt;</span><span class="k">dyn</span> <span class="n">MetricsSink</span><span class="o">&gt;&gt;</span><span class="p">,</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The public surface is small: <code class="language-plaintext highlighter-rouge">ingest_all_sittings</code>, <code class="language-plaintext highlighter-rouge">ingest_sittings_in_range</code>, <code class="language-plaintext highlighter-rouge">ingest_members</code>, and <code class="language-plaintext highlighter-rouge">ingest_member_profiles</code>. Everything else is private.</p>

<h2 id="what-happens-when-a-sitting-is-ingested">What happens when a sitting is ingested</h2>

<p><code class="language-plaintext highlighter-rouge">ingest_sitting</code> is the core unit of work. For each sitting:</p>

<ol>
  <li>
    <p><strong>Sitting</strong>: upserted by URL. The full transcript is stored as JSONB in <code class="language-plaintext highlighter-rouge">raw_json</code>. On conflict, the raw JSON is overwritten but existing summaries are preserved.</p>
  </li>
  <li>
    <p><strong>Speakers</strong>: every distinct contributor name becomes a <code class="language-plaintext highlighter-rouge">speakers</code> row. Speaker names are extracted as-is from the HTML, which means the same person can appear under multiple names across sittings (“Hon. Kaluma”, “Peter Kaluma”, “Hon. Peter Kaluma”).</p>
  </li>
  <li>
    <p><strong>Bills</strong>: the bill extractor scans section headings for patterns ending in “BILL” or “ACT”. Each match becomes a <code class="language-plaintext highlighter-rouge">bill_mentions</code> row joining a <code class="language-plaintext highlighter-rouge">bills</code> record to the sitting, with stage detected from the heading or contribution text. Each speaker who contributed to that section is linked via <code class="language-plaintext highlighter-rouge">bill_mention_speakers</code>, with their full contribution text stored for enrichment.</p>
  </li>
  <li>
    <p><strong>Topics</strong>: “topic” is a generic term used across the codebase for any non-bill discussion item: questions and statements, notices of motion, and communications from the chair. These are identified by section type rather than heading pattern. Each topic is linked to its contributors via <code class="language-plaintext highlighter-rouge">topic_speakers</code>, again with full contribution text stored for enrichment.</p>
  </li>
</ol>

<p>Because bills are linked across sittings rather than stored per-sitting, this schema makes it possible to reconstruct a bill’s full legislative journey: every sitting it appeared in, the stage at each appearance, and everyone who spoke. Bunge Hub surfaces this as an interactive timeline on each bill page. You can trace a bill across sittings, seeing who spoke at each appearance and reading AI-generated summaries of each debate. Stages are inferred from section headings in the transcript so they are a close approximation rather than a guaranteed reflection of the formal legislative record.</p>

<h2 id="the-linking-problem">The linking problem</h2>

<p>After sittings are ingested, member profiles are imported from mzalendo’s member performance tracker. This gives us canonical names, constituencies, parties, and profile URLs.</p>

<p>Linking speakers to members is a three-step pass:</p>

<ol>
  <li>
    <p><strong>URL matching</strong>: when mzalendo includes a profile link on a contribution, exact URL match is used. Most reliable. This accounts for 1,871 of 3,021 speaker records in the current dataset.</p>
  </li>
  <li>
    <p><strong>Fuzzy name matching</strong>: for speakers without a URL, <code class="language-plaintext highlighter-rouge">pg_trgm</code> trigram similarity is computed against all member names via a <code class="language-plaintext highlighter-rouge">match_member</code> SQL function. Matches above a 0.45 score are accepted.</p>
  </li>
  <li>
    <p><strong>Role-based matching</strong>: presiding officers like “Hon. Speaker” and “Hon. Deputy Speaker” don’t fuzzy-match well because the names are too generic. These are resolved by looking up members with <code class="language-plaintext highlighter-rouge">role = 'Speaker'</code> or <code class="language-plaintext highlighter-rouge">role ILIKE '%deputy speaker%'</code> scoped to the relevant house and parliament.</p>
  </li>
</ol>

<p>Steps 2 and 3 combined account for a further 722 matches. 428 speaker records remain unmatched. Most are extraction noise where speech content leaked into the speaker name field during parsing. Others are genuinely ambiguous: presiding officers recorded under role-only labels like “Hon. Chairlady” that are too generic to resolve to a specific member without additional context.</p>

<p>The <code class="language-plaintext highlighter-rouge">match_member</code> function drives step 2. It cleans the raw speaker name, then scores it against every member using both <code class="language-plaintext highlighter-rouge">word_similarity</code> and <code class="language-plaintext highlighter-rouge">similarity</code>, taking whichever is higher:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="k">OR</span> <span class="k">REPLACE</span> <span class="k">FUNCTION</span> <span class="n">match_member</span><span class="p">(</span>
    <span class="n">query_name</span> <span class="nb">TEXT</span><span class="p">,</span>
    <span class="n">min_score</span>  <span class="nb">FLOAT</span> <span class="k">DEFAULT</span> <span class="mi">0</span><span class="p">.</span><span class="mi">3</span>
<span class="p">)</span>
<span class="k">RETURNS</span> <span class="k">TABLE</span> <span class="p">(</span><span class="n">id</span> <span class="n">UUID</span><span class="p">,</span> <span class="n">name</span> <span class="nb">TEXT</span><span class="p">,</span> <span class="n">url</span> <span class="nb">TEXT</span><span class="p">,</span> <span class="n">house</span> <span class="nb">TEXT</span><span class="p">,</span> <span class="n">constituency</span> <span class="nb">TEXT</span><span class="p">,</span> <span class="n">score</span> <span class="nb">FLOAT</span><span class="p">)</span>
<span class="k">LANGUAGE</span> <span class="k">sql</span> <span class="k">STABLE</span> <span class="k">AS</span> <span class="err">$$</span>
    <span class="k">WITH</span> <span class="n">cleaned</span> <span class="k">AS</span> <span class="p">(</span>
        <span class="k">SELECT</span> <span class="n">clean_speaker_name</span><span class="p">(</span><span class="n">query_name</span><span class="p">)</span> <span class="k">AS</span> <span class="n">cn</span>
    <span class="p">)</span>
    <span class="k">SELECT</span>
        <span class="n">m</span><span class="p">.</span><span class="n">id</span><span class="p">,</span> <span class="n">m</span><span class="p">.</span><span class="n">name</span><span class="p">,</span> <span class="n">m</span><span class="p">.</span><span class="n">url</span><span class="p">,</span> <span class="n">m</span><span class="p">.</span><span class="n">house</span><span class="p">,</span> <span class="n">m</span><span class="p">.</span><span class="n">constituency</span><span class="p">,</span>
        <span class="n">greatest</span><span class="p">(</span>
            <span class="n">word_similarity</span><span class="p">(</span><span class="k">c</span><span class="p">.</span><span class="n">cn</span><span class="p">,</span> <span class="n">m</span><span class="p">.</span><span class="n">name</span><span class="p">),</span>
            <span class="n">similarity</span><span class="p">(</span><span class="k">c</span><span class="p">.</span><span class="n">cn</span><span class="p">,</span> <span class="n">m</span><span class="p">.</span><span class="n">name</span><span class="p">)</span>
        <span class="p">)::</span><span class="nb">FLOAT</span> <span class="k">AS</span> <span class="n">score</span>
    <span class="k">FROM</span> <span class="n">members</span> <span class="n">m</span><span class="p">,</span> <span class="n">cleaned</span> <span class="k">c</span>
    <span class="k">WHERE</span> <span class="n">greatest</span><span class="p">(</span>
              <span class="n">word_similarity</span><span class="p">(</span><span class="k">c</span><span class="p">.</span><span class="n">cn</span><span class="p">,</span> <span class="n">m</span><span class="p">.</span><span class="n">name</span><span class="p">),</span>
              <span class="n">similarity</span><span class="p">(</span><span class="k">c</span><span class="p">.</span><span class="n">cn</span><span class="p">,</span> <span class="n">m</span><span class="p">.</span><span class="n">name</span><span class="p">)</span>
          <span class="p">)</span> <span class="o">&gt;=</span> <span class="n">min_score</span>
    <span class="k">ORDER</span> <span class="k">BY</span> <span class="n">score</span> <span class="k">DESC</span>
    <span class="k">LIMIT</span> <span class="mi">5</span>
<span class="err">$$</span><span class="p">;</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">word_similarity</code> is the key function. It checks whether the cleaned name appears as a substring of the member name, which handles the common case where the Hansard records only part of a member’s full name. “Kimani Ichung’wah” scores 0.52 against “Anthony Kimani Ichung’wah” via <code class="language-plaintext highlighter-rouge">word_similarity</code> even though <code class="language-plaintext highlighter-rouge">similarity</code> alone would score lower. <code class="language-plaintext highlighter-rouge">link_speakers_by_name</code> calls <code class="language-plaintext highlighter-rouge">match_member</code> for each unlinked speaker and takes the top result.</p>

<p>The <code class="language-plaintext highlighter-rouge">clean_speaker_name</code> SQL function strips honorifics, titles, constituency parentheticals, and presiding-role prefixes before matching. The Hansard produces a surprising variety of name formats:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- Constituency and party stripped</span>
<span class="nv">"Hon. Kimani Ichung'wah (Kikuyu, UDA)"</span>        <span class="err">→</span> <span class="nv">"Kimani Ichung'wah"</span>

<span class="c1">-- Military/professional title stripped</span>
<span class="nv">"Hon. (Capt.) Ronald Karauri (Kasarani, Ind)"</span> <span class="err">→</span> <span class="nv">"Ronald Karauri"</span>

<span class="c1">-- Temporary Speaker with nested honorific and title</span>
<span class="nv">"The Temporary Speaker (Hon. (Dr) Rachael Nyamai)"</span> <span class="err">→</span> <span class="nv">"Rachael Nyamai"</span>

<span class="c1">-- Hon. prefix instead of The</span>
<span class="nv">"Hon. Temporary Speaker (Hon. Farah Maalim)"</span>  <span class="err">→</span> <span class="nv">"Farah Maalim"</span>

<span class="c1">-- Unclosed parenthesis from malformed HTML</span>
<span class="nv">"Hon. Kimani Ichung'wah (Kikuyu, UDA"</span>         <span class="err">→</span> <span class="nv">"Kimani Ichung'wah"</span>
</code></pre></div></div>

<p>After cleaning, trigram similarity is computed against all member names. “Kimani Ichung’wah” matches <code class="language-plaintext highlighter-rouge">Anthony Kimani Ichung'wah</code> at 0.52, well above the 0.45 threshold. Presiding officer patterns like “Hon. Speaker” and “The Deputy Speaker” produce false positives with fuzzy matching, so those are resolved separately via the role-based pass.</p>

<h2 id="ai-enrichment">AI enrichment</h2>

<p>Summaries are generated separately from ingestion, via the <code class="language-plaintext highlighter-rouge">enrich</code> subcommand of <code class="language-plaintext highlighter-rouge">odnelazm-pipeline</code>. The pipeline supports six enrichment targets:</p>

<table>
  <thead>
    <tr>
      <th>Target</th>
      <th>What it generates</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">bill-mentions</code></td>
      <td>Summary of each bill’s appearance in a sitting, using the full transcript as context</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">bill-journeys</code></td>
      <td>Narrative summary of a bill’s full legislative journey across all sittings</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">bill-speakers</code></td>
      <td>Per-speaker summary of their contributions to a specific bill debate</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">topics</code></td>
      <td>Summary of a topic’s appearance in a sitting</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">topic-speakers</code></td>
      <td>Per-speaker summary of their contributions to a topic</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">sittings</code></td>
      <td>Full structured summary of a sitting</td>
    </tr>
  </tbody>
</table>

<p>All enrichment is done locally using open-source models via LM Studio. The model used to generate each summary is stored alongside it so it can be attributed in the UI and re-enriched with a better model later.</p>

<p>The current model is <strong>Qwen3.5 9B</strong> (<code class="language-plaintext highlighter-rouge">qwen/qwen3.5-9b</code>). I chose it for two reasons: its 262K native context window comfortably holds a full sitting transcript in a single call, and its built-in chain-of-thought reasoning produces noticeably more nuanced summaries than models that don’t reason before generating.</p>

<p>The tradeoff is inference time. At 65K context, Qwen spends the bulk of its budget on reasoning before producing the actual answer. From a recent run:</p>

<ul>
  <li><strong>~14 tokens/second</strong> on Apple M-series (CPU inference)</li>
  <li><strong>~4 seconds</strong> time to first token</li>
  <li><strong>~18,000 input tokens</strong> per bill appearance summary</li>
  <li><strong>~5,000 reasoning tokens</strong> generated before the final output</li>
</ul>

<p>At concurrency 1 on a MacBook, that works out to around 3-4 minutes per summary. A full run over 1,000 bill appearances takes several days of wall-clock time. Running on a machine with a dedicated GPU could bring this down to under 30 seconds per summary.</p>

<p>I will also be testing <strong>Gemma 4 e2b</strong> (<code class="language-plaintext highlighter-rouge">google/gemma-4-e2b</code>) as an alternative. It’s a 2B parameter model with a 128K context window, designed for on-device inference, and should be significantly faster. The quality tradeoff for complex parliamentary text is still to be evaluated.</p>

<p>The <code class="language-plaintext highlighter-rouge">Summarizer</code> trait is simple:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">pub</span> <span class="k">trait</span> <span class="n">Summarizer</span><span class="p">:</span> <span class="nb">Send</span> <span class="o">+</span> <span class="nb">Sync</span> <span class="p">{</span>
    <span class="k">async</span> <span class="k">fn</span> <span class="nf">summarize</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">,</span> <span class="n">prompt</span><span class="p">:</span> <span class="o">&amp;</span><span class="nb">str</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="nb">String</span><span class="o">&gt;</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The caller is responsible for building the full prompt. Prompt builders for each target live in <code class="language-plaintext highlighter-rouge">enricher/prompts.rs</code>. Bill appearance prompts include the full sitting transcript as context so the model can reference cross-bill discussions. Member contribution prompts are narrower, only including that speaker’s contributions.</p>

<h2 id="metrics">Metrics</h2>

<p>The pipeline emits metrics to a Prometheus pushgateway after each enrichment batch. Grafana is used to visualise progress, including throughput, token counts, and time to first token per call.</p>

<h2 id="running-it">Running it</h2>

<p>The pipeline is driven by a single binary, <code class="language-plaintext highlighter-rouge">odnelazm-pipeline</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Ingest 13th Parliament sittings</span>
odnelazm-pipeline ingest <span class="nt">--start-date</span> 2022-09-01 <span class="nt">--end-date</span> 2026-05-19

<span class="c"># Enrich bill appearance summaries</span>
odnelazm-pipeline <span class="nt">--metrics-url</span> http://localhost:9091 <span class="se">\</span>
  enrich bill-mentions <span class="nt">--model</span> qwen/qwen3.5-9b <span class="nt">--concurrency</span> 1
</code></pre></div></div>

<p>Enrichment is idempotent. Items that already have a summary are skipped, so runs can be interrupted and resumed without duplicating work.</p>

<p>The full source is at <a href="https://github.com/mwananchi-tech/odnelazm">mwananchi-tech/odnelazm</a>. Bunge Hub is live at <a href="https://bunge-hub.mwananchi.tech">bunge-hub.mwananchi.tech</a>.</p>]]></content><author><name>Collins Muriuki</name></author><category term="blog" /><category term="Rust" /><category term="PostgreSQL" /><category term="CivicTech" /><category term="Kenya" /><category term="DataEngineering" /><summary type="html"><![CDATA[A technical walkthrough of the data pipeline that powers Bunge Hub. Covers scraping, extraction, speaker linking, and AI enrichment.]]></summary></entry><entry><title type="html">From Bunge Bits to Bunge Hub: building Kenya’s parliamentary data layer</title><link href="https://collinsmuriuki.xyz/from-bunge-bits-to-bunge-hub/" rel="alternate" type="text/html" title="From Bunge Bits to Bunge Hub: building Kenya’s parliamentary data layer" /><published>2026-05-14T12:00:00+03:00</published><updated>2026-05-14T12:00:00+03:00</updated><id>https://collinsmuriuki.xyz/from-bunge-bits-to-bunge-hub</id><content type="html" xml:base="https://collinsmuriuki.xyz/from-bunge-bits-to-bunge-hub/"><![CDATA[<p>This is a story about a problem I kept coming back to, two failed starts, one pivot that changed everything, and a project that is finally built to last.</p>

<h2 id="where-it-started-bunge-bits">Where it started: Bunge Bits</h2>

<p><a href="https://github.com/mwananchi-tech/bunge-bits">Bunge Bits</a> started from a simple observation. The Kenyan Parliament livestreams its sittings on YouTube. Those recordings are publicly available. And yet there was no good way to search what was said, track a specific member’s contributions, or follow a bill through its debates without sitting through hours of video.</p>

<p>The idea was straightforward: transcribe the recordings using AI, summarize the content, and surface it in a searchable interface. I built a pipeline that pulled YouTube livestreams, ran them through a speech-to-text model, then used an LLM to generate summaries of what was discussed.</p>

<p>It worked, technically. The transcripts were readable, the summaries were useful, and a few people in civic and policy circles found it genuinely helpful.</p>

<p>Then the bills arrived.</p>

<p>Cloud LLMs are expensive at scale. Transcribing hours of parliamentary audio and summarizing it sitting by sitting adds up fast. The project had no funding model and no path to one. After a while, the cost of keeping it running outweighed what I could justify spending on a solo side project, and I shut it down.</p>

<p>The lesson I took from Bunge Bits was not that the idea was wrong. It was that I had built on the wrong foundation. AI-generated transcripts as the canonical source of truth is fragile and expensive. Parliament already publishes the official record. I just had not used it.</p>

<h2 id="the-pivot-hansard-as-the-source-of-truth">The pivot: Hansard as the source of truth</h2>

<p>Kenya’s official Hansard, the verbatim record of everything said in Parliament, is published through mzalendo.com. It is structured HTML: sections, headings, contributions, speaker names. It is not perfect, but it is the authoritative record, and it is free to read.</p>

<p>The pivot was to stop transcribing audio and start parsing the Hansard directly. This meant building a scraper.</p>

<p>I wrote the first version of what would become <a href="https://github.com/mwananchi-tech/odnelazm"><code class="language-plaintext highlighter-rouge">odnelazm</code></a> in Rust. The goal was simple: fetch a sitting’s HTML, parse it into structured types, and make that data accessible programmatically.</p>

<p>The parsing problem turned out to be more interesting than expected. mzalendo actually serves data from two distinct sources with different HTML structures: an archive at <code class="language-plaintext highlighter-rouge">info.mzalendo.com</code> covering sittings going back to 2006, and a current tracker at <code class="language-plaintext highlighter-rouge">mzalendo.com/democracy-tools/hansard</code> covering 2013 to present. Both needed separate parsers. And within the current source, the HTML structure changed around early 2026, requiring the parser to handle two formats without breaking.</p>

<p>That kind of structural inconsistency is normal when you are reading someone else’s website as data infrastructure. You learn to build defensively.</p>

<h2 id="odnelazm-the-scraper-and-cli">odnelazm: the scraper and CLI</h2>

<p>Once the parser was solid, I wrapped it in a CLI. <a href="https://github.com/mwananchi-tech/odnelazm/tree/main/crates/odnelazm-cli"><code class="language-plaintext highlighter-rouge">odnelazm-cli</code></a> lets you browse and fetch parliamentary data directly from the terminal.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cargo <span class="nb">install</span> <span class="nt">--git</span> https://github.com/mwananchi-tech/odnelazm odnelazm-cli
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># List recent sittings</span>
odnelazm sittings

<span class="c"># Fetch a full sitting transcript (source detected automatically from the URL)</span>
odnelazm sitting thursday-12th-february-2026-afternoon-sitting-2438

<span class="c"># List all members of the 13th Parliament National Assembly</span>
odnelazm members na 13th-parliament <span class="nt">--all</span> <span class="nt">-o</span> json

<span class="c"># Fetch a member's full profile</span>
odnelazm profile https://mzalendo.com/mps-performance/national-assembly/13th-parliament/boss-gladys-jepkosgei/
</code></pre></div></div>

<p>It supports both archive and current data sources with automatic routing, outputs JSON, CSV, or Parquet, and handles pagination. It is the kind of tool I wished existed when I was building Bunge Bits.</p>

<h2 id="odnelazm-mcp-asking-questions-about-parliament-in-plain-english">odnelazm-mcp: asking questions about Parliament in plain English</h2>

<p>The next step was making the data accessible to LLM clients. I built <a href="https://github.com/mwananchi-tech/odnelazm/tree/main/crates/odnelazm-mcp"><code class="language-plaintext highlighter-rouge">odnelazm-mcp</code></a>, an MCP (Model Context Protocol) server that exposes the scraper as tools, so any compatible client (Claude or ChatGPT) can query parliamentary data directly in conversation.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cargo <span class="nb">install</span> <span class="nt">--git</span> https://github.com/mwananchi-tech/odnelazm odnelazm-mcp-local
</code></pre></div></div>

<p>With it connected to Claude Desktop, you can ask things like:</p>

<ul>
  <li>“Who spoke on the Finance Bill in June 2023?”</li>
  <li>“List all Senate sittings from the first quarter of 2025”</li>
  <li>“What did Gladys Boss Shollei say about housing?”</li>
</ul>

<p>The server handles the fetching and parsing transparently. The LLM reasons over the structured data.</p>

<p>The limitation is context. Hansard transcripts are large. A single sitting can run to tens of thousands of tokens. Fetching multiple sittings, or a member’s full activity history, can easily exceed the context window of most models. The practical guidance is to keep queries narrow: one sitting at a time, one date range at a time. For broad cross-sitting queries, a local model with a large context window (1M+ tokens) handles it better.</p>

<h2 id="bunge-hub-the-full-picture">Bunge Hub: the full picture</h2>

<p><code class="language-plaintext highlighter-rouge">odnelazm</code> as a scraper and CLI is useful for developers and researchers who are comfortable in a terminal. But most people who care about parliamentary accountability are not that audience.</p>

<p>Bunge Hub is the web layer on top of the same data. It runs a PostgreSQL database populated by <a href="https://github.com/mwananchi-tech/odnelazm/tree/main/crates/odnelazm-ingest"><code class="language-plaintext highlighter-rouge">odnelazm-pipeline</code></a>, which handles ingesting sittings, extracting bills and topics, importing member profiles, and linking speaker records to canonical member identities.</p>

<p><img src="/assets/images/bunge-hub3.png" alt="Bunge Hub homepage showing stats, most active members, and recent bills" /></p>

<p>The data model that emerged from this is something I am genuinely proud of. 885 sittings, 391 bills, 9,826 topics, 3,001 speaker records linked where possible to 429 members. You can trace a bill through every sitting it appeared in, see who spoke at each stage, and read their contributions side by side.</p>

<p><img src="/assets/images/bunge-hub1.png" alt="Bill detail page showing the legislative journey and debate summary for the Income Tax Amendment Bill" /></p>

<p>The linking problem, matching a speaker name extracted from a transcript to a canonical member record, turned out to be one of the harder engineering problems in the project. The same person appears as “Hon. Kaluma”, “George Kaluma”, “Peter Kaluma” across different sittings. The solution combines exact URL matching (when mzalendo includes a profile link), fuzzy trigram similarity via <code class="language-plaintext highlighter-rouge">pg_trgm</code>, and a handful of special cases, like the Speaker of the National Assembly, whose contributions are labelled “Hon. Speaker” and do not fuzzy-match anything useful.</p>

<p>On top of this, I added an AI enrichment layer using locally running open-source models via LM Studio. Each bill appearance gets a summary of what was argued and what the outcome was. Each bill’s full journey across Parliament gets a narrative summary. The model used to generate each summary is recorded and displayed in the UI.</p>

<p><img src="/assets/images/bunge-hub2.png" alt="Member profile page for Anthony Kimani Ichung'wah showing sponsored bills and parliamentary activity" /></p>

<h2 id="doing-it-sustainably-this-time">Doing it sustainably this time</h2>

<p>The core lesson from Bunge Bits was about cost. I am not repeating that mistake.</p>

<p>The enrichment pipeline runs locally. No cloud LLM APIs, no per-token billing, no dependency on a service that can change its pricing or shut down. The tradeoff is throughput: a 9B model on consumer hardware is slow. But the data does not expire. A summary generated this week is just as useful next month.</p>

<p>The architecture is designed so that if better hardware or funding becomes available, swapping to a faster model is a one-line change. Every summary is attributed to the model that generated it, so a future re-enrichment pass can target specific rows selectively.</p>

<p>There is also no funding dependency for the core product. The database and web app cost pennies to host at this scale. The ingest pipeline runs on a cron job. If I step away for six months, the data already in the database stays useful.</p>

<p>One other deliberate decision: I created <a href="https://mwananchi-tech.github.io">Mwananchi Tech</a> as an open-source organisation to be the umbrella for these projects, rather than keeping them under my personal GitHub. The practical reason is that associating the work with an organisation rather than an individual makes it easier to attract collaborators and potential partners. If someone wants to contribute or build on top of what is here, they are joining a project, not someone’s personal experiment.</p>

<h2 id="whats-next">What’s next</h2>

<p>Bunge Hub is in its later stages of development. The code is open source at <a href="https://github.com/mwananchi-tech/bunge-hub">mwananchi-tech/bunge-hub</a>, and a full launch post will be published once it is publicly available.</p>

<p>The current data covers the 13th Parliament (September 2022 to present). The archive goes back to 2006. <code class="language-plaintext highlighter-rouge">odnelazm</code> already has a full parser for the archival source at <code class="language-plaintext highlighter-rouge">info.mzalendo.com</code>, so extending coverage to earlier parliaments is a matter of running the pipeline, not building new infrastructure.</p>

<p>The other thing I keep thinking about is YouTube. The recordings that Bunge Bits used to transcribe are still there. Now that the Hansard is the canonical source, the opportunity is different: not transcription, but timestamping. If you could link a specific contribution in the structured transcript to the exact moment in the YouTube recording, you could read a debate and click directly to the video. That closes the loop between the written record and the live moment in a way nothing else currently does.</p>

<p>That is a harder problem. But the foundation is now solid enough to build it on.</p>

<h2 id="why-am-i-doing-this">Why am I doing this?</h2>

<p>Honestly, curiosity got me here. There is a genuine gap nobody in the Kenyan tech space has seriously tried to close, and that bothered me enough to do something about it.</p>

<p>I did worry, at some point, about tying my identity too closely to civic tech. It is a niche space and not always the most visible one. But I have stopped caring about that.</p>

<p>There was also a more uncomfortable fear. People who work in civic tech in Kenya get reminded, sometimes only half-jokingly, that Subarus might come for them. That fear is not entirely abstract. What happened to Rose Njeri was a real reminder of the risks that come with this kind of work in this environment. I thought about it. I will not pretend I did not. But I am confident that what I am building is not illegal. All the data I use is publicly available. The Hansard is the official parliamentary record. I am not hacking anything, exposing anything that is not already public, or doing anything I could not defend clearly and loudly if I had to.</p>

<p>A few days ago I attended a Tech and Democracy meetup organized by <a href="https://sparkable.cc/?tab=Public">Sparkable</a> and met some genuinely interesting people working on accountability and governance from different angles. That conversation spurred me on more than anything else has in a while. If this is the cause my name ends up attached to, I am fine with it. It is technically challenging, it is for the greater good, and it is using the latest tooling and AI to bring some meaning to an era where that can be hard to find.</p>

<p>Also, full transparency: I have had a lot of free time. Not working a full-time job at the moment, which is partly how a project this scope got built by one person in under a year. If you are hiring and any of this resonates, <a href="mailto:hell@collinsmuriuki.xyz">I am open to it</a>.</p>]]></content><author><name>Collins Muriuki</name></author><category term="blog" /><category term="Rust" /><category term="AI" /><category term="CivicTech" /><category term="Kenya" /><category term="PostgreSQL" /><category term="MCP" /><summary type="html"><![CDATA[A year-long journey from an AI transcription pipeline that shut down due to cost, to a sustainable open-source platform for Kenya's parliamentary record built on the Hansard itself.]]></summary></entry><entry><title type="html">Building the Library of Babel in Go</title><link href="https://collinsmuriuki.xyz/building-the-library-of-babel-in-go/" rel="alternate" type="text/html" title="Building the Library of Babel in Go" /><published>2025-12-18T10:00:00+03:00</published><updated>2025-12-18T10:00:00+03:00</updated><id>https://collinsmuriuki.xyz/building-the-library-of-babel-in-go</id><content type="html" xml:base="https://collinsmuriuki.xyz/building-the-library-of-babel-in-go/"><![CDATA[<p>Most of my Go experience before this project was through IaC with Pulumi. Useful work, but it barely scratches the surface of the language. I have been writing Rust for a while, and I found Go’s concurrency model genuinely odd at first. Channels and goroutines felt strange coming from Rust’s ownership model. The syntax took getting used to too. But the more I used it, the more it grew on me. Go is not trying to be Rust. It is simpler, more opinionated, and that simplicity turns out to be a feature.</p>

<p>This project was an excuse to go deeper.</p>

<h2 id="the-library-of-babel">The Library of Babel</h2>

<p><a href="https://en.wikipedia.org/wiki/The_Library_of_Babel">The Library of Babel</a> is a short story by Jorge Luis Borges. The premise is a library that contains every possible 410-page book. Not just the books that have been written, but every combination of characters that could exist. Your biography, before you have lived it. This documentation. Complete nonsense. Every text, in every order, somewhere on a shelf.</p>

<p><a href="https://libraryofbabel.info">Jonathan Basile</a> built a working web implementation of this. A few months ago, <a href="https://youtu.be/yZ6S8fvP5xM?si=jIZt0UBhEMzOBUDG">this video</a> by Merlin on YouTube was randomly recommended to me. I watched it and immediately fell into a rabbit hole about deterministic randomness.</p>

<p>The interesting part is not that the library contains everything. It is how it does it. The content of any page is not stored anywhere. It is computed on demand, deterministically, from its address. Search for a piece of text, and you get back a location in the library where that exact text appears, generated from the search itself as a seed. The library does not remember your query. It just consistently gives you the same page for the same input every time.</p>

<p>That idea stuck with me. I wanted to build it.</p>

<h2 id="the-character-set-and-base29-encoding">The Character Set and Base29 Encoding</h2>

<p>The original Borges concept uses a small alphabet. Most implementations converge on a 29-character set: lowercase a to z, a space, a comma, and a period. That gives you enough to write something coherent, and 29 is a prime number, which has nice properties for base conversion. Some implementations go further: <a href="https://github.com/tdjsnelling/babel">this TypeScript implementation</a> by tdjsnelling extends the charset to 32 characters. I chose to stick with the original 29. I did borrow the addressing system from that project though, which I found quite intuitive.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">charset</span> <span class="o">:=</span> <span class="s">" abcdefghijklmnopqrstuvwxyz,."</span>
</code></pre></div></div>

<p>Each page in the library is 3,200 characters (40 lines of 80 characters). To find a page, you convert its content into a single base-29 number. That number, decomposed into its hierarchical parts, gives you the address. The conversion treats each character as a digit in base 29: space = 0, a = 1, b = 2, and so on through to period = 28.</p>

<p>Going the other way (address back to page content) is repeated division by 29, collecting remainders, then mapping each remainder back to a character. The library is just a very large number space dressed up as rooms and shelves.</p>

<h2 id="seeding-the-search">Seeding the Search</h2>

<p>The thing that makes search work is using the search text itself as a seed to generate a deterministic position. When you search for “hello”, the library does not scan billions of pages looking for the word. It hashes “hello” with SHA256, seeds an RNG from that hash, generates a full 3,200-character page with “hello” planted at a position within it, then converts that page to its base-29 address.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="p">(</span><span class="n">l</span> <span class="n">Library</span><span class="p">)</span> <span class="n">seedPageChars</span><span class="p">(</span><span class="n">text</span> <span class="kt">string</span><span class="p">,</span> <span class="n">variant</span> <span class="kt">int</span><span class="p">)</span> <span class="kt">string</span> <span class="p">{</span>
    <span class="n">input</span> <span class="o">:=</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Sprintf</span><span class="p">(</span><span class="s">"%s</span><span class="se">\x00</span><span class="s">%d"</span><span class="p">,</span> <span class="n">strings</span><span class="o">.</span><span class="n">ToLower</span><span class="p">(</span><span class="n">text</span><span class="p">),</span> <span class="n">variant</span><span class="p">)</span>
    <span class="n">textHash</span> <span class="o">:=</span> <span class="n">sha256</span><span class="o">.</span><span class="n">Sum256</span><span class="p">([]</span><span class="kt">byte</span><span class="p">(</span><span class="n">input</span><span class="p">))</span>
    <span class="n">textSeed</span> <span class="o">:=</span> <span class="kt">int64</span><span class="p">(</span><span class="n">binary</span><span class="o">.</span><span class="n">BigEndian</span><span class="o">.</span><span class="n">Uint64</span><span class="p">(</span><span class="n">textHash</span><span class="p">[</span><span class="o">:</span><span class="m">8</span><span class="p">]))</span>
    <span class="n">rng</span> <span class="o">:=</span> <span class="n">rand</span><span class="o">.</span><span class="n">New</span><span class="p">(</span><span class="n">rand</span><span class="o">.</span><span class="n">NewSource</span><span class="p">(</span><span class="n">textSeed</span><span class="p">))</span>

    <span class="n">maxPosition</span> <span class="o">:=</span> <span class="n">charsPerPage</span> <span class="o">-</span> <span class="nb">len</span><span class="p">(</span><span class="n">text</span><span class="p">)</span>
    <span class="n">position</span> <span class="o">:=</span> <span class="n">rng</span><span class="o">.</span><span class="n">Intn</span><span class="p">(</span><span class="n">maxPosition</span> <span class="o">+</span> <span class="m">1</span><span class="p">)</span>

    <span class="n">pageChars</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">([]</span><span class="kt">byte</span><span class="p">,</span> <span class="n">charsPerPage</span><span class="p">)</span>
    <span class="k">for</span> <span class="n">i</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">charsPerPage</span> <span class="p">{</span>
        <span class="n">pageChars</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">l</span><span class="o">.</span><span class="n">charset</span><span class="p">[</span><span class="n">rng</span><span class="o">.</span><span class="n">Intn</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">l</span><span class="o">.</span><span class="n">charset</span><span class="p">))]</span>
    <span class="p">}</span>

    <span class="nb">copy</span><span class="p">(</span><span class="n">pageChars</span><span class="p">[</span><span class="n">position</span><span class="o">:</span><span class="p">],</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">(</span><span class="n">strings</span><span class="o">.</span><span class="n">ToLower</span><span class="p">(</span><span class="n">text</span><span class="p">)))</span>
    <span class="k">return</span> <span class="kt">string</span><span class="p">(</span><span class="n">pageChars</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">seedPageChars</code> is called by <code class="language-plaintext highlighter-rouge">generateBase29Number</code>, which walks the page character by character and builds the actual base-29 number:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="p">(</span><span class="n">l</span> <span class="n">Library</span><span class="p">)</span> <span class="n">generateBase29Number</span><span class="p">(</span><span class="n">text</span> <span class="kt">string</span><span class="p">,</span> <span class="n">variant</span> <span class="kt">int</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="n">big</span><span class="o">.</span><span class="n">Int</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">result</span> <span class="o">:=</span> <span class="n">big</span><span class="o">.</span><span class="n">NewInt</span><span class="p">(</span><span class="m">0</span><span class="p">)</span>
    <span class="n">pageChars</span> <span class="o">:=</span> <span class="n">l</span><span class="o">.</span><span class="n">seedPageChars</span><span class="p">(</span><span class="n">text</span><span class="p">,</span> <span class="n">variant</span><span class="p">)</span>

    <span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">char</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">pageChars</span> <span class="p">{</span>
        <span class="n">result</span><span class="o">.</span><span class="n">Mul</span><span class="p">(</span><span class="n">result</span><span class="p">,</span> <span class="n">l</span><span class="o">.</span><span class="n">base</span><span class="p">)</span>
        <span class="n">index</span> <span class="o">:=</span> <span class="n">l</span><span class="o">.</span><span class="n">charToIndex</span><span class="p">[</span><span class="n">char</span><span class="p">]</span>
        <span class="n">result</span><span class="o">.</span><span class="n">Add</span><span class="p">(</span><span class="n">result</span><span class="p">,</span> <span class="n">big</span><span class="o">.</span><span class="n">NewInt</span><span class="p">(</span><span class="kt">int64</span><span class="p">(</span><span class="n">index</span><span class="p">)))</span>
    <span class="p">}</span>

    <span class="k">return</span> <span class="n">result</span><span class="p">,</span> <span class="no">nil</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Each character is treated as a digit: multiply the running total by 29, add the character’s index. After 3,200 characters you have a single very large integer that uniquely encodes the page. That number is what gets decomposed into the hexagon, wall, shelf, book, and page coordinates.</p>

<p>The <code class="language-plaintext highlighter-rouge">variant</code> parameter is how multiple results work. Searching with variant 0 gives you one address, variant 1 gives you a different one. The same text plus a different variant integer produces a different seed, a different page, a different address. Every text “appears” in the library an essentially unlimited number of times, just at different coordinates.</p>

<h2 id="how-many-results">How Many Results?</h2>

<p>This is where the math got interesting, and I will be honest: I relied on AI a lot here because combinatorics is not really my forte.</p>

<p>The number of locations where a given text appears should scale inversely with the length of that text. A single character like “a” appears everywhere. The phrase “to be or not to be” appears much less frequently. A full paragraph is vanishingly rare.</p>

<p>I modeled this with exponential decay:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">const</span> <span class="n">exponentialDecayRate</span> <span class="o">=</span> <span class="m">1.10</span>

<span class="k">func</span> <span class="p">(</span><span class="n">l</span> <span class="n">Library</span><span class="p">)</span> <span class="n">GetOccurrenceCount</span><span class="p">(</span><span class="n">text</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">int</span> <span class="p">{</span>
    <span class="n">textLen</span> <span class="o">:=</span> <span class="nb">len</span><span class="p">(</span><span class="n">text</span><span class="p">)</span>
    <span class="n">maxCount</span> <span class="o">:=</span> <span class="m">1</span><span class="n">_000_000_000</span> <span class="c">// 1 billion for single characters</span>
    <span class="n">baseCount</span> <span class="o">:=</span> <span class="n">maxCount</span> <span class="o">/</span> <span class="kt">int</span><span class="p">(</span><span class="n">math</span><span class="o">.</span><span class="n">Pow</span><span class="p">(</span><span class="n">exponentialDecayRate</span><span class="p">,</span> <span class="kt">float64</span><span class="p">(</span><span class="n">textLen</span><span class="o">-</span><span class="m">1</span><span class="p">)))</span>
    <span class="n">baseCount</span> <span class="o">=</span> <span class="n">max</span><span class="p">(</span><span class="m">1</span><span class="p">,</span> <span class="n">baseCount</span><span class="p">)</span>

    <span class="n">hash</span> <span class="o">:=</span> <span class="n">sha256</span><span class="o">.</span><span class="n">Sum256</span><span class="p">([]</span><span class="kt">byte</span><span class="p">(</span><span class="n">strings</span><span class="o">.</span><span class="n">ToLower</span><span class="p">(</span><span class="n">text</span><span class="p">)))</span>
    <span class="n">seed</span> <span class="o">:=</span> <span class="kt">int64</span><span class="p">(</span><span class="n">binary</span><span class="o">.</span><span class="n">BigEndian</span><span class="o">.</span><span class="n">Uint64</span><span class="p">(</span><span class="n">hash</span><span class="p">[</span><span class="o">:</span><span class="m">8</span><span class="p">]))</span>
    <span class="n">rng</span> <span class="o">:=</span> <span class="n">rand</span><span class="o">.</span><span class="n">New</span><span class="p">(</span><span class="n">rand</span><span class="o">.</span><span class="n">NewSource</span><span class="p">(</span><span class="n">seed</span><span class="p">))</span>

    <span class="n">variation</span> <span class="o">:=</span> <span class="n">max</span><span class="p">(</span><span class="m">1</span><span class="p">,</span> <span class="n">baseCount</span><span class="o">/</span><span class="m">4</span><span class="p">)</span>
    <span class="n">adjustment</span> <span class="o">:=</span> <span class="n">rng</span><span class="o">.</span><span class="n">Intn</span><span class="p">(</span><span class="m">2</span><span class="o">*</span><span class="n">variation</span><span class="p">)</span> <span class="o">-</span> <span class="n">variation</span>

    <span class="k">return</span> <span class="n">max</span><span class="p">(</span><span class="m">1</span><span class="p">,</span> <span class="n">baseCount</span><span class="o">+</span><span class="n">adjustment</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Starting at 1 billion for a single character, each additional character divides the result by 1.10. By the time you reach a moderately long phrase, you are down to a handful of results. The ±25% variation seeded from the text hash gives each query a consistent but unique feel. The tradeoff is that the billion cap is completely hardcoded. It is a reasonable approximation of “effectively infinite”, not something derived from first principles. Good enough for this.</p>

<h2 id="the-addressing-system">The Addressing System</h2>

<p>Every page in the library has a five-part address: <code class="language-plaintext highlighter-rouge">&lt;hexagon&gt;.&lt;wall&gt;.&lt;shelf&gt;.&lt;book&gt;.&lt;page&gt;</code>. The structure mirrors Borges’ description: hexagonal rooms with four walls, each holding five shelves, each shelf holding 32 books, each book with 410 pages.</p>

<p>Converting a base-29 number to an address is a series of modular divisions:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="n">newFromBase29Number</span><span class="p">(</span><span class="n">n</span> <span class="o">*</span><span class="n">big</span><span class="o">.</span><span class="n">Int</span><span class="p">)</span> <span class="o">*</span><span class="n">Location</span> <span class="p">{</span>
    <span class="n">temp</span><span class="p">,</span> <span class="n">quotient</span> <span class="o">:=</span> <span class="nb">new</span><span class="p">(</span><span class="n">big</span><span class="o">.</span><span class="n">Int</span><span class="p">)</span><span class="o">.</span><span class="n">Abs</span><span class="p">(</span><span class="n">n</span><span class="p">),</span> <span class="nb">new</span><span class="p">(</span><span class="n">big</span><span class="o">.</span><span class="n">Int</span><span class="p">)</span>

    <span class="n">page</span> <span class="o">:=</span> <span class="nb">new</span><span class="p">(</span><span class="n">big</span><span class="o">.</span><span class="n">Int</span><span class="p">)</span>
    <span class="n">quotient</span><span class="p">,</span> <span class="n">page</span> <span class="o">=</span> <span class="n">quotient</span><span class="o">.</span><span class="n">DivMod</span><span class="p">(</span><span class="n">temp</span><span class="p">,</span> <span class="n">big</span><span class="o">.</span><span class="n">NewInt</span><span class="p">(</span><span class="n">pagesPerBook</span><span class="p">),</span> <span class="n">page</span><span class="p">)</span>
    <span class="n">temp</span><span class="o">.</span><span class="n">Set</span><span class="p">(</span><span class="n">quotient</span><span class="p">)</span>

    <span class="n">book</span> <span class="o">:=</span> <span class="nb">new</span><span class="p">(</span><span class="n">big</span><span class="o">.</span><span class="n">Int</span><span class="p">)</span>
    <span class="n">quotient</span><span class="p">,</span> <span class="n">book</span> <span class="o">=</span> <span class="n">quotient</span><span class="o">.</span><span class="n">DivMod</span><span class="p">(</span><span class="n">temp</span><span class="p">,</span> <span class="n">big</span><span class="o">.</span><span class="n">NewInt</span><span class="p">(</span><span class="n">booksPerShelf</span><span class="p">),</span> <span class="n">book</span><span class="p">)</span>
    <span class="n">temp</span><span class="o">.</span><span class="n">Set</span><span class="p">(</span><span class="n">quotient</span><span class="p">)</span>

    <span class="n">shelf</span> <span class="o">:=</span> <span class="nb">new</span><span class="p">(</span><span class="n">big</span><span class="o">.</span><span class="n">Int</span><span class="p">)</span>
    <span class="n">quotient</span><span class="p">,</span> <span class="n">shelf</span> <span class="o">=</span> <span class="n">quotient</span><span class="o">.</span><span class="n">DivMod</span><span class="p">(</span><span class="n">temp</span><span class="p">,</span> <span class="n">big</span><span class="o">.</span><span class="n">NewInt</span><span class="p">(</span><span class="n">shelvesPerWall</span><span class="p">),</span> <span class="n">shelf</span><span class="p">)</span>
    <span class="n">temp</span><span class="o">.</span><span class="n">Set</span><span class="p">(</span><span class="n">quotient</span><span class="p">)</span>

    <span class="n">wall</span> <span class="o">:=</span> <span class="nb">new</span><span class="p">(</span><span class="n">big</span><span class="o">.</span><span class="n">Int</span><span class="p">)</span>
    <span class="n">quotient</span><span class="p">,</span> <span class="n">wall</span> <span class="o">=</span> <span class="n">quotient</span><span class="o">.</span><span class="n">DivMod</span><span class="p">(</span><span class="n">temp</span><span class="p">,</span> <span class="n">big</span><span class="o">.</span><span class="n">NewInt</span><span class="p">(</span><span class="n">wallsPerHexagon</span><span class="p">),</span> <span class="n">wall</span><span class="p">)</span>
    <span class="n">temp</span><span class="o">.</span><span class="n">Set</span><span class="p">(</span><span class="n">quotient</span><span class="p">)</span>

    <span class="k">return</span> <span class="o">&amp;</span><span class="n">Location</span><span class="p">{</span>
        <span class="n">Hexagon</span><span class="o">:</span> <span class="n">quotient</span><span class="o">.</span><span class="n">Text</span><span class="p">(</span><span class="m">36</span><span class="p">),</span>
        <span class="n">Wall</span><span class="o">:</span>    <span class="kt">int</span><span class="p">(</span><span class="n">wall</span><span class="o">.</span><span class="n">Int64</span><span class="p">()),</span>
        <span class="n">Shelf</span><span class="o">:</span>   <span class="kt">int</span><span class="p">(</span><span class="n">shelf</span><span class="o">.</span><span class="n">Int64</span><span class="p">()),</span>
        <span class="n">Book</span><span class="o">:</span>    <span class="kt">int</span><span class="p">(</span><span class="n">book</span><span class="o">.</span><span class="n">Int64</span><span class="p">()),</span>
        <span class="n">Page</span><span class="o">:</span>    <span class="kt">int</span><span class="p">(</span><span class="n">page</span><span class="o">.</span><span class="n">Int64</span><span class="p">())</span> <span class="o">+</span> <span class="m">1</span><span class="p">,</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The hexagon identifier is whatever is left after stripping out wall, shelf, book, and page. It can be an astronomically large number, so it is stored as a base-36 string (digits and lowercase letters). An address ends up looking something like <code class="language-plaintext highlighter-rouge">3q7z.2.1.14.207</code>. The hierarchy is reversible: you can reconstruct the original base-29 number from any valid address, which is how browsing works.</p>

<h2 id="pagination">Pagination</h2>

<p>One thing the original concept glosses over is what pagination even means when a search for “a” returns close to a billion results.</p>

<p>The variant approach makes this tractable. Variants are sequential integers starting from 0. Pagination is just offset and limit over that sequence:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="p">(</span><span class="n">l</span> <span class="n">Library</span><span class="p">)</span> <span class="n">SearchPaginated</span><span class="p">(</span><span class="n">text</span> <span class="kt">string</span><span class="p">,</span> <span class="n">offset</span><span class="p">,</span> <span class="n">limit</span> <span class="kt">int</span><span class="p">)</span> <span class="p">([]</span><span class="o">*</span><span class="n">Location</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">totalCount</span> <span class="o">:=</span> <span class="n">l</span><span class="o">.</span><span class="n">GetOccurrenceCount</span><span class="p">(</span><span class="n">text</span><span class="p">)</span>

    <span class="k">if</span> <span class="n">offset</span> <span class="o">&lt;</span> <span class="m">0</span> <span class="p">{</span>
        <span class="k">return</span> <span class="no">nil</span><span class="p">,</span> <span class="n">errors</span><span class="o">.</span><span class="n">New</span><span class="p">(</span><span class="s">"offset cannot be negative"</span><span class="p">)</span>
    <span class="p">}</span>
    <span class="k">if</span> <span class="n">limit</span> <span class="o">&lt;=</span> <span class="m">0</span> <span class="p">{</span>
        <span class="k">return</span> <span class="no">nil</span><span class="p">,</span> <span class="n">errors</span><span class="o">.</span><span class="n">New</span><span class="p">(</span><span class="s">"limit must be positive"</span><span class="p">)</span>
    <span class="p">}</span>
    <span class="k">if</span> <span class="n">offset</span> <span class="o">&gt;=</span> <span class="n">totalCount</span> <span class="p">{</span>
        <span class="k">return</span> <span class="p">[]</span><span class="o">*</span><span class="n">Location</span><span class="p">{},</span> <span class="no">nil</span>
    <span class="p">}</span>

    <span class="n">endIndex</span> <span class="o">:=</span> <span class="n">min</span><span class="p">(</span><span class="n">offset</span><span class="o">+</span><span class="n">limit</span><span class="p">,</span> <span class="n">totalCount</span><span class="p">)</span>
    <span class="n">locations</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">([]</span><span class="o">*</span><span class="n">Location</span><span class="p">,</span> <span class="m">0</span><span class="p">,</span> <span class="n">endIndex</span><span class="o">-</span><span class="n">offset</span><span class="p">)</span>

    <span class="k">for</span> <span class="n">variant</span> <span class="o">:=</span> <span class="n">offset</span><span class="p">;</span> <span class="n">variant</span> <span class="o">&lt;</span> <span class="n">endIndex</span><span class="p">;</span> <span class="n">variant</span><span class="o">++</span> <span class="p">{</span>
        <span class="n">bigInt</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">l</span><span class="o">.</span><span class="n">generateBase29Number</span><span class="p">(</span><span class="n">text</span><span class="p">,</span> <span class="n">variant</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
            <span class="k">return</span> <span class="no">nil</span><span class="p">,</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"error generating location for variant %d: %w"</span><span class="p">,</span> <span class="n">variant</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
        <span class="p">}</span>
        <span class="n">locations</span> <span class="o">=</span> <span class="nb">append</span><span class="p">(</span><span class="n">locations</span><span class="p">,</span> <span class="n">newFromBase29Number</span><span class="p">(</span><span class="n">bigInt</span><span class="p">))</span>
    <span class="p">}</span>

    <span class="k">return</span> <span class="n">locations</span><span class="p">,</span> <span class="no">nil</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Each page of results is a slice of variant integers. No state, no cursors, no database. You can reproduce any page of results for any query deterministically.</p>

<h2 id="hacks-and-loose-ends">Hacks and Loose Ends</h2>

<p>There are a few places in the code where I left honest markers of imperfection.</p>

<p>The significant one is in <code class="language-plaintext highlighter-rouge">base29NumberToString</code>. The browse API accepts any valid address, and hexagon identifiers can be any integer from 1 up to the maximum <code class="language-plaintext highlighter-rouge">big.Int</code>. If you manually navigate to a small address like <code class="language-plaintext highlighter-rouge">1.0.0.0.1</code>, the corresponding base-29 number is tiny, and decoding it back to characters produces a single-character page. A page with one character is not ideal. The hack is to take whatever characters the conversion produced, hash them, seed an RNG from that hash, and fill the remaining space. It preserves determinism (the same small address always fills the same way) but it is not elegant.</p>

<h2 id="concurrency-and-the-worker-pool">Concurrency and the Worker Pool</h2>

<p><code class="language-plaintext highlighter-rouge">SearchStream</code> was my first real exercise in Go concurrency. Rather than blocking until all results are computed, it returns a channel immediately and lets the caller consume locations as they are generated. The full implementation looks like this:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="p">(</span><span class="n">l</span> <span class="n">Library</span><span class="p">)</span> <span class="n">SearchStream</span><span class="p">(</span><span class="n">ctx</span> <span class="n">context</span><span class="o">.</span><span class="n">Context</span><span class="p">,</span> <span class="n">text</span> <span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="o">&lt;-</span><span class="k">chan</span> <span class="o">*</span><span class="n">Location</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">totalCount</span> <span class="o">:=</span> <span class="n">l</span><span class="o">.</span><span class="n">GetOccurrenceCount</span><span class="p">(</span><span class="n">text</span><span class="p">)</span>
    <span class="n">locationChan</span><span class="p">,</span> <span class="n">workerChan</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="k">chan</span> <span class="o">*</span><span class="n">Location</span><span class="p">),</span> <span class="nb">make</span><span class="p">(</span><span class="k">chan</span> <span class="kt">int</span><span class="p">,</span> <span class="m">100</span><span class="p">)</span>
    <span class="n">numWorkers</span> <span class="o">:=</span> <span class="n">runtime</span><span class="o">.</span><span class="n">NumCPU</span><span class="p">()</span>
    <span class="k">var</span> <span class="n">wg</span> <span class="n">sync</span><span class="o">.</span><span class="n">WaitGroup</span>

    <span class="k">for</span> <span class="k">range</span> <span class="n">numWorkers</span> <span class="p">{</span>
        <span class="n">wg</span><span class="o">.</span><span class="n">Go</span><span class="p">(</span><span class="k">func</span><span class="p">()</span> <span class="p">{</span>
            <span class="k">for</span> <span class="p">{</span>
                <span class="k">select</span> <span class="p">{</span>
                <span class="k">case</span> <span class="o">&lt;-</span><span class="n">ctx</span><span class="o">.</span><span class="n">Done</span><span class="p">()</span><span class="o">:</span>
                    <span class="k">return</span>
                <span class="k">case</span> <span class="n">variant</span><span class="p">,</span> <span class="n">ok</span> <span class="o">:=</span> <span class="o">&lt;-</span><span class="n">workerChan</span><span class="o">:</span>
                    <span class="k">if</span> <span class="o">!</span><span class="n">ok</span> <span class="p">{</span>
                        <span class="n">workerChan</span> <span class="o">=</span> <span class="no">nil</span>
                        <span class="k">continue</span>
                    <span class="p">}</span>
                    <span class="n">bigInt</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">l</span><span class="o">.</span><span class="n">generateBase29Number</span><span class="p">(</span><span class="n">text</span><span class="p">,</span> <span class="n">variant</span><span class="p">)</span>
                    <span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
                        <span class="k">continue</span>
                    <span class="p">}</span>
                    <span class="n">location</span> <span class="o">:=</span> <span class="n">newFromBase29Number</span><span class="p">(</span><span class="n">bigInt</span><span class="p">)</span>
                    <span class="k">select</span> <span class="p">{</span>
                    <span class="k">case</span> <span class="n">locationChan</span> <span class="o">&lt;-</span> <span class="n">location</span><span class="o">:</span>
                    <span class="k">case</span> <span class="o">&lt;-</span><span class="n">ctx</span><span class="o">.</span><span class="n">Done</span><span class="p">()</span><span class="o">:</span>
                        <span class="k">return</span>
                    <span class="p">}</span>
                <span class="p">}</span>
            <span class="p">}</span>
        <span class="p">})</span>
    <span class="p">}</span>

    <span class="k">go</span> <span class="k">func</span><span class="p">()</span> <span class="p">{</span>
        <span class="k">defer</span> <span class="nb">close</span><span class="p">(</span><span class="n">workerChan</span><span class="p">)</span>
        <span class="k">for</span> <span class="n">variant</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">totalCount</span> <span class="p">{</span>
            <span class="k">select</span> <span class="p">{</span>
            <span class="k">case</span> <span class="n">workerChan</span> <span class="o">&lt;-</span> <span class="n">variant</span><span class="o">:</span>
            <span class="k">case</span> <span class="o">&lt;-</span><span class="n">ctx</span><span class="o">.</span><span class="n">Done</span><span class="p">()</span><span class="o">:</span>
                <span class="k">return</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}()</span>

    <span class="k">go</span> <span class="k">func</span><span class="p">()</span> <span class="p">{</span>
        <span class="k">defer</span> <span class="nb">close</span><span class="p">(</span><span class="n">locationChan</span><span class="p">)</span>
        <span class="n">wg</span><span class="o">.</span><span class="n">Wait</span><span class="p">()</span>
    <span class="p">}()</span>

    <span class="k">return</span> <span class="n">locationChan</span><span class="p">,</span> <span class="no">nil</span>
<span class="p">}</span>
</code></pre></div></div>

<p>There are three moving parts.</p>

<p>The dispatcher goroutine sends variant integers (0, 1, 2…) into a buffered <code class="language-plaintext highlighter-rouge">workerChan</code>. The buffer size of 100 means the dispatcher can stay ahead of the workers without blocking. It stops early if the context is cancelled.</p>

<p>The worker goroutines (one per CPU) pull variants from <code class="language-plaintext highlighter-rouge">workerChan</code>, compute a location, and send it to <code class="language-plaintext highlighter-rouge">locationChan</code>. Each of the two send operations checks <code class="language-plaintext highlighter-rouge">ctx.Done()</code> so that a cancelled context unblocks workers whether they are waiting to receive a job or waiting to send a result.</p>

<p>The closer goroutine waits for all workers to finish via <code class="language-plaintext highlighter-rouge">sync.WaitGroup</code>, then closes <code class="language-plaintext highlighter-rouge">locationChan</code>. The consumer knows the stream is exhausted when the channel closes.</p>

<h2 id="goroutine-leaks">Goroutine Leaks</h2>

<p>The more interesting case is when you only want a subset of results. The test below reads exactly 100 locations then stops:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="n">TestLibrarySearchStream</span><span class="p">(</span><span class="n">t</span> <span class="o">*</span><span class="n">testing</span><span class="o">.</span><span class="n">T</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">library</span> <span class="o">:=</span> <span class="n">New</span><span class="p">()</span>
    <span class="n">ctx</span><span class="p">,</span> <span class="n">cancel</span> <span class="o">:=</span> <span class="n">context</span><span class="o">.</span><span class="n">WithCancel</span><span class="p">(</span><span class="n">context</span><span class="o">.</span><span class="n">Background</span><span class="p">())</span>
    <span class="k">defer</span> <span class="n">cancel</span><span class="p">()</span>

    <span class="n">results</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">library</span><span class="o">.</span><span class="n">SearchStream</span><span class="p">(</span><span class="n">ctx</span><span class="p">,</span> <span class="n">searchText</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
        <span class="n">t</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"search stream failed: %v"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
    <span class="p">}</span>

    <span class="n">locations</span> <span class="o">:=</span> <span class="p">[]</span><span class="o">*</span><span class="n">Location</span><span class="p">{}</span>
    <span class="n">limit</span> <span class="o">:=</span> <span class="m">100</span>
    <span class="k">for</span> <span class="k">range</span> <span class="n">limit</span> <span class="p">{</span>
        <span class="n">location</span> <span class="o">:=</span> <span class="o">&lt;-</span><span class="n">results</span>
        <span class="n">locations</span> <span class="o">=</span> <span class="nb">append</span><span class="p">(</span><span class="n">locations</span><span class="p">,</span> <span class="n">location</span><span class="p">)</span>
    <span class="p">}</span>
    <span class="n">cancel</span><span class="p">()</span>

    <span class="k">if</span> <span class="n">l</span> <span class="o">:=</span> <span class="nb">len</span><span class="p">(</span><span class="n">locations</span><span class="p">);</span> <span class="n">l</span> <span class="o">!=</span> <span class="n">limit</span> <span class="p">{</span>
        <span class="n">t</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"expected %d locations, got %d"</span><span class="p">,</span> <span class="n">limit</span><span class="p">,</span> <span class="n">l</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Once the loop exits, we call <code class="language-plaintext highlighter-rouge">cancel()</code> explicitly rather than relying on <code class="language-plaintext highlighter-rouge">defer</code>. Both work, but calling it immediately after collecting the results we need is the right instinct: it signals the workers to stop as soon as possible rather than waiting until the function returns.</p>

<p>Without that cancellation, you get a goroutine leak. Here is the scenario. <code class="language-plaintext highlighter-rouge">locationChan</code> is unbuffered. Once the consumer stops reading, the next worker trying to send a location will block forever on this:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">select</span> <span class="p">{</span>
<span class="k">case</span> <span class="n">locationChan</span> <span class="o">&lt;-</span> <span class="n">location</span><span class="o">:</span> <span class="c">// blocked: nobody reading</span>
<span class="k">case</span> <span class="o">&lt;-</span><span class="n">ctx</span><span class="o">.</span><span class="n">Done</span><span class="p">()</span><span class="o">:</span>             <span class="c">// never fires: context not cancelled</span>
    <span class="k">return</span>
<span class="p">}</span>
</code></pre></div></div>

<p>All workers stall. <code class="language-plaintext highlighter-rouge">wg.Wait()</code> never returns. <code class="language-plaintext highlighter-rouge">locationChan</code> is never closed. Everything is stuck.</p>

<p>Cancelling the context is what breaks the deadlock. The <code class="language-plaintext highlighter-rouge">case &lt;-ctx.Done()</code> branch fires in every worker, they all return, <code class="language-plaintext highlighter-rouge">wg.Wait()</code> completes, and <code class="language-plaintext highlighter-rouge">locationChan</code> closes. Whether you call <code class="language-plaintext highlighter-rouge">cancel()</code> explicitly after collecting what you need or rely on <code class="language-plaintext highlighter-rouge">defer</code>, the important thing is that it always runs.</p>

<p>The dispatcher has the same guard. If the context is cancelled while the dispatcher is still enqueuing variants, it exits immediately rather than blocking on a full <code class="language-plaintext highlighter-rouge">workerChan</code>:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">select</span> <span class="p">{</span>
<span class="k">case</span> <span class="n">workerChan</span> <span class="o">&lt;-</span> <span class="n">variant</span><span class="o">:</span>
<span class="k">case</span> <span class="o">&lt;-</span><span class="n">ctx</span><span class="o">.</span><span class="n">Done</span><span class="p">()</span><span class="o">:</span>
    <span class="k">return</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="the-nil-channel-trick">The Nil Channel Trick</h2>

<p>There is one subtlety in how workers handle a closed <code class="language-plaintext highlighter-rouge">workerChan</code>. When the dispatcher has sent all variants and its goroutine exits, it closes <code class="language-plaintext highlighter-rouge">workerChan</code>. A worker receives the zero value with <code class="language-plaintext highlighter-rouge">ok = false</code>, then does something that looks odd:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">case</span> <span class="n">variant</span><span class="p">,</span> <span class="n">ok</span> <span class="o">:=</span> <span class="o">&lt;-</span><span class="n">workerChan</span><span class="o">:</span>
    <span class="k">if</span> <span class="o">!</span><span class="n">ok</span> <span class="p">{</span>
        <span class="n">workerChan</span> <span class="o">=</span> <span class="no">nil</span>
        <span class="k">continue</span>
    <span class="p">}</span>
</code></pre></div></div>

<p>It sets its local copy of <code class="language-plaintext highlighter-rouge">workerChan</code> to <code class="language-plaintext highlighter-rouge">nil</code> and loops. A receive on a nil channel blocks forever. On the next iteration, the worker’s <code class="language-plaintext highlighter-rouge">select</code> effectively becomes:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">select</span> <span class="p">{</span>
<span class="k">case</span> <span class="o">&lt;-</span><span class="n">ctx</span><span class="o">.</span><span class="n">Done</span><span class="p">()</span><span class="o">:</span>
    <span class="k">return</span>
<span class="k">case</span> <span class="n">variant</span><span class="p">,</span> <span class="n">ok</span> <span class="o">:=</span> <span class="o">&lt;-</span><span class="no">nil</span><span class="o">:</span> <span class="c">// blocks forever, never selected</span>
    <span class="o">...</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The worker is now parked, waiting only for context cancellation. This is intentional. The alternative would be for the worker to return immediately when <code class="language-plaintext highlighter-rouge">workerChan</code> closes, but that would decrement the <code class="language-plaintext highlighter-rouge">WaitGroup</code> before the worker has necessarily finished sending its last computed location to <code class="language-plaintext highlighter-rouge">locationChan</code>. Staying alive until <code class="language-plaintext highlighter-rouge">ctx.Done()</code> ensures the worker fully drains before the <code class="language-plaintext highlighter-rouge">WaitGroup</code> completes and <code class="language-plaintext highlighter-rouge">locationChan</code> is closed.</p>

<p>Coming from Rust, this all felt foreign. In Rust the compiler enforces the invariants: ownership, <code class="language-plaintext highlighter-rouge">Send</code>/<code class="language-plaintext highlighter-rouge">Sync</code> bounds, lifetime constraints. In Go you reason about it yourself and encode the contract in patterns like this one. The channel syntax took time to internalize, especially <code class="language-plaintext highlighter-rouge">select</code> across multiple channels. I leaned heavily on <a href="https://www.oreilly.com/library/view/learning-go-2nd/9781098139285/">Learning Go</a> by Jon Bodner throughout this project, particularly for the concurrency chapter and general idiomatic patterns. It is a well-written book and I would recommend it to anyone coming from a statically typed language who finds Go’s conventions a bit opaque at first. The mental model holds up once it clicks. Channels are typed queues, goroutines are lightweight threads, and the patterns (worker pool, fan-out, context cancellation) are consistent everywhere you look in the ecosystem.</p>

<h2 id="the-cli-and-web-app">The CLI and Web App</h2>

<p>You can interact with the library through a CLI or the web app at <a href="https://babel.c12i.xyz">babel.c12i.xyz</a>. For the CLI, I used <a href="https://github.com/alecthomas/kong"><code class="language-plaintext highlighter-rouge">kong</code></a>. My bias towards it is straightforward: coming from Rust, I am used to defining CLI commands as structs and letting the framework derive the parser from struct field tags, which is exactly how <code class="language-plaintext highlighter-rouge">clap</code> works. <code class="language-plaintext highlighter-rouge">kong</code> is the closest equivalent in Go, and that familiarity made it an easy call over something like <code class="language-plaintext highlighter-rouge">cobra</code>.</p>

<p>For the web layer, I used <a href="https://github.com/gin-gonic/gin">Gin</a>. It is fast, the routing API is clean, and the choice did not need much deliberation. The UI is mostly AI-generated. I gave Claude some rough requirements for a search form, results list, and browse view with previous/next navigation, and iterated from there. The interesting parts of this project were in the library core, not the frontend.</p>

<hr />

<p>This was a genuinely enjoyable side project. It pushed me further into Go than IaC work ever did, gave me a working understanding of goroutines and channels, and produced something I find kind of beautiful: an infinite library you can search, that requires no storage, generates every page on demand, and always gives you the same answer for the same question.</p>

<p>The code is open source at <a href="https://github.com/c12i/babel-go">github.com/c12i/babel-go</a>. The web app is live at <a href="https://babel.c12i.xyz">babel.c12i.xyz</a>. Go check what the library has to say about your name.</p>]]></content><author><name>Collins Muriuki</name></author><category term="blog" /><category term="Go" /><category term="programming" /><category term="library-of-babel" /><category term="concurrency" /><summary type="html"><![CDATA[A deep dive into building a Go implementation of the Library of Babel, exploring deterministic randomness, base29 encoding, and what it taught me about Go's concurrency model.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://collinsmuriuki.xyz/assets/images/library-of-babel-cover.jpg" /><media:content medium="image" url="https://collinsmuriuki.xyz/assets/images/library-of-babel-cover.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Building Bunge Bits, an AI-Powered Summary Pipeline for the Parliament of Kenya</title><link href="https://collinsmuriuki.xyz/building-bunge-bits/" rel="alternate" type="text/html" title="Building Bunge Bits, an AI-Powered Summary Pipeline for the Parliament of Kenya" /><published>2025-06-12T19:00:00+03:00</published><updated>2025-06-12T19:00:00+03:00</updated><id>https://collinsmuriuki.xyz/building-bunge-bits</id><content type="html" xml:base="https://collinsmuriuki.xyz/building-bunge-bits/"><![CDATA[<h1 id="motivation">Motivation</h1>

<p>I built bunge-bits out of frustration. Kenya’s National Assembly and Senate regularly livestream their proceedings, but these sessions are long, often poorly indexed, and inaccessible to most people. The average citizen doesn’t have the time, or bandwidth, to sit through up to five-hour videos of parliamentary debates. Most people end up relying on the media to highlight key moments, often missing the full context or nuance.</p>

<p>At the same time, Kenya is experiencing a generational shift. Young people are increasingly vocal about governance, public spending, and corruption. I wanted to contribute something tangible to that movement, something that makes civic discussions more transparent, digestible, and shareable.</p>

<p>Bunge Bits is my attempt to bridge that gap: an AI-powered pipeline that listens to Parliament so citizens don’t have to, and summarizes the highlights in plain markdown.</p>

<h1 id="the-stack">The Stack</h1>

<p>This project leans heavily on modern AI tools and a reliable Rust backbone:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">Rust</code>: For performance, error handling, and concurrency.</li>
  <li><code class="language-plaintext highlighter-rouge">yt-dlp</code>: To fetch livestream recordings from YouTube.</li>
  <li><code class="language-plaintext highlighter-rouge">ffmpeg</code>: For slicing audio into manageable chunks.</li>
  <li><code class="language-plaintext highlighter-rouge">OpenAI Whisper</code>: To transcribe spoken audio to text.</li>
  <li><code class="language-plaintext highlighter-rouge">GPT-4</code>: To summarize transcripts into clear, structured markdown.</li>
  <li><code class="language-plaintext highlighter-rouge">Postgres</code>: For storing metadata and summaries.</li>
  <li><code class="language-plaintext highlighter-rouge">Cron jobs</code>: For scheduling runs and cleaning up old artifacts.</li>
</ul>

<p>Here’s how data flows through the system:</p>

<p align="center">
  <img src="/assets/images/bunge-bits-pipeline.png" alt="bunge-bits Pipeline Diagram" width="200" />
</p>

<h1 id="engineering-challenges">Engineering Challenges</h1>

<p>Some parts of this were painful:</p>

<ul>
  <li>Whisper often mangled Kenyan names: “John Mbadi” became “John Birdy”, and it only got worse with native pronunciations.</li>
  <li>Bad transcriptions created hallucinated summaries: GPT-4 would fill in gaps with confident nonsense.</li>
  <li>OpenAI context limits: Long transcripts could easily exceed GPT’s token window, making summarization error-prone or impossible.</li>
  <li>Multiple streams per day: My VPS filled up fast with raw audio and transcripts.</li>
  <li>Chunk order and retries: Ensuring chunks were in sequence and retried correctly took careful design.</li>
  <li>Summary formatting: I needed markdown output with clean headings, bullets, and time-stamped key moments.</li>
</ul>

<h1 id="solutions-and-architecture-decisions">Solutions and Architecture Decisions</h1>

<p>To tackle these, I made a few key calls:</p>

<ul>
  <li>Name mangling and hallucinations: To improve recognition of Kenyan public figures, I enrich GPT-4 prompts with curated lists of MPs, Senators, and Cabinet Secretaries. This leverages both GPT’s internal knowledge and its ability to ground responses via web search, reducing hallucinations and transcription errors.</li>
  <li>System/user prompt separation: This helps enforce markdown formatting, avoid emojis, and ensure consistent structure.</li>
  <li>Linear, contextual summarization pipeline: This was the biggest challenge. To avoid exceeding OpenAI’s context window, I chunk transcripts and summarize each piece separately, feeding each new summary back in as context for the next. The final output is then stitched together in a “combine” step. This closure-based approach let me scale summaries without blowing the token limit.</li>
  <li>Batching via cron: The job runs every 12 hours, fetching and processing only new streams. To conserve storage on the VPS, each run processes a fixed number of unprocessed streams (currently capped at 3), prioritized by upload date. This prevents overload during periods of high parliamentary activity.</li>
  <li>Separate cron for cleanup: A lightweight system cron wipes old audio/transcripts after processing to avoid disk bloat.</li>
</ul>

<h1 id="whats-next">What’s Next</h1>

<p>There’s a lot more I want to do (in order of priority):</p>

<ul>
  <li>A public UI where users can browse summaries and filter by topic or MP.</li>
  <li>Automatic timestamp injection for key moments (e.g. heated debates, bill passages, speaker interventions) to make summaries easier to navigate — this is a planned feature.</li>
  <li>The repo is already open source, but I plan to apply for OpenAI credits, civic tech grants, or journalism fellowships to fund ongoing compute and infrastructure costs.</li>
  <li>Eventually, I’d love to collaborate with civil society groups, media houses, or transparency orgs to expand its reach.</li>
</ul>

<p><a href="https://github.com/c12i/bunge-bits">View the code on GitHub</a></p>]]></content><author><name>Collins Muriuki</name></author><category term="blog" /><category term="Rust" /><category term="AI" /><category term="CivicTech" /><category term="Kenya" /><category term="GPT-4" /><category term="Whisper" /><summary type="html"><![CDATA[A deep dive into how I used Rust, Whisper, and GPT-4 to build a reliable AI pipeline that summarizes livestreams of Kenya’s Parliament, making civic proceedings more accessible to everyday citizens.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://collinsmuriuki.xyz/assets/images/bunge-bits.png" /><media:content medium="image" url="https://collinsmuriuki.xyz/assets/images/bunge-bits.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">The Quest for Compile-Time String Formatting in Rust</title><link href="https://collinsmuriuki.xyz/compile-time-string-formatting/" rel="alternate" type="text/html" title="The Quest for Compile-Time String Formatting in Rust" /><published>2024-09-01T02:30:00+03:00</published><updated>2024-09-01T02:30:00+03:00</updated><id>https://collinsmuriuki.xyz/compile-time-string-formatting</id><content type="html" xml:base="https://collinsmuriuki.xyz/compile-time-string-formatting/"><![CDATA[<p>Imagine you’re deep in Rust code, building a CLI tool that’s going to make waves in the developer community. You’ve got your <code class="language-plaintext highlighter-rouge">clap</code> derive macros set up perfectly, ready to handle command parsing with ease. But then you hit a snag. You need to concatenate some constants for your <code class="language-plaintext highlighter-rouge">about</code> attribute to show compatibility information, and Rust isn’t playing ball.</p>

<p>Welcome to Rust, where safety and zero-cost abstractions reign supreme, but sometimes the simplest tasks can become unexpected challenges.</p>

<h2 id="the-problem-rusts-compile-time-conundrum">The Problem: Rust’s Compile-Time Conundrum</h2>

<p>Why can’t Rust handle simple string concatenation at compile time? Well, it’s not as straightforward as you might think.</p>

<p>Let’s look at an example that seems like it should work, but doesn’t:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">use</span> <span class="nn">clap</span><span class="p">::</span><span class="n">Parser</span><span class="p">;</span>

<span class="k">const</span> <span class="n">TOOL_NAME</span><span class="p">:</span> <span class="o">&amp;</span><span class="nb">str</span> <span class="o">=</span> <span class="s">"SuperTool"</span><span class="p">;</span>
<span class="k">const</span> <span class="n">COMPATIBLE_CRATE</span><span class="p">:</span> <span class="o">&amp;</span><span class="nb">str</span> <span class="o">=</span> <span class="s">"AwesomeDependency"</span><span class="p">;</span>
<span class="k">const</span> <span class="n">COMPATIBLE_VERSION</span><span class="p">:</span> <span class="o">&amp;</span><span class="nb">str</span> <span class="o">=</span> <span class="s">"1.2.3"</span><span class="p">;</span>

<span class="c1">// This doesn't work!</span>
<span class="k">const</span> <span class="n">ABOUT</span><span class="p">:</span> <span class="nb">String</span> <span class="o">=</span> <span class="nd">format!</span><span class="p">(</span>
    <span class="s">"{} - Compatible with {} version {}"</span><span class="p">,</span>
    <span class="n">TOOL_NAME</span><span class="p">,</span> <span class="n">COMPATIBLE_CRATE</span><span class="p">,</span> <span class="n">COMPATIBLE_VERSION</span>
<span class="p">);</span>

<span class="nd">#[derive(Parser,</span> <span class="nd">Debug)]</span>
<span class="nd">#[command(author</span> <span class="nd">=</span> <span class="s">"Alice"</span><span class="nd">,</span> <span class="nd">version,</span> <span class="nd">about</span> <span class="nd">=</span> <span class="nd">ABOUT)]</span>
<span class="k">struct</span> <span class="n">Cli</span> <span class="p">{</span>
    <span class="c1">// CLI arguments would go here</span>
<span class="p">}</span>

<span class="k">fn</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">let</span> <span class="n">args</span> <span class="o">=</span> <span class="nn">Cli</span><span class="p">::</span><span class="nf">parse</span><span class="p">();</span>
    <span class="nd">println!</span><span class="p">(</span><span class="s">"{:?}"</span><span class="p">,</span> <span class="n">args</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Try to compile this, and Rust responds with an error:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>error[E0015]: calls in constants are limited to constant functions, tuple structs and tuple variants
 --&gt; src/main.rs:8:21
  |
8 | const ABOUT: String = format!(
  |                       ^^^^^^ ...

error: aborting due to previous error
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">format!</code> macro, usually so helpful for string formatting, suddenly becomes unavailable in the compile-time context.</p>

<p>Rust’s const evaluation capabilities, while continuously improving, still have limitations. The language designers have prioritized other features, leaving compile-time string formatting as a challenge to overcome.</p>

<h2 id="the-solution-leveraging-crates">The Solution: Leveraging Crates</h2>

<p>Fortunately, the Rust ecosystem provides solutions. One such solution is the <a href="https://docs.rs/const_format/latest/const_format/index.html"><code class="language-plaintext highlighter-rouge">const_format</code></a> crate, which allows for compile-time string formatting.</p>

<p>With <code class="language-plaintext highlighter-rouge">const_format</code>, you can write:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">use</span> <span class="nn">const_format</span><span class="p">::</span><span class="n">formatcp</span><span class="p">;</span>

<span class="k">const</span> <span class="n">TOOL_NAME</span><span class="p">:</span> <span class="o">&amp;</span><span class="nb">str</span> <span class="o">=</span> <span class="s">"SuperTool"</span><span class="p">;</span>
<span class="k">const</span> <span class="n">COMPATIBLE_CRATE</span><span class="p">:</span> <span class="o">&amp;</span><span class="nb">str</span> <span class="o">=</span> <span class="s">"AwesomeDependency"</span><span class="p">;</span>
<span class="k">const</span> <span class="n">COMPATIBLE_VERSION</span><span class="p">:</span> <span class="o">&amp;</span><span class="nb">str</span> <span class="o">=</span> <span class="s">"1.2.3"</span><span class="p">;</span>
<span class="k">const</span> <span class="n">ABOUT</span><span class="p">:</span> <span class="o">&amp;</span><span class="nb">str</span> <span class="o">=</span> <span class="nd">formatcp!</span><span class="p">(</span>
    <span class="s">"{} - Compatible with {} version {}"</span><span class="p">,</span>
    <span class="n">TOOL_NAME</span><span class="p">,</span> <span class="n">COMPATIBLE_CRATE</span><span class="p">,</span> <span class="n">COMPATIBLE_VERSION</span>
<span class="p">);</span>
</code></pre></div></div>

<p>This approach gives us the compile-time string formatting we need.</p>

<h2 id="an-alternative-build-scripts">An Alternative: Build Scripts</h2>

<p>There’s another way to achieve compile-time string formatting without additional crates: build scripts and environment variables.</p>

<p>By using a <code class="language-plaintext highlighter-rouge">build.rs</code> file, we can generate our formatted strings at compile time and pass them to our main code as environment variables. Here’s how:</p>

<ol>
  <li>Create a <code class="language-plaintext highlighter-rouge">build.rs</code> file in your project root:</li>
</ol>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">let</span> <span class="n">tool_name</span> <span class="o">=</span> <span class="s">"SuperTool"</span><span class="p">;</span>
    <span class="k">let</span> <span class="n">compatible_crate</span> <span class="o">=</span> <span class="s">"AwesomeDependency"</span><span class="p">;</span>
    <span class="k">let</span> <span class="n">compatible_version</span> <span class="o">=</span> <span class="s">"1.2.3"</span><span class="p">;</span>
    <span class="k">let</span> <span class="n">about</span> <span class="o">=</span> <span class="nd">format!</span><span class="p">(</span>
        <span class="s">"{} - Compatible with {} version {}"</span><span class="p">,</span>
        <span class="n">tool_name</span><span class="p">,</span> <span class="n">compatible_crate</span><span class="p">,</span> <span class="n">compatible_version</span>
    <span class="p">);</span>

    <span class="nd">println!</span><span class="p">(</span><span class="s">"cargo:rustc-env=ABOUT={}"</span><span class="p">,</span> <span class="n">about</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<ol>
  <li>In your <code class="language-plaintext highlighter-rouge">main.rs</code>, use the environment variable:</li>
</ol>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">use</span> <span class="nn">clap</span><span class="p">::</span><span class="n">Parser</span><span class="p">;</span>

<span class="nd">#[derive(Parser,</span> <span class="nd">Debug)]</span>
<span class="nd">#[command(author</span> <span class="nd">=</span> <span class="s">"Alice"</span><span class="nd">,</span> <span class="nd">version,</span> <span class="nd">about</span> <span class="nd">=</span> <span class="nd">env</span><span class="err">!</span><span class="nd">(</span><span class="s">"ABOUT"</span><span class="nd">))]</span>
<span class="k">struct</span> <span class="n">Cli</span> <span class="p">{</span>
    <span class="c1">// CLI arguments would go here</span>
<span class="p">}</span>

<span class="k">fn</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">let</span> <span class="n">args</span> <span class="o">=</span> <span class="nn">Cli</span><span class="p">::</span><span class="nf">parse</span><span class="p">();</span>
    <span class="nd">println!</span><span class="p">(</span><span class="s">"{:?}"</span><span class="p">,</span> <span class="n">args</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This method allows us to perform the string formatting during the build process and inject the result into our main code.</p>

<h2 id="lessons-learned">Lessons Learned</h2>

<p>This journey through compile-time string formatting in Rust teaches us a few valuable lessons:</p>

<ol>
  <li>Rust prioritizes safety and performance, sometimes at the cost of convenience.</li>
  <li>The Rust ecosystem often provides solutions for language limitations.</li>
  <li>Build scripts are powerful tools that can help overcome certain restrictions.</li>
  <li>Sometimes, the simplest solutions (like using environment variables) can solve complex problems.</li>
  <li>With some creativity, we can usually find a way around language limitations.</li>
</ol>

<p>While Rust may not support compile-time formatting out of the box, we have multiple ways to achieve our goals. Whether it’s through community-created crates or clever use of build scripts, Rust developers always find a way forward.</p>]]></content><author><name>Collins Muriuki</name></author><category term="blog" /><category term="rust" /><category term="programming" /><category term="cli" /><category term="compile-time" /><category term="string-formatting" /><summary type="html"><![CDATA[Exploring the challenges and solutions for compile-time string formatting in Rust CLI applications]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://collinsmuriuki.xyz/assets/images/rust-crab.png" /><media:content medium="image" url="https://collinsmuriuki.xyz/assets/images/rust-crab.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Building a Web Assembly powered password generator</title><link href="https://collinsmuriuki.xyz/wasm-tutorial/" rel="alternate" type="text/html" title="Building a Web Assembly powered password generator" /><published>2020-10-24T22:44:00+03:00</published><updated>2020-10-24T22:44:00+03:00</updated><id>https://collinsmuriuki.xyz/wasm-tutorial</id><content type="html" xml:base="https://collinsmuriuki.xyz/wasm-tutorial/"><![CDATA[<h1 id="building-a-web-assembly-powered-password-generator-with-rust-and-react">Building a Web Assembly-powered Password Generator with Rust and React</h1>

<h2 id="introduction">Introduction</h2>

<p>In this tutorial, we will explore the fascinating intersection of Rust, Web Assembly (Wasm), and React by building a simple password generator. We’ll leverage the speed and performance benefits of Rust, compile it into Wasm, and seamlessly integrate it into a React application. By the end of this project, you’ll have a clear understanding of how these technologies work together.</p>

<h2 id="why-rust-and-web-assembly">Why Rust and Web Assembly?</h2>

<p>Web Assembly is a low-level virtual machine that allows executing code at near-native speeds within web browsers. Rust, known for its speed and memory safety, is an ideal language for Wasm. Unlike high-level languages like JavaScript, Rust provides developers with low-level control and reliable performance, making it an excellent choice for Wasm.</p>

<h2 id="prerequisites">Prerequisites</h2>

<p>Before we dive into the project, ensure you have the following tools and knowledge:</p>

<ul>
  <li>Rust and Cargo: Install them using <a href="https://www.rust-lang.org/tools/install">this link</a>.</li>
  <li>wasm-pack: A tool for building and working with Rust-generated Web Assembly. Install it <a href="https://rustwasm.github.io/wasm-pack/installer/">here</a>.</li>
  <li>Node.js: Required for React development. Install it from <a href="https://nodejs.org/en/">nodejs.org</a>.</li>
  <li>Basic knowledge of Rust and JavaScript/React.js.</li>
</ul>

<h2 id="initializing-the-rust-project">Initializing the Rust Project</h2>

<p>Let’s start by creating a new Rust Web Assembly project using the following command:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>wasm-pack new wasm_pass
</code></pre></div></div>

<p>This command generates a Rust library crate with Web Assembly support. The crucial files are <code class="language-plaintext highlighter-rouge">Cargo.toml</code>, <code class="language-plaintext highlighter-rouge">src/lib.rs</code>, and <code class="language-plaintext highlighter-rouge">src/utils.rs</code>.</p>

<h2 id="dissecting-the-boilerplate">Dissecting the Boilerplate</h2>

<p>Explore the generated project structure:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>wasm_pass/
├── Cargo.toml
├── LICENSE_APACHE
├── LICENSE_MIT
├── README.md
└── src
    ├── lib.rs
    └── utils.rs
</code></pre></div></div>

<ul>
  <li><strong>Cargo.toml</strong>: Contains project dependencies and metadata.</li>
  <li><strong>lib.rs</strong>: The root of the crate, compiled into Web Assembly.</li>
  <li><strong>utils.rs</strong>: Includes debugging utilities (not used in this project).</li>
</ul>

<h2 id="initial-build">Initial Build</h2>

<p>Build the project using:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>wasm-pack build
</code></pre></div></div>

<p>This creates a <code class="language-plaintext highlighter-rouge">pkg</code> directory with the following artifacts:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pkg/
├── package.json
├── README.md
├── wasm_pass.wasm
├── wasm_pass.d.ts
└── wasm_pass.js
</code></pre></div></div>

<p>These files enable npm integration and contain the compiled Web Assembly code.</p>

<h2 id="writing-rust-code">Writing Rust Code</h2>

<p>Update the <code class="language-plaintext highlighter-rouge">Cargo.toml</code> file to include the <code class="language-plaintext highlighter-rouge">rand</code> crate:</p>

<div class="language-toml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[dependencies]</span>
<span class="py">wasm-bindgen</span> <span class="p">=</span> <span class="s">"0.2.63"</span>
<span class="nn">rand</span> <span class="o">=</span> <span class="p">{</span> <span class="py">version</span> <span class="p">=</span> <span class="s">"0.7.3"</span><span class="p">,</span> <span class="py">features</span> <span class="p">=</span> <span class="nn">["wasm-bindgen"]</span> <span class="p">}</span>
</code></pre></div></div>

<p>This adds the random number generation utility to our project. Run <code class="language-plaintext highlighter-rouge">cargo build</code> to download the new dependency.</p>

<p>Implement the password generator logic in <code class="language-plaintext highlighter-rouge">src/lib.rs</code>:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">use</span> <span class="nn">wasm_bindgen</span><span class="p">::</span><span class="nn">prelude</span><span class="p">::</span><span class="o">*</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">rand</span><span class="p">::</span><span class="n">Rng</span><span class="p">;</span>

<span class="nd">#[wasm_bindgen]</span>
<span class="k">pub</span> <span class="k">fn</span> <span class="nf">generate</span><span class="p">(</span><span class="n">len</span><span class="p">:</span> <span class="nb">usize</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">String</span> <span class="p">{</span>
    <span class="k">const</span> <span class="n">CHARSET</span><span class="p">:</span> <span class="o">&amp;</span><span class="p">[</span><span class="nb">u8</span><span class="p">]</span> <span class="o">=</span> <span class="s">"ABCDEFGHIJKLMNOPQRSTUVWXYZ</span><span class="err">\</span><span class="s">
                            abcdefghijklmnopqrstuvwxyz</span><span class="err">\</span><span class="s">
                            0123456789)(*&amp;^%$#@!~"</span><span class="nf">.as_bytes</span><span class="p">();</span>

    <span class="k">let</span> <span class="k">mut</span> <span class="n">rng</span> <span class="o">=</span> <span class="nn">rand</span><span class="p">::</span><span class="nf">thread_rng</span><span class="p">();</span>

    <span class="k">let</span> <span class="n">password</span><span class="p">:</span> <span class="nb">String</span> <span class="o">=</span> <span class="p">(</span><span class="mi">0</span><span class="o">..</span><span class="n">len</span><span class="p">)</span>
        <span class="nf">.map</span><span class="p">(|</span><span class="n">_</span><span class="p">|</span> <span class="p">{</span>
            <span class="k">let</span> <span class="n">idx</span> <span class="o">=</span> <span class="n">rng</span><span class="nf">.gen_range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">CHARSET</span><span class="nf">.len</span><span class="p">());</span>
            <span class="n">CHARSET</span><span class="p">[</span><span class="n">idx</span><span class="p">]</span> <span class="k">as</span> <span class="nb">char</span>
        <span class="p">})</span>
        <span class="nf">.collect</span><span class="p">();</span>
    <span class="n">password</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This function generates a random password of the specified length using the <code class="language-plaintext highlighter-rouge">rand</code> crate.</p>

<h2 id="testing-the-code">Testing the Code</h2>

<p>Write a test for the <code class="language-plaintext highlighter-rouge">generate</code> function in <code class="language-plaintext highlighter-rouge">src/lib.rs</code>:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">#[cfg(test)]</span>
<span class="k">mod</span> <span class="n">tests</span> <span class="p">{</span>
    <span class="k">use</span> <span class="k">super</span><span class="p">::</span><span class="n">generate</span><span class="p">;</span>

    <span class="nd">#[test]</span>
    <span class="k">fn</span> <span class="nf">test_generate</span><span class="p">()</span> <span class="p">{</span>
        <span class="k">let</span> <span class="n">password</span> <span class="o">=</span> <span class="nf">generate</span><span class="p">(</span><span class="mi">20</span><span class="p">);</span>
        <span class="nd">assert_eq!</span><span class="p">(</span><span class="n">password</span><span class="nf">.len</span><span class="p">(),</span> <span class="mi">20</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Run the tests with:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cargo <span class="nb">test</span> <span class="nt">--</span> <span class="nt">--show-output</span>
</code></pre></div></div>

<p>Ensure the tests pass, confirming the functionality of the password generator.</p>

<h2 id="implementing-web-assembly-in-a-react-app">Implementing Web Assembly in a React App</h2>

<p>Create a React app in the <code class="language-plaintext highlighter-rouge">app</code> directory:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir </span>app
<span class="nb">cd </span>app
<span class="nb">mkdir </span>src
<span class="nb">echo</span> <span class="s2">"/node_modules"</span> <span class="o">&gt;</span> .gitignore
npm init <span class="nt">-y</span>
</code></pre></div></div>

<p>Install necessary dependencies:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm <span class="nb">install</span> <span class="nt">--save</span> react react-dom
npm <span class="nb">install</span> <span class="nt">--save-dev</span> @babel/core @babel/preset-env @babel/preset-react babel-loader webpack webpack-cli webpack-dev-server html-webpack-plugin style-loader css-loader html-loader
</code></pre></div></div>

<p>Create the React app files: <code class="language-plaintext highlighter-rouge">index.html</code>, <code class="language-plaintext highlighter-rouge">index.js</code>, and <code class="language-plaintext highlighter-rouge">App.js</code>. Configure Babel with <code class="language-plaintext highlighter-rouge">.babelrc</code> and Webpack with <code class="language-plaintext highlighter-rouge">webpack.config.js</code>.</p>

<p>Finally, run the development server:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm start
</code></pre></div></div>

<h2 id="using-the-package-in-react">Using the Package in React</h2>

<p>Update the <code class="language-plaintext highlighter-rouge">App.js</code> file to use the Rust-generated Web Assembly package:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">React</span><span class="p">,</span> <span class="p">{</span> <span class="nx">useState</span><span class="p">,</span> <span class="nx">useCallback</span><span class="p">,</span> <span class="nx">useEffect</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">react</span><span class="dl">"</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">App</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="p">[</span><span class="nx">password</span><span class="p">,</span> <span class="nx">setPassword</span><span class="p">]</span> <span class="o">=</span> <span class="nx">useState</span><span class="p">(</span><span class="dl">""</span><span class="p">);</span>
    <span class="kd">const</span> <span class="p">[</span><span class="nx">input</span><span class="p">,</span> <span class="nx">setInput</span><span class="p">]</span> <span class="o">=</span> <span class="nx">useState</span><span class="p">(</span><span class="dl">""</span><span class="p">);</span>

    <span class="kd">const</span> <span class="nx">handleChange</span> <span class="o">=</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="nx">setInput</span><span class="p">(</span><span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">value</span><span class="p">);</span>
    <span class="p">};</span>

    <span class="kd">const</span> <span class="nx">generatePassword</span> <span class="o">=</span> <span class="nx">useCallback</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="kd">const</span> <span class="nx">module</span> <span class="o">=</span> <span class="k">import</span><span class="p">(</span><span class="dl">"</span><span class="s2">wasm-pass</span><span class="dl">"</span><span class="p">)</span>
        <span class="nx">module</span><span class="p">.</span><span class="nx">then</span><span class="p">(({</span><span class="nx">generate</span><span class="p">})</span> <span class="o">=&gt;</span> <span class="p">{</span>
            <span class="nx">setPassword</span><span class="p">(</span><span class="nx">generate</span><span class="p">(</span><span class="nb">parseInt</span><span class="p">(</span><span class="nx">input</span><span class="p">)))</span>
        <span class="p">}).</span><span class="k">catch</span><span class="p">(</span><span class="nx">err</span> <span class="o">=&gt;</span> <span class="p">{</span>
            <span class="nx">alert</span><span class="p">(</span><span class="nx">err</span><span class="p">.</span><span class="nx">toString</span><span class="p">())</span>
        <span class="p">})</span>
    <span class="p">},</span> <span class="p">[</span><span class="nx">input</span><span class="p">]);</span>

    <span class="nx">useEffect</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="nx">generatePassword</span><span class="p">()</span>
    <span class="p">},</span> <span class="p">[</span><span class="nx">input</span><span class="p">]);</span>

    <span class="k">return</span> <span class="p">(</span>
        <span class="o">&lt;</span><span class="nx">div</span><span class="o">&gt;</span>
            <span class="o">&lt;</span><span class="nx">p</span><span class="o">&gt;</span><span class="nx">Enter</span> <span class="nx">password</span> <span class="na">length</span><span class="p">:</span><span class="o">&lt;</span><span class="sr">/p</span><span class="err">&gt;
</span>            <span class="o">&lt;</span><span class="nx">input</span> <span class="nx">onChange</span><span class="o">=</span><span class="p">{</span><span class="nx">handleChange</span><span class="p">}</span> <span class="nx">type</span><span class="o">=</span><span class="dl">"</span><span class="s2">number</span><span class="dl">"</span> <span class="nx">value</span><span class="o">=</span><span class="p">{</span><span class="nx">input</span><span class="p">}</span> <span class="sr">/</span><span class="err">&gt;
</span>            <span class="o">&lt;</span><span class="nx">button</span> <span class="nx">onClick</span><span class="o">=</span><span class="p">{</span><span class="nx">generatePassword</span><span class="p">}</span><span class="o">&gt;</span><span class="nx">Generate</span> <span class="nx">Password</span><span class="o">&lt;</span><span class="sr">/button</span><span class="err">&gt;
</span>            <span class="o">&lt;</span><span class="nx">p</span><span class="o">&gt;</span><span class="nx">Your</span> <span class="na">password</span><span class="p">:</span><span class="o">&lt;</span><span class="sr">/p</span><span class="err">&gt;
</span>            <span class="o">&lt;</span><span class="nx">strong</span><span class="o">&gt;</span><span class="p">{</span><span class="nx">password</span><span class="p">}</span><span class="o">&lt;</span><span class="sr">/strong</span><span class="err">&gt;
</span>        <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt;
</span>    <span class="p">);</span>
<span class="p">};</span>

<span class="k">export</span> <span class="k">default</span> <span class="nx">App</span><span class="p">;</span>
</code></pre></div></div>

<p>This React component allows users to input a password length, click a button, and receive a randomly generated password from the Rust Web Assembly package.</p>

<h2 id="conclusion">Conclusion</h2>

<p>This tutorial showcases the integration of Rust and Web Assembly in a React application, providing a powerful blend of performance and modern front-end technologies. Explore further resources to deepen your understanding of Rust and Web Assembly:</p>

<ul>
  <li><a href="https://doc.rust-lang.org/book/">The Rust Book</a></li>
  <li><a href="https://rustwasm.github.io/docs/book/">Rust and Web Assembly Book</a></li>
  <li><a href="https://rustwasm.github.io/docs/wasm-bindgen/introduction.html">wasm-bindgen Documentation</a></li>
  <li><a href="https://wasmbyexample.dev/home.en-us.html">Wasm By Example</a></li>
  <li><a href="https://webassembly.org/">WebAssembly.org</a></li>
</ul>

<p>Check the <a href="https://wasm-pass.collinsmuriuki.xyz">full working implementation</a> of this project for reference. Happy coding!</p>]]></content><author><name>collinsmuriuki</name></author><category term="blog" /><category term="rust" /><category term="wasm" /><category term="react.js" /><category term="javascript" /><summary type="html"><![CDATA[Building a Web Assembly powered password generator with rust-wasm and React.js.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://collinsmuriuki.xyz/assets/images/rust-ferris.jpeg" /><media:content medium="image" url="https://collinsmuriuki.xyz/assets/images/rust-ferris.jpeg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Optimizing Node.js Memory Allocation for High-Performance Applications</title><link href="https://collinsmuriuki.xyz/node-memory/" rel="alternate" type="text/html" title="Optimizing Node.js Memory Allocation for High-Performance Applications" /><published>2018-05-23T19:00:00+03:00</published><updated>2018-05-23T19:00:00+03:00</updated><id>https://collinsmuriuki.xyz/node-memory</id><content type="html" xml:base="https://collinsmuriuki.xyz/node-memory/"><![CDATA[<h2 id="introduction">Introduction</h2>

<p>If you’re frequently developing and deploying Node.js applications, encountering the infamous <code class="language-plaintext highlighter-rouge">FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed</code> error is not uncommon. This issue arises particularly when your server engages in memory-intensive tasks, such as handling large data transfers with limited system resources.</p>

<h2 id="understanding-the-v8-engine">Understanding the V8 Engine</h2>

<p>Node.js relies on Google’s V8 engine, an open-source, high-performance JavaScript and WebAssembly engine written in C++. Integrated into both Chrome and Node.js, V8 implements ECMAScript and WebAssembly and caters to various operating systems and processor architectures.</p>

<p>When a Node.js process begins, V8 allocates a specific amount of memory based on your hardware specifications. Monitoring and managing this allocated memory is crucial, especially when dealing with resource-demanding applications.</p>

<h2 id="checking-allocated-memory">Checking Allocated Memory</h2>

<p>To gain insights into your process’s allocated memory, create an <code class="language-plaintext highlighter-rouge">index.js</code> script with the following code:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">v8</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">v8</span><span class="dl">"</span><span class="p">);</span>

<span class="kd">const</span> <span class="nx">totalHeapSize</span> <span class="o">=</span> <span class="nx">v8</span><span class="p">.</span><span class="nx">getHeapStatistics</span><span class="p">().</span><span class="nx">total_available_size</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">totalHeapSizeInGB</span> <span class="o">=</span> <span class="p">(</span><span class="nx">totalHeapSize</span> <span class="o">/</span> <span class="mi">1024</span> <span class="o">/</span> <span class="mi">1024</span> <span class="o">/</span> <span class="mi">1024</span><span class="p">).</span><span class="nx">toFixed</span><span class="p">(</span><span class="mi">2</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span>
    <span class="s2">`Total Heap Size is </span><span class="p">${</span><span class="nx">totalHeapSize</span><span class="p">}</span><span class="s2"> bytes`</span><span class="p">,</span>
    <span class="s2">`</span><span class="p">${</span><span class="nx">totalHeapSizeInGB</span><span class="p">}</span><span class="s2"> GB`</span>
<span class="p">);</span>
</code></pre></div></div>

<p>This script utilizes the <code class="language-plaintext highlighter-rouge">v8</code> module, an integral part of Node.js, offering methods like <code class="language-plaintext highlighter-rouge">v8.getHeapStatistics()</code> to fetch heap-related memory information.</p>

<h3 id="adjusting-memory-capacity">Adjusting Memory Capacity</h3>

<p>You can increase the memory capacity by using the <code class="language-plaintext highlighter-rouge">--max-old-space-size</code> argument when running your script, specifying the desired memory allocation in kilobytes:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>node <span class="nt">--max-old-space-size</span><span class="o">=</span>4096 index.js
</code></pre></div></div>

<p>Upon rerunning the script, you should observe an augmented total heap size:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Total Heap Size is 4283117000,   GB 3.99
</code></pre></div></div>

<p>In this example, the memory has been doubled, providing more breathing room for your processes.</p>

<h2 id="important-considerations">Important Considerations</h2>

<p>Before adjusting memory allocation, ensure you have sufficient free memory. Attempting to allocate more memory than available on your system is counterproductive.</p>

<p>Moreover, prioritize refactoring your code for better memory efficiency as a primary strategy before resorting to memory allocation adjustments.</p>

<h2 id="conclusion">Conclusion</h2>

<p>Optimizing memory allocation is a key aspect of maintaining high-performance Node.js applications. By understanding the V8 engine’s role and implementing judicious memory adjustments, you can enhance your application’s stability and responsiveness. Always balance these optimizations with efficient code practices to achieve optimal results.</p>]]></content><author><name>collinsmuriuki</name></author><category term="blog" /><category term="node" /><category term="javascript" /><summary type="html"><![CDATA[Optimizing Node.js Memory Allocation for High-Performance Applications.]]></summary></entry></feed>