<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>engineering &amp;mdash; laxmena</title>
    <link>https://laxmena.com/tag:engineering</link>
    <description></description>
    <pubDate>Wed, 15 Apr 2026 01:21:20 +0000</pubDate>
    <image>
      <url>https://i.snap.as/n9575tJN.png</url>
      <title>engineering &amp;mdash; laxmena</title>
      <link>https://laxmena.com/tag:engineering</link>
    </image>
    <item>
      <title>Hone vs. The 1 Billion Row Challenge</title>
      <link>https://laxmena.com/hone-vs-the-1-billion-row-challenge?pk_campaign=rss-feed</link>
      <description>&lt;![CDATA[1,000,000,000 rows of data. No hand-tuning. Just an agent, a benchmark, and a budget.&#xA;&#xA;The 1 Billion Row Challenge is simple on paper: read a file with 1B rows of weather station measurements, compute min/mean/max per station, as fast as possible. In Python, a naive solution takes minutes. The best human-optimized ones use memory-mapped files, multiprocessing, and numpy.&#xA;&#xA;I&#39;m not optimizing it by hand. I&#39;m giving it to Hone — and letting it figure it out.&#xA;&#xA;Hone is now on PyPI. Install it with pip install hone-ai.&#xA;&#xA;This is a living document. I&#39;ll update it as each run completes. Follow the code at laxmena/hone-1brc.&#xA;&#xA;!--more--&#xA;&#xA;---&#xA;&#xA;The Setup&#xA;&#xA;The challenge: Parse a 1B-row file. Each row: Hamburg;12.0. Compute min/mean/max per station. Print results sorted alphabetically.&#xA;&#xA;The metric: Wall-clock runtime in seconds. Lower is better.&#xA;&#xA;The constraints: Python standard library only. No numpy, no pandas, no third-party packages. Correctness must be preserved — output format and values must not change.&#xA;&#xA;The baseline: Simple. Correct. Slow. One thread, one line at a time, float() on every value.&#xA;&#xA;---&#xA;&#xA;Results at a Glance&#xA;&#xA;| Run | Model | Dataset | Baseline | Optimized | Improvement |&#xA;|-----|-------|---------|----------|-----------|-------------|&#xA;| 1 | Haiku | 1M rows | 0.546s | 0.471s | 13.7% |&#xA;| 2 | Haiku | 100M rows | 47.197s | 42.739s | 9.4% |&#xA;| 3 | Sonnet | 100M rows | 48.104s | 10.110s | 79% |&#xA;| 4 | Sonnet (100M solution, no re-run) | 1B rows | 487.525s | 130.080s | 73.3% |&#xA;| 5 | Sonnet | 1B rows | 487.525s | 90.929s | 81.4% |&#xA;&#xA;---&#xA;&#xA;Episode 1: Haiku, 1M rows — 13.7% faster&#xA;March 25, 2026&#xA;&#xA;0.546s → 0.471s&#xA;&#xA;First run: claude-haiku-4-5, 1M rows, $5 budget, 50 max iterations.&#xA;&#xA;The 13.7% gain looks decent on paper. It isn&#39;t. The absolute numbers are tiny — we&#39;re talking 75 milliseconds. At this scale, Python startup time and OS disk caching dominate. The agent is optimizing noise, not the algorithm. Haiku made incremental tweaks but never found a structural breakthrough.&#xA;&#xA;Wrong dataset size. Move on.&#xA;&#xA;---&#xA;&#xA;Hone v1.2.0: --goal-file&#xA;March 25, 2026&#xA;&#xA;Episode 1 exposed a friction point. Pasting a long goal string into the terminal every run is error-prone and hard to version. For complex, multi-constraint goals it breaks down fast.&#xA;&#xA;I added --goal-file to Hone — pass a path to a plain text file, Hone reads the goal from there. Same idea as Karpathy&#39;s program.md in autoresearch. The goal now lives alongside the code, versioned in git.&#xA;&#xA;Live in v1.2.0. pip install --upgrade hone-ai.&#xA;&#xA;---&#xA;&#xA;Episode 2: Haiku, 100M rows — 9.4% faster&#xA;March 25, 2026&#xA;&#xA;47.197s → 42.739s&#xA;&#xA;10x harder dataset. Now I/O pressure actually matters — 4.5 seconds saved is a real signal.&#xA;&#xA;But Haiku still couldn&#39;t find the structural moves. It made safe, local edits — better buffering, minor parsing cleanup — and never stepped back to reconsider the architecture. No parallelism. No mmap. No integer parsing. It hit its ceiling.&#xA;&#xA;---&#xA;&#xA;Episode 3: Sonnet, 100M rows — 79% faster&#xA;March 25, 2026&#xA;&#xA;48.104s → 10.110s&#xA;&#xA;Same benchmark. Same constraints. One change: claude-haiku-4-5 → claude-sonnet-4-6.&#xA;&#xA;38 seconds saved. The agent didn&#39;t tune the baseline — it replaced it.&#xA;&#xA;What Sonnet actually did&#xA;&#xA;1. Text → Binary reads with mmap&#xA;&#xA;The baseline opens the file in text mode and reads line by line. Sonnet switched to binary mode with memory-mapped I/O — the OS maps the file directly into memory, eliminating repeated read syscalls.&#xA;&#xA;2. float() → integer arithmetic&#xA;&#xA;Every float() call in the baseline is expensive. Sonnet eliminated them entirely. Temperatures are stored as integers ×10 — 12.3 becomes 123. The decimal point is skipped by knowing its fixed position in the byte string. Division back to float happens only once, at output time. It also pre-built a lookup table for all valid temperature values (-99.9 to 99.9) to skip even manual parsing on the common case.&#xA;&#xA;3. Multiprocessing across all CPU cores&#xA;&#xA;The baseline is single-threaded. Sonnet split the file into cpucount() × 8 chunks, aligned each boundary to the next newline to avoid splitting rows, and ran each chunk in a separate process. Results merged at the end.&#xA;&#xA;4. strip() + index() → partition()&#xA;&#xA;The baseline does line.strip() then line.index(&#34;;&#34;) — two passes. Sonnet used line.partition(b&#39;;&#39;) — one pass, station and temperature in a single call.&#xA;&#xA;Why Haiku couldn&#39;t find this&#xA;&#xA;Haiku made safe, local edits. It never stepped back to reconsider the architecture. Sonnet saw the whole picture: the bottleneck isn&#39;t any single line, it&#39;s the approach. Single-threaded text parsing doesn&#39;t scale. The winning move was to throw it out and start from a parallel, binary-aware design.&#xA;&#xA;Q: Does model choice matter more than iteration count?&#xA;&#xA;---&#xA;&#xA;Episode 4: Sonnet&#39;s 100M solution, dropped on 1B rows — 73.3% faster&#xA;April 7, 2026&#xA;&#xA;487.525s → 130.080s&#xA;&#xA;Before spending more API budget, I wanted to answer a simpler question first: does the architecture Sonnet found at 100M rows even generalize? No new Hone run. No new cost. Just the existing solution, run unchanged against the full 1B dataset.&#xA;&#xA;357 seconds saved. The answer is yes — it generalizes. mmap, multiprocessing, and integer arithmetic aren&#39;t tricks tuned to a particular file size. They&#39;re structural. The solution held up.&#xA;&#xA;But 130 seconds also exposed the ceiling. Optimizing against a 10x smaller proxy leaves performance on the table. The solution was good — just not good enough. Time to run Hone against the real target.&#xA;&#xA;Source code as Gist here&#xA;&#xA;---&#xA;&#xA;Episode 5: Sonnet, 1B rows directly — 81.4% faster&#xA;April 7, 2026&#xA;&#xA;487.525s → 90.929s&#xA;&#xA;Same model, same constraints. This time Hone optimized against the full 1B row dataset from the start.&#xA;&#xA;396 seconds saved. Under 91 seconds for a billion rows of Python.&#xA;&#xA;The gains from Episode 4 weren&#39;t wasted — they were the floor. Hone started from a strong architecture and pushed further. 81.4% beats the 79% from Episode 3. More data, better result. The solution isn&#39;t fragile.&#xA;&#xA;Source code as Gist here&#xA;&#xA;The lesson: optimize against the real target. A proxy dataset is useful for iteration speed, but the final run needs to face the actual problem.&#xA;&#xA;---&#xA;&#xA;What&#39;s Next&#xA;&#xA;Under 91 seconds on 1B rows. The question now is how much headroom is left — and whether Hone can find it without numpy or third-party packages.&#xA;&#xA;---&#xA;&#xA;Updates appear here as experiments run. Subscribe below or follow via RSS._&#xA;&#xA;#engineering #hone #ai&#xA;&#xA;!--emailsub--]]&gt;</description>
      <content:encoded><![CDATA[<p>1,000,000,000 rows of data. No hand-tuning. Just an agent, a benchmark, and a budget.</p>

<p>The <a href="https://github.com/gunnarmorling/1brc">1 Billion Row Challenge</a> is simple on paper: read a file with 1B rows of weather station measurements, compute min/mean/max per station, as fast as possible. In Python, a naive solution takes minutes. The best human-optimized ones use memory-mapped files, multiprocessing, and numpy.</p>

<p>I&#39;m not optimizing it by hand. I&#39;m giving it to <a href="https://github.com/laxmena/hone">Hone</a> — and letting it figure it out.</p>

<p>Hone is now on PyPI. Install it with <code>pip install hone-ai</code>.</p>

<p>This is a living document. I&#39;ll update it as each run completes. Follow the code at <a href="https://github.com/laxmena/hone-1brc">laxmena/hone-1brc</a>.</p>



<hr/>

<h2 id="the-setup" id="the-setup">The Setup</h2>

<p><strong>The challenge:</strong> Parse a 1B-row file. Each row: <code>Hamburg;12.0</code>. Compute min/mean/max per station. Print results sorted alphabetically.</p>

<p><strong>The metric:</strong> Wall-clock runtime in seconds. Lower is better.</p>

<p><strong>The constraints:</strong> Python standard library only. No numpy, no pandas, no third-party packages. Correctness must be preserved — output format and values must not change.</p>

<p><strong>The baseline:</strong> Simple. Correct. Slow. One thread, one line at a time, <code>float()</code> on every value.</p>

<hr/>

<h2 id="results-at-a-glance" id="results-at-a-glance">Results at a Glance</h2>

<table>
<thead>
<tr>
<th>Run</th>
<th>Model</th>
<th>Dataset</th>
<th>Baseline</th>
<th>Optimized</th>
<th>Improvement</th>
</tr>
</thead>

<tbody>
<tr>
<td>1</td>
<td>Haiku</td>
<td>1M rows</td>
<td>0.546s</td>
<td>0.471s</td>
<td>13.7%</td>
</tr>

<tr>
<td>2</td>
<td>Haiku</td>
<td>100M rows</td>
<td>47.197s</td>
<td>42.739s</td>
<td>9.4%</td>
</tr>

<tr>
<td>3</td>
<td>Sonnet</td>
<td>100M rows</td>
<td>48.104s</td>
<td>10.110s</td>
<td><strong>79%</strong></td>
</tr>

<tr>
<td>4</td>
<td>Sonnet (100M solution, no re-run)</td>
<td>1B rows</td>
<td>487.525s</td>
<td>130.080s</td>
<td>73.3%</td>
</tr>

<tr>
<td>5</td>
<td>Sonnet</td>
<td>1B rows</td>
<td>487.525s</td>
<td>90.929s</td>
<td><strong>81.4%</strong></td>
</tr>
</tbody>
</table>

<hr/>

<h2 id="episode-1-haiku-1m-rows-13-7-faster" id="episode-1-haiku-1m-rows-13-7-faster">Episode 1: Haiku, 1M rows — 13.7% faster</h2>

<p><em>March 25, 2026</em></p>

<p><code>0.546s → 0.471s</code></p>

<p>First run: <code>claude-haiku-4-5</code>, 1M rows, $5 budget, 50 max iterations.</p>

<p>The 13.7% gain looks decent on paper. It isn&#39;t. The absolute numbers are tiny — we&#39;re talking 75 milliseconds. At this scale, Python startup time and OS disk caching dominate. The agent is optimizing noise, not the algorithm. Haiku made incremental tweaks but never found a structural breakthrough.</p>

<p>Wrong dataset size. Move on.</p>

<hr/>

<h2 id="hone-v1-2-0-goal-file" id="hone-v1-2-0-goal-file">Hone v1.2.0: <code>--goal-file</code></h2>

<p><em>March 25, 2026</em></p>

<p>Episode 1 exposed a friction point. Pasting a long goal string into the terminal every run is error-prone and hard to version. For complex, multi-constraint goals it breaks down fast.</p>

<p>I added <code>--goal-file</code> to Hone — pass a path to a plain text file, Hone reads the goal from there. Same idea as Karpathy&#39;s <code>program.md</code> in autoresearch. The goal now lives alongside the code, versioned in git.</p>

<p>Live in <a href="https://github.com/laxmena/hone/commit/477a83d5050628355bf45ceded3807fea75b8ce6">v1.2.0</a>. <code>pip install --upgrade hone-ai</code>.</p>

<hr/>

<h2 id="episode-2-haiku-100m-rows-9-4-faster" id="episode-2-haiku-100m-rows-9-4-faster">Episode 2: Haiku, 100M rows — 9.4% faster</h2>

<p><em>March 25, 2026</em></p>

<p><code>47.197s → 42.739s</code></p>

<p>10x harder dataset. Now I/O pressure actually matters — 4.5 seconds saved is a real signal.</p>

<p>But Haiku still couldn&#39;t find the structural moves. It made safe, local edits — better buffering, minor parsing cleanup — and never stepped back to reconsider the architecture. No parallelism. No mmap. No integer parsing. It hit its ceiling.</p>

<hr/>

<h2 id="episode-3-sonnet-100m-rows-79-faster" id="episode-3-sonnet-100m-rows-79-faster">Episode 3: Sonnet, 100M rows — <strong>79% faster</strong></h2>

<p><em>March 25, 2026</em></p>

<p><code>48.104s → 10.110s</code></p>

<p>Same benchmark. Same constraints. One change: <code>claude-haiku-4-5</code> → <code>claude-sonnet-4-6</code>.</p>

<p>38 seconds saved. The agent didn&#39;t tune the baseline — it replaced it.</p>

<h3 id="what-sonnet-actually-did" id="what-sonnet-actually-did">What Sonnet actually did</h3>

<p><strong>1. Text → Binary reads with <code>mmap</code></strong></p>

<p>The baseline opens the file in text mode and reads line by line. Sonnet switched to binary mode with memory-mapped I/O — the OS maps the file directly into memory, eliminating repeated read syscalls.</p>

<p><strong>2. <code>float()</code> → integer arithmetic</strong></p>

<p>Every <code>float()</code> call in the baseline is expensive. Sonnet eliminated them entirely. Temperatures are stored as integers ×10 — <code>12.3</code> becomes <code>123</code>. The decimal point is skipped by knowing its fixed position in the byte string. Division back to float happens only once, at output time. It also pre-built a lookup table for all valid temperature values (<code>-99.9</code> to <code>99.9</code>) to skip even manual parsing on the common case.</p>

<p><strong>3. Multiprocessing across all CPU cores</strong></p>

<p>The baseline is single-threaded. Sonnet split the file into <code>cpu_count() × 8</code> chunks, aligned each boundary to the next newline to avoid splitting rows, and ran each chunk in a separate process. Results merged at the end.</p>

<p><strong>4. <code>strip()</code> + <code>index()</code> → <code>partition()</code></strong></p>

<p>The baseline does <code>line.strip()</code> then <code>line.index(&#34;;&#34;)</code> — two passes. Sonnet used <code>line.partition(b&#39;;&#39;)</code> — one pass, station and temperature in a single call.</p>

<h3 id="why-haiku-couldn-t-find-this" id="why-haiku-couldn-t-find-this">Why Haiku couldn&#39;t find this</h3>

<p>Haiku made safe, local edits. It never stepped back to reconsider the architecture. Sonnet saw the whole picture: the bottleneck isn&#39;t any single line, it&#39;s the approach. Single-threaded text parsing doesn&#39;t scale. The winning move was to throw it out and start from a parallel, binary-aware design.</p>

<p><strong>Q: Does model choice matter more than iteration count?</strong></p>

<hr/>

<h2 id="episode-4-sonnet-s-100m-solution-dropped-on-1b-rows-73-3-faster" id="episode-4-sonnet-s-100m-solution-dropped-on-1b-rows-73-3-faster">Episode 4: Sonnet&#39;s 100M solution, dropped on 1B rows — 73.3% faster</h2>

<p><em>April 7, 2026</em></p>

<p><code>487.525s → 130.080s</code></p>

<p>Before spending more API budget, I wanted to answer a simpler question first: does the architecture Sonnet found at 100M rows even generalize? No new Hone run. No new cost. Just the existing solution, run unchanged against the full 1B dataset.</p>

<p>357 seconds saved. The answer is yes — it generalizes. mmap, multiprocessing, and integer arithmetic aren&#39;t tricks tuned to a particular file size. They&#39;re structural. The solution held up.</p>

<p>But 130 seconds also exposed the ceiling. Optimizing against a 10x smaller proxy leaves performance on the table. The solution was good — just not good enough. Time to run Hone against the real target.</p>

<p><a href="https://gist.github.com/laxmena/a55238ce48ab0a3157087b8f345a0775">Source code as Gist here</a></p>

<hr/>

<h2 id="episode-5-sonnet-1b-rows-directly-81-4-faster" id="episode-5-sonnet-1b-rows-directly-81-4-faster">Episode 5: Sonnet, 1B rows directly — <strong>81.4% faster</strong></h2>

<p><em>April 7, 2026</em></p>

<p><code>487.525s → 90.929s</code></p>

<p>Same model, same constraints. This time Hone optimized against the full 1B row dataset from the start.</p>

<p>396 seconds saved. Under 91 seconds for a billion rows of Python.</p>

<p>The gains from Episode 4 weren&#39;t wasted — they were the floor. Hone started from a strong architecture and pushed further. 81.4% beats the 79% from Episode 3. More data, better result. The solution isn&#39;t fragile.</p>

<p><a href="https://gist.github.com/laxmena/0cb6ba6c5d8a5e235d245295afa0b9fd">Source code as Gist here</a></p>

<p>The lesson: optimize against the real target. A proxy dataset is useful for iteration speed, but the final run needs to face the actual problem.</p>

<hr/>

<h2 id="what-s-next" id="what-s-next">What&#39;s Next</h2>

<p>Under 91 seconds on 1B rows. The question now is how much headroom is left — and whether Hone can find it without numpy or third-party packages.</p>

<hr/>

<p><em>Updates appear here as experiments run. Subscribe below or follow via <a href="https://write.as/laxmena/feed/">RSS</a>.</em></p>

<p><a href="https://laxmena.com/tag:engineering" class="hashtag"><span>#</span><span class="p-category">engineering</span></a> <a href="https://laxmena.com/tag:hone" class="hashtag"><span>#</span><span class="p-category">hone</span></a> <a href="https://laxmena.com/tag:ai" class="hashtag"><span>#</span><span class="p-category">ai</span></a></p>


]]></content:encoded>
      <guid>https://laxmena.com/hone-vs-the-1-billion-row-challenge</guid>
      <pubDate>Wed, 25 Mar 2026 04:06:42 +0000</pubDate>
    </item>
    <item>
      <title>I Built a Tool That Optimizes Code While You Sleep</title>
      <link>https://laxmena.com/i-built-a-tool-that-optimizes-code-while-you-sleep?pk_campaign=rss-feed</link>
      <description>&lt;![CDATA[A few weeks ago, I watched a Karpathy talk where he described running an agentic loop to auto-tune LLM fine-tuning pipelines. The core idea was simple: give the agent a goal, a way to measure progress, and let it iterate autonomously until it gets there.&#xA;&#xA;I couldn&#39;t stop thinking about it.&#xA;&#xA;Not because of the fine-tuning use case — but because the pattern felt universally useful. Most software has something you want to improve and a way to measure it. Why are we still doing the iteration loop by hand?&#xA;&#xA;So I built Hone.&#xA;&#xA;!--more--&#xA;&#xA;What Hone Does&#xA;&#xA;Hone is a CLI tool. You give it three things:&#xA;&#xA;A goal, in plain English&#xA;&#xA;A file or directory to optimize&#xA;&#xA;A benchmark command that outputs a number&#xA;&#xA;Then you leave.&#xA;&#xA;Hone runs a loop: it asks an LLM what to try next, applies the changes, runs your benchmark, and decides whether to keep the result or revert it. It logs every iteration — the score, the diff, and the agent&#39;s reasoning — and stops when it hits your target or you tell it to.&#xA;&#xA;hone &#34;Optimize processlogs.py to run under 0.02 seconds&#34; &#xA;     --bench &#34;python benchlogs.py&#34; &#xA;     --files &#34;processlogs.py&#34; &#xA;     --optimize lower &#xA;     --target 0.02 &#xA;     --budget 2.0&#xA;&#xA;That&#39;s the entire interface.&#xA;&#xA;---&#xA;&#xA;Experiment 1: The Log Parser&#xA;&#xA;The first real test was a deliberately naive Python log parser. The task: analyze 150,000 lines of server logs and return the top 3 most-visited endpoints with unique IP counts.&#xA;&#xA;The baseline code was the kind you&#39;d write in an interview warm-up: readlines() into memory, a list for uniqueness checking (O(n) per insert), a regex match on every line. It took 1.54 seconds.&#xA;&#xA;I set a target of 0.02 seconds — roughly 75x faster — and launched Hone with a $2 budget.&#xA;&#xA;Here&#39;s what happened over 20 iterations:&#xA;&#xA;| Iter | Score | What the agent did |&#xA;|------|-------|--------------------|&#xA;| 1–4 | 0.8s → 0.4s | Replaced list with set for O(1) uniqueness, pre-bound set.add to skip attribute lookup overhead |&#xA;| 5–9 | 0.4s → 0.15s | Switched from readlines() to streaming with f, dropped unnecessary string allocations |&#xA;| 10–14 | 0.15s → 0.09s | Compiled regex outside the loop, switched from re.match to re.search with anchored pattern |&#xA;| 15–17 | 0.09s → 0.07s | Plateaued. Agent recognized it had hit the ceiling of single-threaded Python looping. |&#xA;| 18–20 | 0.07s → 0.037s | Changed the rules entirely. Abandoned line-by-line parsing. Read the file as a raw binary blob. Deployed re.findall() over the entire content in one pass. |&#xA;&#xA;The final move was the interesting one. The agent didn&#39;t just tune the existing approach — it recognized the approach itself was the bottleneck and replaced it. That pivot happened at iteration 18, after the agent wrote in its reasoning:&#xA;&#xA;  &#34;The real bottleneck is the Python loop and split() calls. Try using a compiled regex to extract the endpoint in one operation across the entire file.&#34;&#xA;&#xA;Final result: 1.54s → 0.037s. A 41x speedup. Autonomously.&#xA;&#xA;It didn&#39;t hit the 0.02 target — that&#39;s likely beyond what single-threaded Python can do on this task without going to C extensions. But a 41x improvement for $1.84 in API costs is a real result.&#xA;&#xA;---&#xA;&#xA;Experiment 2: Nearest Driver Dispatch&#xA;&#xA;The second experiment was closer to production code. The problem: given a set of riders and a pool of drivers, find the nearest driver for each rider using haversine distance.&#xA;&#xA;The baseline was an O(R × D) brute-force loop — calculate the full haversine distance between every rider and every driver. With 500 riders and 1,000 drivers, that&#39;s 500,000 distance calculations per call. Baseline: 2.18 seconds.&#xA;&#xA;Run 1 — I launched Hone with no hints. Just: &#34;optimize this to run faster.&#34;&#xA;&#xA;The agent went straight for spatial indexing. It built a grid over the geographic area, bucketed drivers into cells, and used Manhattan distance pre-filtering to eliminate distant candidates before running haversine. It also replaced the standard math module haversine with a vectorized approximation valid for short distances.&#xA;&#xA;Result: 0.1496 seconds. A 14.6x speedup.&#xA;&#xA;Run 2 — I ran Hone again on the output from Run 1.&#xA;&#xA;This is where it got interesting. The agent looked at the already-optimized code and found something the previous run missed: the grid search still checked every driver in candidate cells, even after it had already found a close one.&#xA;&#xA;The fix: stop searching the moment you find a driver within an acceptable radius. Expand the search radius incrementally — start small, grow outward — instead of checking all candidates at once.&#xA;&#xA;  &#34;The algorithm beats the data structure. Grid resolution barely matters. Early termination dominates.&#34;&#xA;&#xA;Result: 0.069 seconds. Another 2.1x on top of an already fast baseline.&#xA;&#xA;Two runs, $3 total, brute-force O(R×D) → smart early-termination spatial search. The agent arrived at an approach that a senior engineer would recognize as correct — not by knowing the algorithm upfront, but by observing what the benchmark rewarded.&#xA;&#xA;---&#xA;&#xA;What I Learned&#xA;&#xA;The benchmark is everything. Hone is only as good as your measurement. If your benchmark is slow to run, the loop is slow. If it doesn&#39;t capture what you actually care about, the agent will optimize the wrong thing. The one thing you must get right before you start is: &#34;does this number actually reflect what I want?&#34;&#xA;&#xA;The agent is a good low-level optimizer. It reliably finds the obvious wins: wrong data structures, redundant computations, missed language primitives. These are also the wins that take a human the most time — not because they&#39;re hard to understand, but because you have to actually sit down and do them.&#xA;&#xA;It surprises you at the edges. The log parser pivot from line-by-line to whole-file regex wasn&#39;t something I would have thought to suggest in the initial prompt. It emerged from the agent hitting a wall and reasoning about why_ it had hit a wall. That&#39;s the behavior that makes agentic loops interesting.&#xA;&#xA;The conversation thread is the memory. The most important architectural decision in Hone was keeping the LLM conversation alive across iterations. The agent doesn&#39;t just see the current score — it sees everything it tried, what worked, and what was reverted. That&#39;s what allows the pivot at iteration 18. Without it, the agent would start fresh each time and repeat the same early optimizations.&#xA;&#xA;Cost is low. Time savings are high. Both experiments ran under $4. The engineering time to achieve the same results manually — writing hypotheses, applying changes, running benchmarks, reverting dead ends — would have been hours. The ROI on agentic loops is already real, and we&#39;re at the beginning.&#xA;&#xA;---&#xA;&#xA;What&#39;s Next&#xA;&#xA;Hone v0 is rough. There&#39;s no sandbox for shell commands, no git-based snapshots, no dry-run mode. These are on the list.&#xA;&#xA;More interesting to me is expanding the use cases. The same loop that optimizes a log parser can optimize:&#xA;&#xA;LLM prompts against an eval suite (highest impact use case)&#xA;RAG pipelines against a retrieval benchmark&#xA;API costs against a quality-constrained spend target&#xA;&#xA;The pattern is the same. The benchmark changes. Hone doesn&#39;t care.&#xA;&#xA;If you want to try it:&#xA;&#xA;git clone https://github.com/laxmena/hone&#xA;cd hone &amp;&amp; pip install -e .&#xA;&#xA;And if you have a benchmark that Hone should try — I want to hear about it.&#xA;&#xA;#engineering #ai&#xA;&#xA;---&#xA;&#xA;!--more--&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p>A few weeks ago, I watched a <a href="https://x.com/karpathy">Karpathy talk</a> where he described running an agentic loop to auto-tune LLM fine-tuning pipelines. The core idea was simple: give the agent a goal, a way to measure progress, and let it iterate autonomously until it gets there.</p>

<p>I couldn&#39;t stop thinking about it.</p>

<p>Not because of the fine-tuning use case — but because the <em>pattern</em> felt universally useful. Most software has something you want to improve and a way to measure it. Why are we still doing the iteration loop by hand?</p>

<p>So I built <a href="https://github.com/laxmena/hone">Hone</a>.</p>



<h2 id="what-hone-does" id="what-hone-does">What Hone Does</h2>

<p>Hone is a CLI tool. You give it three things:</p>
<ol><li><p>A goal, in plain English</p></li>

<li><p>A file or directory to optimize</p></li>

<li><p>A benchmark command that outputs a number</p></li></ol>

<p>Then you leave.</p>

<p>Hone runs a loop: it asks an LLM what to try next, applies the changes, runs your benchmark, and decides whether to keep the result or revert it. It logs every iteration — the score, the diff, and the agent&#39;s reasoning — and stops when it hits your target or you tell it to.</p>

<pre><code class="language-bash">hone &#34;Optimize process_logs.py to run under 0.02 seconds&#34; 
     --bench &#34;python bench_logs.py&#34; 
     --files &#34;process_logs.py&#34; 
     --optimize lower 
     --target 0.02 
     --budget 2.0
</code></pre>

<p>That&#39;s the entire interface.</p>

<hr/>

<h2 id="experiment-1-the-log-parser" id="experiment-1-the-log-parser">Experiment 1: The Log Parser</h2>

<p>The first real test was a deliberately naive Python log parser. The task: analyze 150,000 lines of server logs and return the top 3 most-visited endpoints with unique IP counts.</p>

<p>The baseline code was the kind you&#39;d write in an interview warm-up: <code>readlines()</code> into memory, a list for uniqueness checking (O(n) per insert), a regex match on every line. It took <strong>1.54 seconds</strong>.</p>

<p>I set a target of 0.02 seconds — roughly 75x faster — and launched Hone with a $2 budget.</p>

<p>Here&#39;s what happened over 20 iterations:</p>

<table>
<thead>
<tr>
<th>Iter</th>
<th>Score</th>
<th>What the agent did</th>
</tr>
</thead>

<tbody>
<tr>
<td>1–4</td>
<td>0.8s → 0.4s</td>
<td>Replaced list with <code>set</code> for O(1) uniqueness, pre-bound <code>set.add</code> to skip attribute lookup overhead</td>
</tr>

<tr>
<td>5–9</td>
<td>0.4s → 0.15s</td>
<td>Switched from <code>readlines()</code> to streaming with <code>f</code>, dropped unnecessary string allocations</td>
</tr>

<tr>
<td>10–14</td>
<td>0.15s → 0.09s</td>
<td>Compiled regex outside the loop, switched from <code>re.match</code> to <code>re.search</code> with anchored pattern</td>
</tr>

<tr>
<td>15–17</td>
<td>0.09s → 0.07s</td>
<td>Plateaued. Agent recognized it had hit the ceiling of single-threaded Python looping.</td>
</tr>

<tr>
<td>18–20</td>
<td>0.07s → <strong>0.037s</strong></td>
<td><strong>Changed the rules entirely.</strong> Abandoned line-by-line parsing. Read the file as a raw binary blob. Deployed <code>re.findall()</code> over the entire content in one pass.</td>
</tr>
</tbody>
</table>

<p>The final move was the interesting one. The agent didn&#39;t just tune the existing approach — it recognized the approach itself was the bottleneck and replaced it. That pivot happened at iteration 18, after the agent wrote in its reasoning:</p>

<blockquote><p><em>“The real bottleneck is the Python loop and split() calls. Try using a compiled regex to extract the endpoint in one operation across the entire file.”</em></p></blockquote>

<p><strong>Final result: 1.54s → 0.037s. A 41x speedup. Autonomously.</strong></p>

<p>It didn&#39;t hit the 0.02 target — that&#39;s likely beyond what single-threaded Python can do on this task without going to C extensions. But a 41x improvement for $1.84 in API costs is a real result.</p>

<hr/>

<h2 id="experiment-2-nearest-driver-dispatch" id="experiment-2-nearest-driver-dispatch">Experiment 2: Nearest Driver Dispatch</h2>

<p>The second experiment was closer to production code. The problem: given a set of riders and a pool of drivers, find the nearest driver for each rider using haversine distance.</p>

<p>The baseline was an O(R × D) brute-force loop — calculate the full haversine distance between every rider and every driver. With 500 riders and 1,000 drivers, that&#39;s 500,000 distance calculations per call. Baseline: <strong>2.18 seconds</strong>.</p>

<p><strong>Run 1</strong> — I launched Hone with no hints. Just: <em>“optimize this to run faster.”</em></p>

<p>The agent went straight for spatial indexing. It built a grid over the geographic area, bucketed drivers into cells, and used Manhattan distance pre-filtering to eliminate distant candidates before running haversine. It also replaced the standard <code>math</code> module haversine with a vectorized approximation valid for short distances.</p>

<p>Result: <strong>0.1496 seconds. A 14.6x speedup.</strong></p>

<p><strong>Run 2</strong> — I ran Hone again on the output from Run 1.</p>

<p>This is where it got interesting. The agent looked at the already-optimized code and found something the previous run missed: the grid search still checked every driver in candidate cells, even after it had already found a close one.</p>

<p>The fix: stop searching the moment you find a driver within an acceptable radius. Expand the search radius incrementally — start small, grow outward — instead of checking all candidates at once.</p>

<blockquote><p><em>“The algorithm beats the data structure. Grid resolution barely matters. Early termination dominates.”</em></p></blockquote>

<p>Result: <strong>0.069 seconds. Another 2.1x on top of an already fast baseline.</strong></p>

<p>Two runs, $3 total, brute-force O(R×D) → smart early-termination spatial search. The agent arrived at an approach that a senior engineer would recognize as correct — not by knowing the algorithm upfront, but by observing what the benchmark rewarded.</p>

<hr/>

<h2 id="what-i-learned" id="what-i-learned">What I Learned</h2>

<p><strong>The benchmark is everything.</strong> Hone is only as good as your measurement. If your benchmark is slow to run, the loop is slow. If it doesn&#39;t capture what you actually care about, the agent will optimize the wrong thing. The one thing you must get right before you start is: “does this number actually reflect what I want?”</p>

<p><strong>The agent is a good low-level optimizer.</strong> It reliably finds the obvious wins: wrong data structures, redundant computations, missed language primitives. These are also the wins that take a human the most time — not because they&#39;re hard to understand, but because you have to actually sit down and do them.</p>

<p><strong>It surprises you at the edges.</strong> The log parser pivot from line-by-line to whole-file regex wasn&#39;t something I would have thought to suggest in the initial prompt. It emerged from the agent hitting a wall and reasoning about <em>why</em> it had hit a wall. That&#39;s the behavior that makes agentic loops interesting.</p>

<p><strong>The conversation thread is the memory.</strong> The most important architectural decision in Hone was keeping the LLM conversation alive across iterations. The agent doesn&#39;t just see the current score — it sees everything it tried, what worked, and what was reverted. That&#39;s what allows the pivot at iteration 18. Without it, the agent would start fresh each time and repeat the same early optimizations.</p>

<p><strong>Cost is low. Time savings are high.</strong> Both experiments ran under $4. The engineering time to achieve the same results manually — writing hypotheses, applying changes, running benchmarks, reverting dead ends — would have been hours. The ROI on agentic loops is already real, and we&#39;re at the beginning.</p>

<hr/>

<h2 id="what-s-next" id="what-s-next">What&#39;s Next</h2>

<p>Hone v0 is rough. There&#39;s no sandbox for shell commands, no git-based snapshots, no dry-run mode. These are on the list.</p>

<p>More interesting to me is expanding the use cases. The same loop that optimizes a log parser can optimize:</p>
<ul><li><strong>LLM prompts</strong> against an eval suite (highest impact use case)</li>
<li><strong>RAG pipelines</strong> against a retrieval benchmark</li>
<li><strong>API costs</strong> against a quality-constrained spend target</li></ul>

<p>The pattern is the same. The benchmark changes. Hone doesn&#39;t care.</p>

<p>If you want to try it:</p>

<pre><code class="language-bash">git clone https://github.com/laxmena/hone
cd hone &amp;&amp; pip install -e .
</code></pre>

<p>And if you have a benchmark that Hone should try — I want to hear about it.</p>

<p><a href="https://laxmena.com/tag:engineering" class="hashtag"><span>#</span><span class="p-category">engineering</span></a> <a href="https://laxmena.com/tag:ai" class="hashtag"><span>#</span><span class="p-category">ai</span></a></p>

<hr/>


]]></content:encoded>
      <guid>https://laxmena.com/i-built-a-tool-that-optimizes-code-while-you-sleep</guid>
      <pubDate>Tue, 24 Mar 2026 03:06:12 +0000</pubDate>
    </item>
    <item>
      <title>RiskChain: The Messy Middle: Building a Risk Graph from Scratch</title>
      <link>https://laxmena.com/riskchain-the-messy-middle-building-a-risk-graph-from-scratch?pk_campaign=rss-feed</link>
      <description>&lt;![CDATA[An ongoing weekend project documenting the journey of uncovering hidden connections in corporate financial filings—the stumbles, the learnings, the &#39;aha!&#39; moments, and everything in between. Started January 2025.&#xA;&#xA;---&#xA;&#xA;What is RiskChain?&#xA;&#xA;The core idea is simple but ambitious: find hidden connections and risk trails that aren&#39;t immediately obvious when you&#39;re just reading through a 10-K filing.&#xA;&#xA;!--more--&#xA;&#xA;Instead of treating each financial document as an isolated artifact, I&#39;m building a system to:&#xA;&#xA;Extract risk factors from 10-K filings (2004-2025) across 75 companies&#xA;Embed and connect these risks to find non-obvious relationships&#xA;Build a graph that reveals risk clusters, patterns, and &#34;trails&#34; that could signal systemic weaknesses or early warning signs&#xA;&#xA;Why 10-K filings? Because companies are required to disclose risks in specific sections (Item 1 and Item 1a), and there&#39;s a decade+ of structured data just sitting there.&#xA;&#xA;---&#xA;&#xA;The Vision&#xA;&#xA;Here&#39;s the full pipeline I&#39;m building toward:&#xA;&#xA;[Raw Financial Data]&#xA;  ├── SEC Filings (10-K/Q) ── News Articles ── Earnings Transcripts ── Other Reports&#xA;          │&#xA;          ▼&#xA;[1. Ingestion &amp; Chunking]&#xA;  → Parse documents (PDF/HTML) → Split into sentences → Group into ~500-word chunks&#xA;          │&#xA;          ▼&#xA;[2. Risk Extraction]&#xA;  → Use Gemini Flash per chunk → Extract 3-5 specific risk factors + severity&#xA;          │&#xA;          ▼&#xA;[3. Storage &amp; Embeddings]&#xA;  → SQLite DB (with sqlite-vec) → Embed risk labels (embedding-gemma-300m) → Deduplicate similar risks&#xA;          │&#xA;          ▼&#xA;[4. Graph Construction]&#xA;  → Nodes = unique risks&#xA;  → Edges = &#xA;      ├─ Semantic similarity (embeddings)&#xA;      └─ Statistical co-occurrence (PMI)&#xA;          │&#xA;          ▼&#xA;[5. Hierarchical Clustering]&#xA;  → Apply Leiden algorithm (Surprise function) → Build risk hierarchy tree&#xA;  → Compute novelty scores for under-explored areas&#xA;          │&#xA;          ▼&#xA;[6. CLI / Interface Layer]&#xA;  → Persistent server for fast queries&#xA;  → Commands: searchrisks, browsetree, crossreportrisks, etc.&#xA;          │&#xA;          ▼&#xA;[7. Agent Workflow (Claude / similar)]&#xA;  ├── Stage 1: Ideation ── Browse tree → Propose novel risk chains (novelty bias)&#xA;  ├── Stage 2: Research ── Dive into chunks → Extract &amp; order excerpts&#xA;  └── Stage 3: Output ── Generate RiskChain (visual trail with edges + narrative)&#xA;          │&#xA;          ▼&#xA;[8. Presentation &amp; Action]&#xA;  → Web dashboard / exported report&#xA;  → Visual graph + highlighted excerpts + suggested hedges / alerts&#xA;  → Human review → Iterate via feedback&#xA;&#xA;It&#39;s ambitious. It&#39;s probably overambitious. But that&#39;s the goal.&#xA;&#xA;---&#xA;&#xA;Current Status&#xA;&#xA;Phase: 2 - Chunking Strategy ✓&#xA;Progress: Data downloaded → Chunking complete → Ready for Risk Extraction&#xA;&#xA;---&#xA;&#xA;Stay Updated&#xA;&#xA;I&#39;m documenting this journey every weekend—the wins, the blockers, the learnings. If you want regular updates on how RiskChain develops, subscribe below to get new posts delivered to your inbox.&#xA;&#xA;!--more--&#xA;&#xA;---&#xA;&#xA;Progress Log&#xA;&#xA;Weekend 1 | Jan 18, 2025 | Phase 1: Download Script ✓&#xA;&#xA;What I built:&#xA;Downloaded 10-K filings for 75 companies from 2004-2025 using the Python edgartools library. Curated a list of significant companies (including ones that went bankrupt in 2008—why not?). Got the script working and only extracting the relevant sections (Item 1, Item 7, Item 8) to keep things lean.&#xA;&#xA;The messy parts (aka real life):&#xA;I initially tried sec-edgar-downloader to connect to SEC and download. Spent way too much time on this approach, got stuck in the data cleaning rabbit hole, and realized I was losing sight of the actual goal. The real issue? Many of the 10-K filings before the SEC standardized their item categorization didn&#39;t play nice with the tool.&#xA;&#xA;  Lesson learned: when you&#39;re iterating, it&#39;s okay to abandon the &#34;perfect&#34; approach for one that ships faster.&#xA;&#xA;Then I switched to edgartools (also known as edgar). This library gave me more flexibility, though the documentation still wasn&#39;t intuitive for my specific use case. But instead of giving up, I dug into the source code. That&#39;s when things clicked. Sometimes the best learning comes from reading other people&#39;s code instead of waiting for docs to explain everything.&#xA;&#xA;The &#39;aha!&#39; moment:&#xA;&#xA;  My wife helped me understand what Item 1, Item 1a, Item 7, and Item 8 actually mean in a 10-K filing. She translated the financial jargon into plain English, and suddenly the document structure made sense. Having someone who can bridge the domain knowledge gap is invaluable. I realized I was building this in a foreign domain—finance is not my native language, and that&#39;s okay.&#xA;&#xA;What blocked me:&#xA;&#xA;Figuring out the right tool for downloading (sec-edgar-downloader vs edgartools vs rolling my own)&#xA;Understanding that parsing 10-K files is genuinely harder than it looks (inconsistent structures across years, weird formatting, embedded tables)&#xA;&#xA;Next up: Phase 2: Chunking strategy. Need to figure out how to split these documents intelligently for downstream LLM tasks.&#xA;&#xA;---&#xA;&#xA;Weekend 2 | Jan 23, 2025 | Phase 2: Chunking Strategy ✓&#xA;&#xA;What I built:&#xA;Implemented chunking using wtpsplitter and stored all chunks as markdown files with YAML frontmatter metadata (ticker, filing date, company name, chunk ID, item section). Now sitting on several thousand chunks, each \~1000 characters max, ready for extraction.&#xA;&#xA;The messy parts (aka real life):&#xA;I tried two chunking strategies: RecursiveChunker and wtpsplitter. RecursiveChunker felt like brute force—just splitting on token counts. But wtpsplitter was smarter; it respects sentence boundaries and creates more semantically coherent chunks.&#xA;&#xA;Storing these as markdown files locally feels like a step backward (shouldn&#39;t I be using a database?), but honestly, it&#39;s perfect for iteration. I can inspect the chunks, debug the metadata, and understand what&#39;s happening before I add the complexity of a full DB setup.&#xA;&#xA;The &#39;aha!&#39; moment:&#xA;&#xA;  Chunk quality matters way more than I initially thought. The way you split text directly impacts whether an LLM can extract meaningful risk factors later. Sentence-aware chunking beats token-counting brutality. This made me reconsider the whole &#34;let me jump straight to a database&#34; instinct. Sometimes you need to slow down and get the fundamentals right first.&#xA;&#xA;What blocked me:&#xA;&#xA;Deciding between chunking strategies (trial and error on a few approaches)&#xA;Understanding the tradeoff between local file storage and &#34;proper&#34; database setup (spoiler: local storage is fine for now)&#xA;Realizing I was overthinking this phase when the real value comes next&#xA;&#xA;Next up: Phase 3: Risk Extraction. I&#39;ll iterate through each chunk and use Claude/Gemini to extract 3-5 risk factors per chunk. This is where the actual signal starts emerging.&#xA;&#xA;---&#xA;&#xA;Why This Matters (and Why I&#39;m Excited)&#xA;&#xA;Most financial analysis tools treat risks as isolated items. &#34;Company X faces supply chain risk.&#34; &#34;Company Y has regulatory exposure.&#34; But what if you could see that 40 companies in the industrial sector all mention the same emerging regulatory risk, and 3 of them went bankrupt 2 years later?&#xA;&#xA;That&#39;s the thesis here. Hidden connections. Patterns that emerge when you look at scale.&#xA;&#xA;Also, I&#39;m learning a ton: SEC filing structures, chunking strategies, embedding models, graph theory, the Leiden algorithm... This is weekend learning on steroids.&#xA;&#xA;---&#xA;&#xA;Updates added weekly (weekends permitting). Check back for new learnings, blockers, and wins.&#xA;&#xA;---&#xA;&#xA;Resources &amp; References&#xA;&#xA;Inspiration: Syntopic Reading with Claude — The original spark for connecting documents at scale&#xA;Graph Clustering: Leiden Algorithm Documentation — For hierarchical risk clustering&#xA;SEC Data Tool: edgartools (edgar) — Python library for downloading SEC filings&#xA;Alternative Tool: sec-edgar-downloader — The tool I explored first (works well for recent filings; struggled with older 10-Ks before SEC standardization)&#xA;&#xA;#engineering #ai]]&gt;</description>
      <content:encoded><![CDATA[<p><em>An ongoing weekend project documenting the journey of uncovering hidden connections in corporate financial filings—the stumbles, the learnings, the &#39;aha!&#39; moments, and everything in between. Started January 2025.</em></p>

<hr/>

<h3 id="what-is-riskchain" id="what-is-riskchain">What is RiskChain?</h3>

<p>The core idea is simple but ambitious: <strong>find hidden connections and risk trails that aren&#39;t immediately obvious</strong> when you&#39;re just reading through a 10-K filing.</p>



<p>Instead of treating each financial document as an isolated artifact, I&#39;m building a system to:</p>
<ul><li>Extract risk factors from 10-K filings (2004-2025) across 75 companies</li>
<li>Embed and connect these risks to find non-obvious relationships</li>
<li>Build a graph that reveals risk clusters, patterns, and “trails” that could signal systemic weaknesses or early warning signs</li></ul>

<p>Why 10-K filings? Because companies are <em>required</em> to disclose risks in specific sections (Item 1 and Item 1a), and there&#39;s a decade+ of structured data just sitting there.</p>

<hr/>

<h2 id="the-vision" id="the-vision">The Vision</h2>

<p>Here&#39;s the full pipeline I&#39;m building toward:</p>

<pre><code>[Raw Financial Data]
  ├── SEC Filings (10-K/Q) ── News Articles ── Earnings Transcripts ── Other Reports
          │
          ▼
[1. Ingestion &amp; Chunking]
  → Parse documents (PDF/HTML) → Split into sentences → Group into ~500-word chunks
          │
          ▼
[2. Risk Extraction]
  → Use Gemini Flash per chunk → Extract 3-5 specific risk factors + severity
          │
          ▼
[3. Storage &amp; Embeddings]
  → SQLite DB (with sqlite-vec) → Embed risk labels (embedding-gemma-300m) → Deduplicate similar risks
          │
          ▼
[4. Graph Construction]
  → Nodes = unique risks
  → Edges = 
      ├─ Semantic similarity (embeddings)
      └─ Statistical co-occurrence (PMI)
          │
          ▼
[5. Hierarchical Clustering]
  → Apply Leiden algorithm (Surprise function) → Build risk hierarchy tree
  → Compute novelty scores for under-explored areas
          │
          ▼
[6. CLI / Interface Layer]
  → Persistent server for fast queries
  → Commands: search_risks, browse_tree, cross_report_risks, etc.
          │
          ▼
[7. Agent Workflow (Claude / similar)]
  ├── Stage 1: Ideation ── Browse tree → Propose novel risk chains (novelty bias)
  ├── Stage 2: Research ── Dive into chunks → Extract &amp; order excerpts
  └── Stage 3: Output ── Generate RiskChain (visual trail with edges + narrative)
          │
          ▼
[8. Presentation &amp; Action]
  → Web dashboard / exported report
  → Visual graph + highlighted excerpts + suggested hedges / alerts
  → Human review → Iterate via feedback
</code></pre>

<p>It&#39;s ambitious. It&#39;s probably overambitious. But that&#39;s the goal.</p>

<hr/>

<h2 id="current-status" id="current-status">Current Status</h2>

<p><strong>Phase: 2 – Chunking Strategy</strong> ✓
<strong>Progress:</strong> Data downloaded → Chunking complete → Ready for Risk Extraction</p>

<hr/>

<h2 id="stay-updated" id="stay-updated">Stay Updated</h2>

<p>I&#39;m documenting this journey every weekend—the wins, the blockers, the learnings. If you want regular updates on how RiskChain develops, subscribe below to get new posts delivered to your inbox.</p>



<hr/>

<h2 id="progress-log" id="progress-log">Progress Log</h2>

<h3 id="weekend-1-jan-18-2025-phase-1-download-script" id="weekend-1-jan-18-2025-phase-1-download-script">Weekend 1 | Jan 18, 2025 | Phase 1: Download Script ✓</h3>

<p><strong>What I built:</strong>
Downloaded 10-K filings for 75 companies from 2004-2025 using the Python <code>edgartools</code> library. Curated a list of significant companies (including ones that went bankrupt in 2008—why not?). Got the script working and only extracting the relevant sections (Item 1, Item 7, Item 8) to keep things lean.</p>

<p><strong>The messy parts (aka real life):</strong>
I initially tried <code>sec-edgar-downloader</code> to connect to SEC and download. Spent way too much time on this approach, got stuck in the data cleaning rabbit hole, and realized I was losing sight of the actual goal. The real issue? Many of the 10-K filings before the SEC standardized their item categorization didn&#39;t play nice with the tool.</p>

<blockquote><p><strong>Lesson learned:</strong> when you&#39;re iterating, it&#39;s okay to abandon the “perfect” approach for one that ships faster.</p></blockquote>

<p>Then I switched to <code>edgartools</code> (also known as <code>edgar</code>). This library gave me more flexibility, though the documentation still wasn&#39;t intuitive for my specific use case. But instead of giving up, I dug into the source code. That&#39;s when things clicked. Sometimes the best learning comes from reading other people&#39;s code instead of waiting for docs to explain everything.</p>

<p><strong>The &#39;aha!&#39; moment:</strong></p>

<blockquote><p>My wife helped me understand what Item 1, Item 1a, Item 7, and Item 8 actually mean in a 10-K filing. She translated the financial jargon into plain English, and suddenly the document structure made sense. <strong>Having someone who can bridge the domain knowledge gap is invaluable.</strong> I realized I was building this in a foreign domain—finance is not my native language, and that&#39;s okay.</p></blockquote>

<p><strong>What blocked me:</strong></p>
<ul><li>Figuring out the right tool for downloading (<code>sec-edgar-downloader</code> vs <code>edgartools</code> vs rolling my own)</li>
<li>Understanding that parsing 10-K files is genuinely harder than it looks (inconsistent structures across years, weird formatting, embedded tables)</li></ul>

<p><strong>Next up:</strong> Phase 2: Chunking strategy. Need to figure out how to split these documents intelligently for downstream LLM tasks.</p>

<hr/>

<h3 id="weekend-2-jan-23-2025-phase-2-chunking-strategy" id="weekend-2-jan-23-2025-phase-2-chunking-strategy">Weekend 2 | Jan 23, 2025 | Phase 2: Chunking Strategy ✓</h3>

<p><strong>What I built:</strong>
Implemented chunking using <code>wtpsplitter</code> and stored all chunks as markdown files with YAML frontmatter metadata (ticker, filing date, company name, chunk ID, item section). Now sitting on several thousand chunks, each ~1000 characters max, ready for extraction.</p>

<p><strong>The messy parts (aka real life):</strong>
I tried two chunking strategies: <code>RecursiveChunker</code> and <code>wtpsplitter</code>. RecursiveChunker felt like brute force—just splitting on token counts. But <code>wtpsplitter</code> was smarter; it respects sentence boundaries and creates more semantically coherent chunks.</p>

<p>Storing these as markdown files locally feels like a step backward (shouldn&#39;t I be using a database?), but honestly, it&#39;s perfect for iteration. I can inspect the chunks, debug the metadata, and understand what&#39;s happening before I add the complexity of a full DB setup.</p>

<p><strong>The &#39;aha!&#39; moment:</strong></p>

<blockquote><p><strong>Chunk quality matters way more than I initially thought.</strong> The way you split text directly impacts whether an LLM can extract meaningful risk factors later. Sentence-aware chunking beats token-counting brutality. This made me reconsider the whole “let me jump straight to a database” instinct. Sometimes you need to slow down and get the fundamentals right first.</p></blockquote>

<p><strong>What blocked me:</strong></p>
<ul><li>Deciding between chunking strategies (trial and error on a few approaches)</li>
<li>Understanding the tradeoff between local file storage and “proper” database setup (spoiler: local storage is fine for now)</li>
<li>Realizing I was overthinking this phase when the real value comes next</li></ul>

<p><strong>Next up:</strong> Phase 3: Risk Extraction. I&#39;ll iterate through each chunk and use Claude/Gemini to extract 3-5 risk factors per chunk. This is where the actual signal starts emerging.</p>

<hr/>

<h2 id="why-this-matters-and-why-i-m-excited" id="why-this-matters-and-why-i-m-excited">Why This Matters (and Why I&#39;m Excited)</h2>

<p>Most financial analysis tools treat risks as isolated items. “Company X faces supply chain risk.” “Company Y has regulatory exposure.” But <strong>what if you could see that 40 companies in the industrial sector all mention the same emerging regulatory risk, and 3 of them went bankrupt 2 years later?</strong></p>

<p>That&#39;s the thesis here. Hidden connections. Patterns that emerge when you look at scale.</p>

<p>Also, I&#39;m learning a <em>ton</em>: SEC filing structures, chunking strategies, embedding models, graph theory, the Leiden algorithm... This is weekend learning on steroids.</p>

<hr/>

<p><em>Updates added weekly (weekends permitting). Check back for new learnings, blockers, and wins.</em></p>

<hr/>

<h2 id="resources-references" id="resources-references">Resources &amp; References</h2>
<ul><li><strong>Inspiration:</strong> <a href="https://pieterma.es/syntopic-reading-claude/">Syntopic Reading with Claude</a> — The original spark for connecting documents at scale</li>
<li><strong>Graph Clustering:</strong> <a href="https://leidenalg.readthedocs.io/en/stable/">Leiden Algorithm Documentation</a> — For hierarchical risk clustering</li>
<li><strong>SEC Data Tool:</strong> <a href="https://github.com/dgunning/edgartools">edgartools (edgar)</a> — Python library for downloading SEC filings</li>
<li><strong>Alternative Tool:</strong> <a href="https://sec-edgar-downloader.readthedocs.io/en/latest/">sec-edgar-downloader</a> — The tool I explored first (works well for recent filings; struggled with older 10-Ks before SEC standardization)</li></ul>

<p><a href="https://laxmena.com/tag:engineering" class="hashtag"><span>#</span><span class="p-category">engineering</span></a> <a href="https://laxmena.com/tag:ai" class="hashtag"><span>#</span><span class="p-category">ai</span></a></p>
]]></content:encoded>
      <guid>https://laxmena.com/riskchain-the-messy-middle-building-a-risk-graph-from-scratch</guid>
      <pubDate>Sat, 24 Jan 2026 04:11:23 +0000</pubDate>
    </item>
  </channel>
</rss>