-- SurrealDB SQL Export
-- Generated on 2026-07-13 19:26:54

-- Drop existing tables (if any)

-- Create tables
DEFINE TABLE IF NOT EXISTS page SCHEMALESS; 

DEFINE TABLE IF NOT EXISTS section SCHEMALESS;

DEFINE TABLE IF NOT EXISTS taxonomy_term SCHEMALESS;

DEFINE TABLE IF NOT EXISTS page_taxonomy_term SCHEMALESS;


DEFINE EVENT OVERWRITE create_post_taxonomy_relationships ON TABLE post WHEN $event = "CREATE" THEN {
  LET $cat = $value.categories;
  FOR $c in $cat {
    LET $existing = (SELECT name from category where name = $c)[0];
    IF $existing == NONE {
      LET $id = (CREATE category CONTENT { name: $c } RETURN id);
      RELATE $value->has_taxonomy->$id CONTENT { type: 'category' }
    }
  };
  LET $tags = $value.tags;
  FOR $c in $tags {
    LET $existing = (SELECT name from category where name = $c)[0];
    IF $existing == NONE {
      LET $id = (CREATE category CONTENT { name: $c } RETURN id);
      RELATE $value->has_taxonomy->$id CONTENT { type: 'tag' }
    }
  };
  LET $series = $value.series;
  FOR $c in $series {
    LET $existing = (SELECT name from category where name = $c)[0];
    IF $existing == NONE {
      LET $id = (CREATE category CONTENT { name: $c } RETURN id);
      RELATE $value->has_taxonomy->$id CONTENT { type: 'series' }
    }
  };
  LET $projects = $value.projects;
  FOR $c in $projects {
    LET $existing = (SELECT name from category where name = $c)[0];
    IF $existing == NONE {
      LET $id = (CREATE category CONTENT { name: $c } RETURN id);
      RELATE $value->has_taxonomy->$id CONTENT { type: 'project' }
    }
  };
  CREATE log CONTENT { timestamp: time::now(), categories: $cat };
};


-- Insert pages




  
  
  
  

  
    
  
  
    
  
  
    
  
  

  CREATE post CONTENT {
      title: "Half of My Hybrid Search Was Silently Off",
      slug: "building-the-harness-retrieval",
      path: "https://parkerjones.dev/posts/building-the-harness-retrieval/",
      content: "<blockquote>\n<p><strong>TL;DR</strong> — I pointed my own retrieval engine at my own blog and it ranked the wrong post\nfirst. Not dirty data, not bad embeddings: one word of Rust. Search terms were joined with a\nspace, which SQLite's full-text search reads as <em>AND</em>, so every natural-language question\nsilently zeroed out the keyword half of \"hybrid\" search and left it running on embeddings\nalone. Swapping the space for <code>OR</code> moved the right answer from #3 to #1.</p>\n<p><strong>Takeaway:</strong> retrieval is an engineering surface with its own silent failures — so don't\nreach for it until your context genuinely won't fit in the model's window. Cache-and-dump\n(paste it all in, let caching pay for it) is the boring default that wins until it\nphysically can't.</p>\n</blockquote>\n<p>I asked my own search engine for the one post I've written about saving tokens with a Rust\nCLI. I've written exactly one. It came back third.</p>\n<pre><code data-lang=\"text\">❯ ctx search &quot;the post about saving tokens with a rust cli&quot; --mode hybrid --explain\nSearch: mode=hybrid, alpha=0.60, candidates: 0 keyword + 80 vector\n\n1. [0.60] filesystem:blog / rust-memory-cheatsheet.md\n    scoring: keyword=0.000  semantic=1.000  → hybrid=0.600\n2. [0.56] filesystem:blog / nes1.md\n    scoring: keyword=0.000  semantic=0.927  → hybrid=0.556\n3. [0.45] filesystem:blog / rtk-token-killer.md\n    scoring: keyword=0.000  semantic=0.745  → hybrid=0.447\n</code></pre>\n<p>Ahead of the right answer: a Rust <em>memory</em> cheatsheet, and an emulator for the <em>NES</em>. The\npost it should have handed me — <a href=\"https://parkerjones.dev/posts/rtk-token-killer/\">\"How I Cut My Claude Code Token Usage by 90% with\nRTK\"</a>, tagged <code>tokens</code>, <code>rust</code>, <code>cli</code>, transparently about saving\ntokens with a Rust CLI — lost to two posts whose main qualification is that they also say\n\"Rust\" a lot.</p>\n<p>Nothing here is dirty. The corpus is 40 blog posts I wrote and proofread. The right document\nis in the index. The query is a plain English question. And <a href=\"https://parkerjones.dev/posts/context-harness-rag-rust/\">Context\nHarness</a> — the retrieval engine I built and have been\n<a href=\"https://parkerjones.dev/posts/building-the-harness-theater/\">writing this series about</a> — confidently ranked a\ncheatsheet about smart pointers above it.</p>\n<p><a href=\"https://parkerjones.dev/posts/building-the-harness-theater/\">Last post</a> I promised this one would be \"what\nretrieval actually buys you, and the specific moments it's slop in, slop out.\" I want to\ncorrect that framing before I use it, because \"slop in, slop out\" is garbage-in-garbage-out,\nand GIGO is the most-written and least-interesting take in all of RAG: clean your chunks, tune\nyour overlap, and so on. My chunks are clean. What bit me was something else, and — I'll spoil\nthe ending — it turned out to be one word of my own code. So this is two posts in one: the bug\nand the fix, which is the honest shape of building anything; and then the larger question the\nbug throws into relief, which is whether you should be reaching for retrieval at all.</p>\n<h2 id=\"the-take-i-m-not-writing\">The take I'm not writing</h2>\n<p>The failure above is not dirty data. It's structural, and it hides inside the word everyone\nreaches for when they want retrieval to sound trustworthy: <strong>hybrid.</strong></p>\n<p>The pitch for hybrid search is genuinely good, which is why everyone repeats it. Keyword search\n— <a rel=\"external\" href=\"https://en.wikipedia.org/wiki/Okapi_BM25\">BM25</a> over an inverted index — nails exact terms\nand whiffs on paraphrase: it'll find \"BM25\" and miss \"that ranking function with term-frequency\nsaturation.\" Semantic search — cosine similarity over embeddings — is the mirror image: catches\nthe paraphrase, misses the exact string. So you run both and blend the scores. Keyword covers\nsemantic's blind spot; semantic covers keyword's. A safety net with two independent ropes.</p>\n<p>Context Harness does exactly this, on purpose. Keyword candidates come from SQLite's FTS5\n<code>bm25()</code>. Vector candidates come from <a rel=\"external\" href=\"https://github.com/parallax-labs/context-harness/blob/main/docs/adr/0004-brute-force-vector-search.md\">brute-force cosine\nsimilarity</a>\nover every chunk — no approximate index, just the dot products. Each side is <a rel=\"external\" href=\"https://github.com/parallax-labs/context-harness/blob/main/docs/adr/0005-hybrid-scoring-with-min-max-normalization.md\">min-max\nnormalized per\nquery</a>,\nthen blended:</p>\n<pre><code data-lang=\"text\">hybrid = (1 - alpha) * keyword_norm + alpha * semantic_norm\n</code></pre>\n<p>with <code>alpha = 0.6</code>, leaning slightly toward semantics. That's the whole scoring model. Two\nropes, one knot. So why did a post that's <em>about</em> saving tokens with a Rust CLI lose to a\nmemory cheatsheet?</p>\n<h2 id=\"read-the-first-line-of-the-output\">Read the first line of the output</h2>\n<pre><code data-lang=\"text\">Search: mode=hybrid, alpha=0.60, candidates: 0 keyword + 80 vector\n</code></pre>\n<p><strong>Zero keyword candidates.</strong> One rope wasn't there. \"Hybrid\" quietly degraded to pure semantic\nsearch, and pure semantics decided \"rust cli tokens\" was closest to a cheatsheet about Rust\nmemory. I only know one rope was missing because I passed <code>--explain</code>; without it I'd have\ngotten a clean-looking ranked list and no hint that half the machinery had switched off.</p>\n<p>Why zero? Watch it flip on a single word:</p>\n<pre><code data-lang=\"text\">❯ ctx search &quot;tokens rust cli&quot; --mode keyword\n1. [1.00] filesystem:blog / rtk-token-killer.md\n\n❯ ctx search &quot;saving tokens rust cli&quot; --mode keyword\n(no results)\n</code></pre>\n<p><code>tokens rust cli</code> finds the right post instantly, perfect score. Add <code>saving</code> — a word that\nisn't in that post — and the entire result set collapses to nothing. That is the behavior of an\n<em>AND</em>: every term required, one miss and you get zero. And a natural-language question is a pile\nof words your target document mostly doesn't contain (<code>the</code>, <code>post</code>, <code>about</code>, <code>saving</code>, <code>with</code>,\n<code>a</code>), so under AND the keyword rope goes slack on exactly the queries a human or a model\nactually types. It only holds for keyword-salad input that nobody writes at a chat box.</p>\n<p>I didn't design an AND. So I opened the file.</p>\n<h2 id=\"one-word\">One word</h2>\n<p>The query text becomes an FTS5 <code>MATCH</code> string in one small function\n(<a rel=\"external\" href=\"https://github.com/parallax-labs/context-harness/blob/main/crates/context-harness/src/sqlite_store.rs\"><code>sqlite_store.rs</code></a>):</p>\n<pre><code data-lang=\"rust\">fn fts_query_from_user_text(query: &amp;str) -&gt; String {\n    query\n        .split(|c: char| !(c.is_alphanumeric() || c == &#39;_&#39;))\n        .filter(|term| !term.is_empty())\n        .collect::&lt;Vec&lt;_&gt;&gt;()\n        .join(&quot; &quot;)          // ← the bug\n}\n</code></pre>\n<p>It splits the query into words and joins them with a space. And in FTS5, a space between terms\n<em>is</em> an implicit AND. So <code>the post about saving tokens with a rust cli</code> becomes a query that\nmeans <em>every one of those words must appear in a single document</em> — which nothing does. The\nkeyword arm wasn't broken. It was doing precisely what I told it to, and what I told it was\nwrong.</p>\n<p>You can watch the fix work before changing any Rust, straight against the FTS index — space\n(AND) versus <code>OR</code>:</p>\n<pre><code data-lang=\"text\">-- implicit AND (space-joined): the current behavior\nsqlite&gt; SELECT bm25(chunks_fts), title FROM chunks_fts ...\n        WHERE chunks_fts MATCH &#39;the post about saving tokens with a rust cli&#39; ...;\n-- (no rows)\n\n-- OR-joined\nsqlite&gt; ... WHERE chunks_fts MATCH &#39;the OR post OR about OR saving OR tokens OR ... OR cli&#39; ...;\n-11.43  rtk-token-killer.md\n -9.01  context-harness-rag-rust.md\n -7.65  rtk-token-killer.md\n</code></pre>\n<p>OR lets a document match on <em>any</em> term, and BM25 does the rest: common words like <code>the</code> and\n<code>with</code> appear everywhere, so their inverse-document-frequency weight is near zero and they\ncontribute almost nothing; rare, load-bearing words like <code>tokens</code> and <code>rust</code> and <code>cli</code> drive the\nscore. The right post wins on the terms that matter. So the fix is one word:</p>\n<pre><code data-lang=\"diff\">-        .join(&quot; &quot;)\n+        .join(&quot; OR &quot;)\n</code></pre>\n<h2 id=\"the-after\">The after</h2>\n<p>Rebuild <code>ctx</code>, re-run the exact query that started this — same corpus, same index, nothing\nre-embedded, only the query construction changed:</p>\n<pre><code data-lang=\"text\">❯ ctx search &quot;the post about saving tokens with a rust cli&quot; --mode hybrid --explain\nSearch: mode=hybrid, alpha=0.60, candidates: 80 keyword + 80 vector\n\n1. [0.85] filesystem:blog / rtk-token-killer.md\n    scoring: keyword=1.000  semantic=0.745  → hybrid=0.847\n2. [0.69] filesystem:blog / rust-memory-cheatsheet.md\n    scoring: keyword=0.224  semantic=1.000  → hybrid=0.690\n3. [0.67] filesystem:blog / nes1.md\n    scoring: keyword=0.364  semantic=0.878  → hybrid=0.673\n</code></pre>\n<p><code>candidates: 80 keyword + 80 vector</code> — both ropes taut. The token-killer post goes from third to\nfirst; the cheatsheet and the emulator, which only ever had the semantic rope, drop below it.</p>\n\n\n<figure class=\"post-image\">\n  <img src=\"https://parkerjones.dev/processed_images/retrieval-before-after.17baee1f49fc6711.webp\" width=\"1600\" height=\"840\"\n       alt=\"Side-by-side terminal output of the same ctx search query before and after the one-word fix. Before, with terms joined by a space, the keyword arm returns 0 candidates and rtk-token-killer.md ranks third, behind a Rust memory cheatsheet and a NES emulator. After, with terms joined by OR, the keyword arm returns 80 candidates and rtk-token-killer.md ranks first.\" loading=\"lazy\" decoding=\"async\">\n  <figcaption>The same query, before and after the one-word change. Left: `join(&#x27; &#x27;)` — 0 keyword candidates, the right post buried at #3. Right: `join(&#x27; OR &#x27;)` — 80 keyword candidates, the right post first.</figcaption>\n</figure>\n<p>That's the entire fix, and it's the whole reason to instrument retrieval in the first place: the\n<code>--explain</code> line is what turned \"the results feel off\" into \"one arm reports zero candidates,\"\nwhich is a bug you can find in a file instead of a vibe you argue about.</p>\n<p>It wasn't a one-off, either. Earlier I'd asked a four-repo index — 446 documents, roughly 700,000\ntokens — how its <em>own</em> hybrid scoring normalizes, and the document literally titled\n<code>hybrid-scoring-with-min-max-normalization</code> came in <strong>second</strong>, beaten by fifteen thousandths of\na point by a vaguer doc that caught the one stray keyword hit. After the fix:</p>\n<pre><code data-lang=\"text\">❯ ctx search &quot;how does hybrid scoring normalize keyword and semantic scores&quot; --mode hybrid --explain\n1. [1.00] filesystem:ch / 0005-hybrid-scoring-with-min-max-normalization.md\n    scoring: keyword=1.000  semantic=1.000  → hybrid=1.000\n</code></pre>\n<p>First, perfect score. The engine can now find the spec that describes the engine. And the query\nthat already worked — a keyword-rich <code>set up remote build machines for nix</code>, which was pulling\nits right answer at a clean <code>1.000</code> before — still sits at <code>1.000</code> after, with the cross-repo\nrunbook it surfaced from a <em>different</em> repo actually scoring higher than before. On the three\nqueries that defined the problem, the right answer is now first, and the control didn't regress.</p>\n<p>I'm being careful with that sentence, because OR is not free — it broadens the candidate pool,\nso somewhere it has to cost me. So I ran a bigger check: fifteen queries by hand across both\ncorpora, each with the answer I expected written down first. Twelve came back identical, two\nimproved, and one regressed — <em>how do I run a database in the browser</em> used to put my\n<a href=\"https://parkerjones.dev/posts/databses-in-the-browser/\">database-in-the-browser post</a> first, and now ranks a\nclosely related SurrealDB-caching post there instead, with the on-the-nose post bumped to\nsecond. OR pulled in a neighbor and the blend preferred it: a real, if small, tax, landing\nexactly where I'd have guessed — a keyword-shaped query where AND's precision was quietly doing\nwork. Better than three cherry-picked wins. Still fifteen queries I chose, graded against\nanswers I decided were right, which is not a benchmark — it's marking my own homework, and it's\nthe next post's problem. For now the honest claim is narrow and true: a one-word bug was\nzeroing out half of hybrid search on every natural-language query, and now it isn't.</p>\n<h2 id=\"the-bug-is-the-argument\">The bug is the argument</h2>\n<p>Here's the part that outlasts the fix. A single word — a space where an <code>OR</code> belonged — silently\ndisabled one of the two retrieval strategies I built the whole engine around, on exactly the\ninputs it would see in real use, and it produced <em>confident, plausible, cited</em> wrong answers the\nentire time. No crash. No error. A ranked list with scores, every one of them a lie of omission.\nI'd have kept trusting it if I hadn't gone looking.</p>\n<p>That is the true cost of retrieval, and it's why the interesting question isn't \"how do I make\nRAG better,\" it's \"should I be running RAG here at all.\" Because look at what the whole episode\nwas <em>for</em>: those 40 blog posts are about 60,000 tokens. They fit in a context window with room\nto spare. The correct thing to do with a corpus that small is not to retrieve over it — it's to\npaste every post into the prompt, let <a rel=\"external\" href=\"https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching\">prompt\ncaching</a> make the repeat\ncost trivial, and let the model read all of it. Do that and this bug <em>cannot happen to you</em>,\nbecause there's no ranking step to get wrong. There's no #1 to incorrectly promote, because there\nis no list. I introduced an entire class of silent failure to solve a problem — \"which twelve\nchunks?\" — that, at this scale, I did not have.</p>\n<p><strong>Cache-and-dump is the monolith of context: unglamorous, and correct right up until it\nphysically won't fit in the window.</strong> Retrieval is the service you extract when the monolith\noverflows — and, exactly like extracting a microservice, the moment you do it you sign up for an\noperational surface the monolith never had. Query construction. FTS semantics. Score\nnormalization. A one-word bug that halves your quality without telling you. You don't take that\non because it's elegant. You take it on because you were forced to. And the reasons that force\nyou are specific — not \"it's cheaper\" (with caching, dumping is cheap; <a href=\"https://parkerjones.dev/posts/building-the-harness-theater/\">the invoice going up\nisn't the work getting better</a>), but capability:</p>\n<ol>\n<li><strong>Scale past the window</strong> — a corpus you genuinely can't fit, like those four repos.</li>\n<li><strong>Relevance you can't predict</strong> — the one document out of hundreds you didn't know you'd need\nuntil the question existed.</li>\n<li><strong>A corpus that changes</strong> — so you don't re-shovel and re-cache everything on every edit.</li>\n<li><strong>Attention rot</strong> — even when it all fits, <a rel=\"external\" href=\"https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents\">more context is not free\naccuracy</a>;\na model handed 700K tokens attends worse than one handed the right 5K.</li>\n</ol>\n<p>That cross-repo runbook the fixed engine surfaced — a doc from one repository answering a\nquestion I'd only documented in another, neither aware the other exists — is retrieval earning\nits keep, and it earned it because reason #1 was real: 700K tokens is not going in a window. On\nthe blog, none of the four apply, so retrieval had no job, and inventing one for it is how I\nended up ranking a smart-pointer cheatsheet above the post I asked for.</p>\n<p>This is the same lesson as <a href=\"https://parkerjones.dev/posts/building-the-harness-theater/\">last post</a>, one layer down.\nThere I'd chopped a solo project into 78 documents and six agents because decomposition <em>felt</em>\nlike rigor, and called it microservices-on-myself. Retrieval on a corpus that fits in the window\nis the identical mistake: paying the coordination cost of a distributed system to solve a problem\na monolith already had handled. Extract the service when you're forced. Not before.</p>\n<h2 id=\"distrust-the-retriever-enough-to-instrument-it\">Distrust the retriever — enough to instrument it</h2>\n<p>There's a discipline I lean on hard at work; I'll keep it generic, but it travels: treat the\nmodel as an <em>advisor to be verified, not an authority to be trusted</em>. You don't take its\nconfident output at face value — you build the system so the output gets checked. Almost nobody\nextends that same suspicion to the <em>retriever</em>. Retrieved context gets a pass. It feels like\nground truth: it came from your own documents, it has a similarity score, it's the thing you\npoint to when someone asks whether the model made it up. But <code>rust-memory-cheatsheet</code> came back\nat <code>0.60</code> for a question about token savings, and that number was never a measure of whether it\nwas <em>right</em> — it was a measure of embedding proximity with one of two ropes cut, which is a very\ndifferent thing wearing the same costume. The model downstream can't tell the difference. It\nreceives the top chunk as grounding and does what it does with grounding: builds a confident,\nwell-cited answer on top of it. On a bad ranking, retrieval doesn't remove the confabulation\nrisk — it <em>supplies</em> the confabulation with a footnote.</p>\n<p>So distrust it. But the useful form of distrust isn't a shrug about how retrieval is\ntreacherous; it's <code>--explain</code>. Instrument the thing so that \"this feels wrong\" becomes \"the\nkeyword arm returned zero candidates,\" which is a bug with a location and a fix. Suspicion that\nends in humility is a pose. Suspicion that ends in a diff is engineering.</p>\n<h2 id=\"where-this-goes\">Where this goes</h2>\n<p>Context Harness got one bug better this afternoon, in public, and I'll take it — but the honest\nscope of what I proved is: on three queries that used to fail, the right answer is now first. The\nreranker I keep <a rel=\"external\" href=\"https://github.com/parallax-labs/context-harness/blob/main/docs/prd/0009-retrieval-quality-and-dogfooding.md\">deferring</a>\nwould catch a different, harder class of miss; OR-joining is not the last word on query\nconstruction; and I still can't tell you, with a number, whether the engine is <em>good</em> — only that\nit's better on the cases I looked at, which is exactly how you fool yourself.</p>\n<p>Which is the next problem, and a harder one than a one-word bug: I've spent this whole post saying\n\"first\" and \"better\" and \"wrong\" as if I can measure any of it. I mostly can't — not the way a\nbenchmark would — and the gap between what I can measure and what actually matters is where the\nnext post lives.</p>\n<p>— Parker Jones</p>\n",
      summary: null,
      date: "2026-07-09T00:00:00Z",
      reading_time: 13,
      metadata: {"cover":"images/building-harness-retrieval-cover.png","featured":true,"series_order":2},
      tags: ["ai","agents","rag","retrieval","context-engineering","claude-code","mcp","rust","local-first"],
      categories: ["software"],
      series: ["building-the-harness"],
      projects: []
  };



  
  
  
  

  
    
  
  
    
  
  
    
  
  

  CREATE post CONTENT {
      title: "I Built a Harness to Build a Harness. One of Them Worked.",
      slug: "building-the-harness-theater",
      path: "https://parkerjones.dev/posts/building-the-harness-theater/",
      content: "<p>Here is the number I keep not saying out loud: <strong>78.</strong></p>\n<p>That's how many numbered documentation files I wrote to build <a href=\"https://parkerjones.dev/posts/context-harness-rag-rust/\">Context Harness</a> — 13 product requirements docs, 23 architecture decision records, 15 specs, 9 design docs, 18 runbooks — plus five policy documents governing them, plus six AI agents whose only job is to help write more. For a project with one developer. Me.</p>\n<p>And when I went back through the git log to write this, the surprise was that the harness worked. I used it — the doc pipeline, the six agents, the whole apparatus — to build the engine feature by feature, and the commits show the seams. That part isn't the confession.</p>\n<p>Context Harness works. It's a local-first RAG engine in Rust: ingests your files, chunks and embeds them, serves <a rel=\"external\" href=\"https://github.com/parallax-labs/context-harness/blob/main/docs/spec/0003-hybrid-scoring.md\">hybrid search</a> to any AI tool over MCP. Single binary, six build targets, <a rel=\"external\" href=\"https://github.com/parallax-labs/context-harness/blob/main/docs/adr/0004-brute-force-vector-search.md\">brute-force cosine similarity</a> that clears 10,000 chunks in single-digit milliseconds. I'm not being falsely modest about the engineering — it's real. <a rel=\"external\" href=\"https://github.com/parallax-labs/context-harness\">Forty strangers starred it</a>; a couple hundred people pulled the release binaries across eight versions. What I can't point to is one person — myself very much included — who has aimed it at a problem that wasn't itself. Months on, the only corpus it has really indexed is its own source code: a tool built to hand your context to an AI, whose only context has ever been <em>itself</em>. Stars are interest, downloads are curiosity, and neither is a job.</p>\n<p>So before I write four more posts about \"harness engineering,\" here's what I'm actually holding: a harness that did its job — it built the engine — and an engine still waiting for one of its own. Both real. One proven, one not. The gap between them is the whole series.</p>\n<h2 id=\"the-word-everyone-s-using-and-nobody-defines-the-same-way\">The word everyone's using and nobody defines the same way</h2>\n<p>\"Harness engineering\" arrived fully formed sometime this spring, the way these things do. <a rel=\"external\" href=\"https://robearlam.com/blog/an-introduction-to-harness-engineering\">Rob Earlam wrote a genuinely good five-part series</a> building his own website with an agent pipeline — a Product Owner agent hands to a Design agent hands to a Tech Lead hands to Build hands to QA, an Orchestrator reading a <code>run-state.md</code> between them. It's clean, it's sequential, and if you've ever shipped software you'll recognize the shape immediately: it's the SDLC, staffed by robots.</p>\n<p>Meanwhile the phrase itself has at least three parents. <a rel=\"external\" href=\"https://mitchellh.com/writing/my-ai-adoption-journey\">Mitchell Hashimoto</a> describes <em>engineering the harness</em> as a habit: any time the agent makes a mistake, you take the time to fix its environment so it can't make that mistake again. LangChain, in <a rel=\"external\" href=\"https://www.langchain.com/blog/the-anatomy-of-an-agent-harness\">\"The Anatomy of an Agent Harness\"</a>, gave us the tidy equation <strong>Agent = Model + Harness</strong> — \"if you're not the model, you're the harness.\" And <a rel=\"external\" href=\"https://martinfowler.com/articles/harness-engineering.html\">Birgitta Böckeler, on Martin Fowler's site</a>, frames it as a <em>cybernetic governor</em> — feedforward guides that steer before the agent acts, feedback sensors that catch it after — a steering loop, not a one-pass pipeline.</p>\n<p>Everyone cites the same kind of stat: hold the model fixed, change only the harness, watch the score jump. The cleanest one I can actually source is LangChain's — same model (<code>gpt-5.2-codex</code>), harness-only changes, <a rel=\"external\" href=\"https://www.langchain.com/blog/improving-deep-agents-with-harness-engineering\">52.8% to 66.5% on Terminal-Bench 2.0</a>, enough to move from outside the top 30 to the top 5. The more dramatic one making the rounds — <a rel=\"external\" href=\"https://hal.cs.princeton.edu/corebench_hard\">42% to 78%</a> — is real too, but it's Claude Opus 4.5 on <em>scientific-reproducibility</em> tasks, and by the time it reaches the LinkedIn post it's lost the part where that isn't the coding you do all day. I'll wave the numbers around like everyone else. I'll also tell you I've never reproduced them, and neither, probably, have you.</p>\n<p>What I notice about all of it: it's triumphant. The scores only ever go up and to the right; the pipelines are always clean. Nobody in this literature is holding 78 documents and an engine whose only real user, so far, is itself.</p>\n<h2 id=\"what-the-harness-actually-was\">What the harness actually was</h2>\n<p>Strip the grandeur and here's the machine. Five directories, each a level of authority: <code>docs/prd/</code> (what to build and why), <code>docs/adr/</code> (why I picked an approach), <code>docs/spec/</code> (exactly how it behaves — the contract), <code>docs/design/</code> (thinking out loud, non-authoritative), <code>docs/runbook/</code> (how to operate it). Every file numbered <code>NNNN-kebab-title.md</code>; every directory fronted by a <code>0000</code> policy that governs the rest.</p>\n<p>Then six agents — one per layer, plus a coordinator — and here's the part that matters: <strong>they don't run anything.</strong> A Context Harness agent is a Lua script that generates a prompt and then stops. It never calls a model. Invoke <a rel=\"external\" href=\"https://github.com/parallax-labs/context-harness/blob/main/agents/spec-writer.lua\"><code>spec-writer</code></a> and it prints exactly what it would hand one:</p>\n<pre><code data-lang=\"text\">❯ ctx agent test spec-writer --arg feature=&quot;hybrid search API&quot;\nAgent: spec-writer\nSource: lua (agents/spec-writer.lua)\nTools: search, get\n\nSystem prompt (2343 chars):\n  You are a Spec Writer for Context Harness.\n  Your job is to write authoritative specifications that conform to the Spec Policy.\n  ...\n  ### When to Write a Spec\n  PREFERRED: Implement the feature first, then write the spec describing actual behavior.\n  ...\n\nTools override: search, get\nResolved in 239ms\n</code></pre>\n<p>In 239 milliseconds, without calling a model once, it did three things. It assembled that 2,343-character system prompt — the whole spec policy, inlined, down to <em>no \"could,\" no \"might.\"</em> It ran a hybrid search over everything already indexed (three queries: the feature, <code>spec &lt;feature&gt;</code>, <code>&lt;feature&gt; behavior</code>), deduped the hits, sorted them into <em>existing specs</em> and <em>related docs</em>, and pre-seeded them as context — <em>here's what already exists; <code>get</code> the full text before you write.</em> Then it handed back the prompt, a scoped toolset (<code>search</code>, <code>get</code>), and that retrieved context — and stopped. Whatever model is driving your editor does the actual writing.</p>\n<p>The <a rel=\"external\" href=\"https://github.com/parallax-labs/context-harness/blob/main/agents/doc-coordinator.lua\"><code>doc-coordinator</code></a> sits on top and is even more hands-off. Give it a feature and it searches all five layers, reports what exists and what's missing, and emits an ordered plan — <em>write this PRD, then this ADR, use <code>spec-writer</code> for the contract.</em> Its own instructions state the boundary in one line: \"You coordinate but do not write docs yourself.\"</p>\n<p>So the real output was never the documents. It was a <strong>prompt with the right context already loaded into it</strong> — produced deterministically, handed to a model the harness doesn't own. Assemble context, borrow the model. Hold that thought; it turns out to be the entire thesis of this series, and I shipped it as a side effect before I had the words for it.</p>\n<p>What the machine emitted in the end: <a rel=\"external\" href=\"https://github.com/parallax-labs/context-harness/tree/main/docs\">78 numbered documents</a> and a RAG engine that conforms to them. ADR-0015 is a representative specimen — status, context, decision, five numbered principles, alternatives, consequences. Multiply by 78. It is, and I mean this, a genuinely handsome pile of documents.</p>\n<p>And I did point it at real work — its own. A <a rel=\"external\" href=\"https://github.com/parallax-labs/context-harness/blob/main/config/ctx.toml\">checked-in <code>config/ctx.toml</code></a> aims the connectors at Context Harness's own <code>./docs</code>, its <code>./crates/**/*.rs</code>, and its README, registers all six agents, and opens with a single instruction: <code># Run from repo root: ctx sync all &amp;&amp; ctx serve</code>. The harness indexed the codebase it was building and fed that back to the agents building it. You can watch it in the log — <code>design: multi workspace routing design</code>, then <code>feat: multi-workspace MCP router</code> two days later; <code>feat: sync progress on stderr (design from SYNC_PROGRESS.md)</code> — 145 commits across four months, design turning into features. So when I say the harness is unproven, I don't mean it didn't work. It worked. I mean the <em>engine</em> it built never got aimed at anyone else's problem.</p>\n<h2 id=\"how-it-works-driving-it-from-claude-code\">How it works: driving it from Claude Code</h2>\n<p>The agents aren't a CLI party trick — they're served over MCP, so the editor drives them. <a rel=\"external\" href=\"https://parallax-labs.github.io/context-harness/\">Install <code>ctx</code></a>, start the server, and point Claude Code at it:</p>\n<pre><code data-lang=\"bash\">ctx serve mcp   # search + the six agents, at 127.0.0.1:7331/mcp\nclaude mcp add --transport http context-harness http://127.0.0.1:7331/mcp\n</code></pre>\n<p>Type <code>/mcp</code> in Claude Code and the server's capabilities show up split into exactly the two halves from a moment ago. The <strong>agents are MCP prompts</strong> — you fire them deliberately, as slash commands like <code>/mcp__context-harness__spec-writer</code>. The <strong><code>search</code> and <code>get</code> are MCP tools</strong> — the model reaches for them on its own, mid-thought, as <code>mcp__context-harness__search</code>. Prompts are yours to invoke; tools are the model's to call. That split is the entire design in one line.</p>\n<p>Here's a feature going through it, the way the multi-workspace router actually did. You start at the top, with the coordinator:</p>\n<pre><code>/mcp__context-harness__doc-coordinator &quot;multi-workspace routing&quot;\n</code></pre>\n<p>It hybrid-searches all five doc layers, tells you what exists and what's missing, and hands back a plan — <em>write a PRD, then an ADR, lock the contract with <code>spec-writer</code></em> — then stops, because it coordinates but does not write. You work the plan by firing the writers it named, one at a time. Each drops a fully-loaded starting point into the chat — the policy inlined, plus the existing docs it retrieved — and from there the model in Claude Code does the writing, calling <code>search</code> and <code>get</code> itself to pull the full text of anything it needs. Then you implement against the spec and commit. <code>design:</code>, then <code>feat:</code>. The trail in the log isn't an accident; it's the shape of the pipeline.</p>\n\n\n<figure class=\"post-image\">\n  <img src=\"https://parkerjones.dev/processed_images/harness-flow.a0d7ef39d46c7fd8.webp\" width=\"1600\" height=\"1128\"\n       alt=\"Driving the harness from Claude Code: after ctx serve mcp and claude mcp add, you fire the doc-coordinator then spec-writer prompts inside Claude Code; each returns a context-loaded prompt; the model writes the spec and code, calling the search and get tools itself; the run ends in design: and feat: commits.\" loading=\"lazy\" decoding=\"async\">\n  <figcaption>The feature-building loop, driven from Claude Code — you fire the agents (prompts), the model does the work, and the harness only plans and retrieves.</figcaption>\n</figure>\n<p>Notice what Context Harness never does in that loop: run a model. It plans, retrieves, and assembles; Claude Code brings the intelligence. <em>Assemble context, borrow the model</em> — the same four words as before, except now you can watch them run.</p>\n<p>It borrows something else, too: the control loop. Three different things get called \"orchestration,\" and it's worth being precise about which one is happening here.</p>\n<ul>\n<li><strong>The loop</strong> — a model calling tools in a cycle toward a goal — is Claude Code's, not the harness's.</li>\n<li><strong>Subagent orchestration</strong> — one agent <em>autonomously</em> spawning and coordinating others (Claude Code's own sub-agents; Rob's Orchestrator firing PO → Design → Build → QA off a state file) — the harness doesn't do at all. The <code>doc-coordinator</code> hands you a plan; it <em>can't</em> fire the writers itself, because MCP prompts are user-invoked and the model can't chain them.</li>\n<li><strong>What Context Harness does</strong> is neither: it's a human-fired <em>context-and-plan injector</em> into someone else's loop. You're the orchestrator; the harness just makes sure each step starts with the right context already loaded.</li>\n</ul>\n<p>Whether that's a limitation or the whole point — whether autonomous orchestration earns its keep, or \"read the plan, fire the next command\" is the boring thing that wins — is a post later in this series. For now the split is clean: the harness assembles the context; the loop and the orchestration are borrowed.</p>\n<h2 id=\"i-did-microservices-to-myself\">I did microservices to myself</h2>\n<p>Now set my machine beside Rob's. His: a Product Owner agent handing to Design handing to Tech Lead handing to Build handing to QA, an orchestrator shuttling between them. Mine: a coordinator that plans and delegates to single-purpose writers, one per layer. Same shape. We each took a process, chopped it into specialized agents, and wired them to an orchestrator.</p>\n<p>We built microservices.</p>\n<p>Not literally — but the instinct is identical, and so is the failure mode. Remember how that went? For about five years the industry believed the answer to every architecture question was \"more, smaller services.\" Then everyone counted the operational cost of all that wiring — the network calls, the distributed failure modes, the sheer ceremony — and quietly retreated to the <a rel=\"external\" href=\"https://signalvnoise.com/svn3/the-majestic-monolith/\">majestic monolith</a>. <a rel=\"external\" href=\"https://martinfowler.com/bliki/MonolithFirst.html\">Start with the monolith</a>; extract a service only when something concrete forces you to. Most teams that went services-first paid for it.</p>\n<p>The field didn't land on \"it depends, it's all just taste.\" It developed a spine: a boring default, with an opinion attached. And I think harness engineering is going to travel the exact same arc. Right now we're in the services-first phase — everyone's building elaborate multi-agent pipelines because decomposition <em>feels</em> like rigor. In a couple of years the default will be boring: one capable agent, good context, and a small amount of plumbing that nobody finds impressive. The harness that wins will be the one you can't demo.</p>\n<p>I have proof this is already happening, because it happened inside my own repo. My <a rel=\"external\" href=\"https://github.com/parallax-labs/context-harness/blob/main/docs/adr/0015-spec-driven-development.md\">spec-driven-development ADR</a> is emphatic: we <em>program to the spec</em> — the implementation conforms to the spec, and the spec is never updated to match the code after the fact. Very rigorous. Very impressive. And then, three principles down in the very same document, the <em>preferred workflow</em> turns out to be: <strong>implement first, then write the spec.</strong> The ceremony and the pragmatism are sitting side by side, and the pragmatism already won. I just hadn't noticed I'd written down the fight and its outcome.</p>\n<h2 id=\"where-i-might-be-wrong\">Where I might be wrong</h2>\n<p>The honest counter to \"it deflates into plumbing\" is one word: <strong>Kubernetes.</strong></p>\n<p>Kubernetes did not get simpler. It got <em>abstracted</em>. The complexity is all still there — it just moved behind a managed platform you rent, and the \"boring default\" turned out to be enormous and someone else's problem. That's the other arc this could take, and there are companies betting hard on it right now: <a rel=\"external\" href=\"https://www.langchain.com/langgraph-platform\">LangChain</a>, <a rel=\"external\" href=\"https://www.databricks.com/blog/introducing-omnigent-meta-harness-combine-control-and-share-your-agents\">Databricks</a>, and everyone else selling a managed harness where the orchestration lives in their cloud and you stop thinking about it.</p>\n<p>So maybe I'm wrong. Maybe the harness doesn't deflate; it gets rented. The signal I'm watching for is which one becomes <em>invisible</em> first — the plumbing, or the platform. If in two years the interesting work is choosing which harness vendor to lock into, the monolith people lost. If it's a boring config file in your repo that you barely think about, I was right. I genuinely don't know yet, and I'd rather tell you that than pick the ending that flatters my thesis.</p>\n<h2 id=\"would-i-build-it-again-yes-was-most-of-it-ceremony-also-yes\">Would I build it again? Yes. Was most of it ceremony? Also yes.</h2>\n<p>Both are true, and holding both is the only honest position. I would write the specs again — the act of stating what the system <em>should</em> do, in language that forbids \"might\" and \"could,\" caught real design mistakes before they cost me anything. But if you made me bet, maybe three of those 78 documents are why the project didn't collapse into mud, and the other 75 I performed because ceremony feels like progress. <strong>Figuring out which three is the actual point of this series.</strong> Not \"here's my framework, adopt it.\" I don't trust anyone who's that sure this early, including the version of me who wrote 78 files.</p>\n<h2 id=\"the-nail\">The nail</h2>\n<p>Here's the part that took me embarrassingly long to say plainly. I don't have a \"team context problem\" or a \"knowledge management problem.\" Those are euphemisms, and they smuggle in a lie — that the cupboard is bare. It isn't.</p>\n<p>At work I have real pieces. A registry of reusable skills and agent rules a model can discover on demand over MCP. A hard-won framework for building systems that <em>check</em> the model instead of trusting it — that treat it as an advisor to be verified, not an authority. Both genuinely AI-native. And they don't compose. The skills live in one place; the principles live as a thousand-line design document in another; neither travels. An engineer starting in a different repo can't ask \"how do I apply this here?\" and get a usable answer — they go find the document, read the whole thing, extract the idea, and re-translate it by hand, in whatever editor they happen to be in. The knowledge is either too welded to the system that birthed it to reuse, or too abstract to apply. A <code>CLAUDE.md</code> in one repo is great — for that one repo. It composes across exactly none of the others.</p>\n<p>So what I want is a <strong>declarative harness for day-to-day development</strong>: reproducible, transparent, portable across whatever editor I'm using this month — Cursor, Claude Code, whatever ships next — where a hard-won principle becomes a discoverable skill that travels across repos, and \"give the agent cross-repo awareness\" does <em>not</em> mean pasting every repo's markdown into the context window until it's <a rel=\"external\" href=\"https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents\">full of text and short on attention</a>.</p>\n<p>In the meantime, what I actually run is duct tape: one <code>CLAUDE.md</code> at the root above all my repos, a wiki, an Obsidian vault. It works — I think. I can't prove it. The only number I can put on it is token spend, which is high, because the whole approach is concatenation: shovel the context in and hope the model reads the right part of it. \"The invoice went up\" is not the same as \"the work got better,\" and the distance between those two is the honest state of the art. The elaborate harness did its one job — it built the engine — and went quiet. The boring one I lean on every day, and still can't measure.</p>\n<p>Say that requirement out loud and the redemption is obvious: cross-repo awareness <em>without</em> dumping everything into context is retrieval, not concatenation. Which is the one thing the engine I shelved actually does. I built a retrieval harness, wrote it off because it had no job outside itself, and the whole time it was standing next to the job.</p>\n<p>So maybe you have the job. It's the same wiring as above — except this time you point <code>ctx.toml</code> at <em>your</em> repos, the several that don't know about each other, and <code>ctx sync all</code> collapses them into one index. Now the model in Claude Code can <code>search</code> and <code>get</code> across all of them, retrieved on demand instead of pasted: your cross-repo context on tap, without shoveling it into the window. It's <a rel=\"external\" href=\"https://github.com/parallax-labs/context-harness\">open source</a>, the <a rel=\"external\" href=\"https://parallax-labs.github.io/context-harness/docs/\">docs</a> are thorough, and I'd genuinely like to hear from anyone who finds the deployment I haven't.</p>\n<p>The bet — and it is a bet, not a conclusion — is that Context Harness stops being a search box and becomes the thing that <strong>self-hosts the harness</strong>: the agents, the roles, the workflow, backed by a knowledge base you build once and carry with you. The doc-writing agents it already ships are the proof of concept; the missing piece is making that harness portable and its knowledge base buildable and sideloadable — the same substrate underneath Cursor today, Claude Code tomorrow, whatever ships next, local-first and inspectable, not rented from a cloud. That's the thread the rest of this series pulls on. Not a competing editor: I have no interest in fighting Anthropic and Google over the surface, and it's the wrong altitude anyway. The plumbing is the bet — in the prediction, and in what I'm building.</p>\n<p>Whether it works, I'll find out in public. There's an honest chance that <a rel=\"external\" href=\"https://modelcontextprotocol.io/specification\">MCP</a> carries context and tools but not the control loop — no primitive in the spec owns the agent's orchestration; that lives in the host — in which case \"portable harness\" is really \"portable context, borrowed orchestration,\" and I'll say so when I hit that wall. That's the deal for this whole series: real receipts, real numbers, and the parts where I was wrong or just performing rigor, on the record.</p>\n<p>Next: what retrieval actually buys you, and the specific moments it's slop in, slop out.</p>\n<p>— Parker Jones</p>\n",
      summary: null,
      date: "2026-07-06T00:00:00Z",
      reading_time: 17,
      metadata: {"featured":true,"series_order":1},
      tags: ["ai","agents","claude-code","context-engineering","harness-engineering","mcp","rust","local-first"],
      categories: ["software"],
      series: ["building-the-harness"],
      projects: []
  };



  
  
  
  

  
    
  
  
    
  
  
    
  
  

  CREATE post CONTENT {
      title: "A Hammer Still Looking for Its Nail: Context Harness and the Team-Context Problem",
      slug: "context-harness-site-search",
      path: "https://parkerjones.dev/posts/context-harness-site-search/",
      content: "<p><a href=\"https://parkerjones.dev/posts/context-harness-rag-rust/\">Part 1</a> was the engine: a local-first RAG index in Rust — SQLite, hybrid search, Lua connectors, served to any tool over MCP. This post is the harder question one layer up, the one the engine only makes sense as an answer to: <strong>context that stays current and shareable across a team</strong> — and whether <a href=\"https://parkerjones.dev/posts/context-harness-rag-rust/\">Context Harness</a> is really the shape of that answer, or a hammer I enjoyed building and then went looking for a nail to justify.</p>\n<span id=\"continue-reading\"></span>\n<p>I want to be honest about the register here, because it's different from Part 1. That post was a retrospective — a thing I built, surveyed, and understood. This one isn't finished. Context Harness is a work in progress, and I'm still genuinely working out whether its core bet is right. So this is me reasoning in public, dogfooding a tool and watching whether it earns its place — not selling you a conclusion I don't have. Pretending otherwise would be its own kind of slop, which is funny, because slop is the thing the tool exists to fight.</p>\n<h2 id=\"the-itch\">The itch</h2>\n<p>Context Harness came from confabulation. AI tools are great until they start confidently inventing details about <em>my</em> code — a function signature that doesn't exist, a config key I never wrote, a system summarized the way it isn't. The fix is grounding: give the model the real material and it stops guessing. <a rel=\"external\" href=\"https://cursor.com/\">Cursor</a> does this with semantic search over a repo. I wanted that, but portable, multi-repo, and pointable at any pile of my own work — and the index it builds is a database (a SQLite file, hybrid keyword-plus-vector search, served to any tool over MCP). That's the engine I <a href=\"https://parkerjones.dev/posts/context-harness-rag-rust/\">described in Part 1</a>.</p>\n<h2 id=\"i-m-already-in-the-format-camp\">I'm already in the format camp</h2>\n<p>Before I defend the engine, I should admit which side I've actually lived on. The loudest current answer to \"how do you give AI context\" isn't an engine at all — it's a <strong>format</strong>: markdown with frontmatter, in a directory, that agents read directly. Google's <a rel=\"external\" href=\"https://cloud.google.com/blog/products/data-analytics/how-the-open-knowledge-format-can-improve-data-sharing\">Open Knowledge Format</a>, <a rel=\"external\" href=\"https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f\">Karpathy's LLM-wiki</a>, the <code>AGENTS.md</code> convention, every Obsidian vault. No SDK, no runtime, no index — the format <em>is</em> the contribution, and a long-context model just reads the files.</p>\n<p>I'm not skeptical of this from the outside. I've kept markdown knowledge vaults for years — personal and professional — since before \"LLM wiki\" was a phrase. For one person, or a couple of people who trust each other's notes, it is genuinely the right tool. Lightweight, portable, human-readable, version-controlled. If that's your scale, you probably don't need anything I'm building.</p>\n<h2 id=\"where-the-format-quietly-breaks\">Where the format quietly breaks</h2>\n<p>It breaks at <em>team</em> scale, and I've watched it break. Past about three people, a shared markdown corpus develops the same disease every wiki has ever had: it goes stale, nobody owns curating it, and it grows into an overwhelming pile of text that no one trusts is current. The format assumes someone keeps the files good. At small scale that someone exists. On a large engineering team riddled with silos, they don't — and \"just read the folder\" turns into \"read a thousand documents, half of them six months out of date, and hope.\"</p>\n<p>This isn't a niche complaint. Portable, shareable, <em>current</em> team context is, as far as I can tell, an unsolved problem — even well-resourced internal tooling at large companies hasn't really cracked it. That part I'm confident about. It's the part I'm <em>less</em> sure about that this whole post is circling.</p>\n<h2 id=\"the-bet-i-m-dogfooding\">The bet I'm dogfooding</h2>\n<p>Context Harness's bet is that the thing markdown can't be — at team scale — is exactly what a context <em>engine</em> can: a shared store that <strong>stays current</strong> (it re-syncs from the sources instead of waiting for a human to update prose), is <strong>shareable</strong> (the index is an artifact a team can pull, not a vault each person curates alone), and <strong>possesses skills</strong> rather than being inert text — the Lua connectors, tools, and agents that let it ingest weird sources and act on what it finds. A worthy context tool, the way I think about it, is a current, shareable thing that <em>does</em> things — not an overwhelming corpus that rots.</p>\n\n\n<figure class=\"post-image\">\n  <img src=\"https://parkerjones.dev/processed_images/context-harness-format-vs-engine.c815f8fc3cad4f9d.webp\" width=\"1600\" height=\"1000\"\n       alt=\"Two-column comparison. Left, the format camp: a markdown vault labelled lightweight, portable, human-readable, with an arrow down to a team-scale failure state — stale, unowned, untrusted. Right, the engine camp: Context Harness, a database index that stays current by re-syncing from sources, is shareable as one artifact instead of a per-person vault, and carries skills via Lua connectors, tools, and agents. A vertical line marks the team-scale threshold where the left side rots and the right side is the bet.\" loading=\"lazy\" decoding=\"async\">\n  <figcaption>The whole argument in one picture: the format camp is right up to team scale, where it rots — and the bet is that a current, shareable, skill-bearing index is the thing markdown structurally can&#x27;t be.</figcaption>\n</figure>\n<p>Two honest clarifications, because they matter and they cut against my own earlier framing:</p>\n<ul>\n<li><strong>\"Local-first\" is mostly a cover story.</strong> Under the hood the design is <em>configuration-driven, with multiple backends</em> — local is one configuration, not a religion — and yes, there's a paid tier in the back of my mind. That flexibility is precisely what could make it team-shareable; a purist local-only tool couldn't be.</li>\n<li><strong>Search is a component, not the point.</strong> A serious context tool needs good retrieval, sure. But \"search the blog\" and \"keep a team's context fresh and shareable\" only overlap at the word <em>search</em>. The first is a commodity; the second is the actual problem. I spent <a href=\"https://parkerjones.dev/posts/surrealdb-wasm-experiment/\">a whole post in another series</a> untangling the same search-versus-state conflation, and I don't want to repeat it here.</li>\n</ul>\n<h2 id=\"the-browser-rag-tangent-still-half-baked\">The browser-RAG tangent (still half-baked)</h2>\n<p>There's a tempting offshoot I haven't resolved: <code>ctx export</code> already dumps the index to JSON, and you could pair that with on-device embeddings (<a rel=\"external\" href=\"https://huggingface.co/docs/transformers.js/index\">Transformers.js</a> with the same small <code>all-MiniLM-L6-v2</code> model the engine can embed with locally) and a vector index (<a rel=\"external\" href=\"https://github.com/asg017/sqlite-vec\"><code>sqlite-vec</code></a> runs in WASM) to do retrieval entirely in a browser tab. For a forty-post blog it's a 25MB model aimed at a mosquito. For the multi-repo workspace the engine was built for, maybe not. I file it under \"experiments I want to run,\" not \"roadmap.\"</p>\n<h2 id=\"what-i-m-actually-doing-about-it\">What I'm actually doing about it</h2>\n<p>Dogfooding, mostly — which is the only honest way to find out if the bet holds. I run Context Harness against my own vaults and my own multi-repo work, and I watch for the moment it either earns its keep or doesn't: does a freshly-synced, shareable, skill-bearing index actually beat \"just keep good markdown,\" once the corpus is too big and too shared for any one person to keep good? I don't have that answer yet. Some days the format camp looks obviously right and the engine looks like a hammer I built because hammers are fun. Other days I hit exactly the staleness-at-scale wall the engine is meant for, and it feels like the only thing that could work.</p>\n<p>The meta-loop is the honest ending, and I'll leave it unresolved on purpose: I built a tool to fight AI slop by grounding models in real context, I'm using a model to reason about whether that tool is even the right shape, and the answer is genuinely <em>I'm still finding out</em>. Naming that uncertainty — instead of papering over it with a confident conclusion — is the same discipline the tool is supposed to enforce on the models. So that's where Context Harness is: a hammer still looking for its nail, being swung in public until I know whether the nail is real.</p>\n<p><em>— Parker Jones</em></p>\n",
      summary: "<p><a href=\"https://parkerjones.dev/posts/context-harness-rag-rust/\">Part 1</a> was the engine: a local-first RAG index in Rust — SQLite, hybrid search, Lua connectors, served to any tool over MCP. This post is the harder question one layer up, the one the engine only makes sense as an answer to: <strong>context that stays current and shareable across a team</strong> — and whether <a href=\"https://parkerjones.dev/posts/context-harness-rag-rust/\">Context Harness</a> is really the shape of that answer, or a hammer I enjoyed building and then went looking for a nail to justify.</p>",
      date: "2026-06-29T00:00:00Z",
      reading_time: 7,
      metadata: {"cover":"images/context-harness-site-search-cover.png","series_order":2},
      tags: ["rust","rag","ai","embeddings","local-first","mcp","search","wasm"],
      categories: ["software"],
      series: ["context-harness"],
      projects: []
  };



  
  
  
  

  
    
  
  
    
  
  
    
  
  

  CREATE post CONTENT {
      title: "Databases in the Browser",
      slug: "databses-in-the-browser",
      path: "https://parkerjones.dev/posts/databses-in-the-browser/",
      content: "<p><a href=\"/posts/static-can-be-dynamic/\">Part 1</a> laid out the heresy: a static site can be genuinely <em>dynamic</em> if you stop reaching for a JavaScript framework and let a real database — running in the reader's browser, with no server at all — do the work.</p>\n<p>This post is the field guide for that idea: the actual landscape of databases you can run in a browser tab, before I show you the experiment where I tried to live by it.</p>\n<span id=\"continue-reading\"></span>\n<p>I went down this road for a concrete reason — this blog runs a real database client-side, in WebAssembly, to power its tag and category pages (that's the next post). But before I write up <em>my</em> experiment, I want to do the thing I wish more \"look what I built\" posts did: survey the actual field first. What's genuinely possible in mid-2026, what's mature versus what's a demo, and what the tradeoffs really are. The crux of this blog is learning as I go, and you can't tell whether your own hack was clever or foolish until you know what everyone else has already figured out.</p>\n<p>A second confession is baked into that one. I started this series a while back, drafting with OpenAI's models — o1, GPT-4 — and then let it stall. I'm picking it back up with Claude Opus 4.8 as a writing partner, because the <em>other</em> experiment running through this blog is inserting AI into my own workflow: figuring out what it's good for, where it leads me astray, and how to find my own voice by arguing with a model until the words are actually mine. So I'll be transparent about it as I go. When the AI helped me build something sharp, I'll say so; when it helped me cheerfully overcomplicate things — like the database you'll meet in the next post — I'll say that too. Sometimes the experiment doesn't make sense, and naming that is the whole point.</p>\n<p>So: a field guide. Then, in the next post, I'll show you the experiment this blog actually runs — and the question underneath it that turned out to be far more interesting than search.</p>\n<h2 id=\"why-put-a-database-in-the-browser-at-all\">Why put a database in the browser at all?</h2>\n<p>The reflexive answer from a backend engineer is \"you wouldn't.\" Databases belong on servers; the browser gets JSON over an API. But there's a real thesis underneath the trend, and it's worth taking seriously:</p>\n<ul>\n<li><strong>No backend.</strong> For a static site, a CLI's docs, a side project — anything where you don't want to run and pay for a server — a database that lives entirely in the client is <em>operationally free</em>. No connection pool, no migrations at 2am, no bill.</li>\n<li><strong>Instant.</strong> Queries that never leave the machine have no network latency. The \"no spinners\" feeling is real.</li>\n<li><strong>Offline.</strong> If the data is already local, the app works on a plane.</li>\n<li><strong>Private.</strong> Data that never leaves the device can't leak from a server that was never stood up.</li>\n</ul>\n<p>That list isn't mine — it's most of the argument for <strong>local-first software</strong>, which is the intellectual backdrop for this whole space.</p>\n<h2 id=\"the-local-first-movement\">The local-first movement</h2>\n<p>The phrase comes from a 2019 Ink &amp; Switch essay, <a rel=\"external\" href=\"https://www.inkandswitch.com/essay/local-first/\">\"Local-first software: you own your data, in spite of the cloud\"</a> (Kleppmann, Wiggins, van Hardenberg, McGranaghan). It lays out seven ideals worth knowing, because almost everything in this post is chasing some subset of them:</p>\n<ol>\n<li><strong>No spinners</strong> — the UI responds at local speed.</li>\n<li><strong>Your work is not trapped on one device</strong> — it syncs.</li>\n<li><strong>The network is optional</strong> — offline is a first-class state, not an error.</li>\n<li><strong>Seamless collaboration</strong> — multiple people, no lock-the-file pain.</li>\n<li><strong>The Long Now</strong> — your data outlives the vendor.</li>\n<li><strong>Security and privacy by default</strong> — local storage, ideally end-to-end encrypted.</li>\n<li><strong>You retain ultimate ownership and control</strong> — it's <em>your</em> data, on <em>your</em> disk.</li>\n</ol>\n<p>The hard part of that list is #4 — collaboration without a central server arbitrating writes. The answer the field landed on is <strong>CRDTs</strong> (conflict-free replicated data types), data structures that merge concurrent edits deterministically. The two libraries you'll actually encounter are <a rel=\"external\" href=\"https://yjs.dev/\">Yjs</a> (the de-facto standard behind a lot of collaborative editors) and <a rel=\"external\" href=\"https://automerge.org/\">Automerge</a> (JSON-shaped documents with full change history).</p>\n<p>CRDTs solve merging; you still need to <em>move</em> the bytes and <em>store</em> them. That's the layer where a small industry has appeared:</p>\n<table><thead><tr><th>Project</th><th>What it is</th><th>Sync model</th></tr></thead><tbody>\n<tr><td><a rel=\"external\" href=\"https://electric-sql.com/\">ElectricSQL</a></td><td>Sync layer for Postgres → client</td><td>Streams Postgres changes to local replicas</td></tr>\n<tr><td><a rel=\"external\" href=\"https://rxdb.info/\">RxDB</a></td><td>Local-first NoSQL DB for JS</td><td>Offline-first; pluggable replication</td></tr>\n<tr><td><a rel=\"external\" href=\"https://pouchdb.com/\">PouchDB</a></td><td>JS database that speaks CouchDB</td><td>Multi-master HTTP replication</td></tr>\n<tr><td><a rel=\"external\" href=\"https://www.evolu.dev/\">Evolu</a></td><td>Local-first on SQLite-WASM</td><td>End-to-end encrypted, CRDT merge</td></tr>\n<tr><td><a rel=\"external\" href=\"https://www.triplit.dev/\">Triplit</a></td><td>Full-stack sync database</td><td>Real-time WebSocket sync, offline mode</td></tr>\n<tr><td><a rel=\"external\" href=\"https://jazz.tools/\">Jazz</a></td><td>Local-first app framework on CRDTs</td><td>Real-time sync, row-level permissions</td></tr>\n</tbody></table>\n<p>I'm not endorsing any of these for a blog — most are aimed at collaborative apps far heavier than a personal site. They're here because they're the context. When someone says \"database in the browser,\" they might mean any of three very different things: a <em>sync framework</em> (the table above), an <em>embedded database engine</em> compiled to WASM (next section), or a <em>purpose-built search index</em> (the section after that). Conflating them is how people end up shipping 30 megabytes to render a search box.</p>\n<h2 id=\"the-contenders-real-databases-compiled-to-wasm\">The contenders: real databases compiled to WASM</h2>\n<p>This is the part that still feels slightly illicit. You can take a database written in C or Rust, compile it to WebAssembly, and run the actual engine — query planner and all — inside a browser tab. Four are worth knowing.</p>\n<h3 id=\"sqlite-wasm\">SQLite-WASM</h3>\n<p>The one to start with. The SQLite project ships an official WASM build (<a rel=\"external\" href=\"https://sqlite.org/wasm/doc/trunk/persistence.md\"><code>@sqlite.org/sqlite-wasm</code></a>), and there are older community efforts — <code>sql.js</code> (in-memory, around since 2014), <code>wa-sqlite</code>, and the now-retired <code>absurd-sql</code>.</p>\n<p>The whole game with SQLite-in-the-browser is <strong>persistence</strong>, because a tab has no filesystem. Your options, roughly in order of modernity:</p>\n<ul>\n<li><strong>In-memory</strong> — fast, simple, gone on refresh. Fine for read-only data you reload anyway.</li>\n<li><strong>IndexedDB-backed</strong> — the engine serializes pages into IndexedDB. Works everywhere; degrades on large databases.</li>\n<li><strong>OPFS (Origin Private File System)</strong> — a real, fast, origin-scoped filesystem the browser gives you. This is the modern answer, and SQLite ships multiple OPFS VFS variants. The catch: the standard one needs <code>SharedArrayBuffer</code>, which means serving your site with <a rel=\"external\" href=\"https://web.dev/articles/coop-coep\">COOP/COEP headers</a> — a genuine deployment tax. The <code>opfs-sahpool</code> VFS sidesteps the header requirement at the cost of single-connection access, which makes it the pragmatic choice when you can't set those headers — though the standard <code>opfs</code> VFS is the actual default if you can.</li>\n</ul>\n<p>Realities to budget for: the WASM binary is several megabytes, Safari has historically been the buggy one, and OPFS isn't available in private-browsing mode. None of that is fatal — it's just the stuff nobody mentions until you hit it.</p>\n<h3 id=\"duckdb-wasm\">DuckDB-WASM</h3>\n<p><a rel=\"external\" href=\"https://duckdb.org/library/duckdb-wasm/\">DuckDB-WASM</a> takes DuckDB — the in-process analytics engine — and runs it in a browser tab. The trick that makes it special is <strong>HTTP range requests</strong> — point it at a Parquet file on a CDN and it reads only the columns and row-groups your query touches, instead of downloading the whole file. It's how tools like <a rel=\"external\" href=\"https://observablehq.com/framework/lib/duckdb\">Observable</a> do interactive analytics over remote datasets with no backend.</p>\n<p>It's production-grade and genuinely impressive. The constraint to remember: it's <strong>analytical and ephemeral</strong>. There's no persistence story like SQLite's — you load data in, you query it, it's gone on refresh. That's the right shape for dashboards and data exploration, the wrong shape for \"remember this user's notes.\"</p>\n<h3 id=\"pglite\">PGlite</h3>\n<p><a rel=\"external\" href=\"https://pglite.dev/\">PGlite</a> is the audacious one: <em>real Postgres</em>, compiled to WASM, in about 3MB gzipped — no Linux VM, no container, just Postgres-in-process. It persists to IndexedDB in the browser, supports live/reactive queries, and — the part relevant to this series — runs <a rel=\"external\" href=\"https://github.com/electric-sql/pglite\"><code>pgvector</code></a>, so you can do <strong>vector similarity search</strong> entirely client-side. It's built by the ElectricSQL folks, so it pairs naturally with their sync layer.</p>\n<p>The honest status: it's still pre-1.0 / alpha. The capability is startling; I wouldn't bet a production app on it yet. For experiments and offline-first prototypes, it's a remarkable thing to have.</p>\n<h3 id=\"surrealdb-wasm\">SurrealDB-WASM</h3>\n<p>This is the one I actually used here, so I'll be precise about its reality. SurrealDB ships <a rel=\"external\" href=\"https://surrealdb.com/docs/sdk/javascript/engines/wasm\">a WASM build of its engine</a>, and as of early 2026 the separate <code>surrealdb.wasm</code> repo was folded into the main JavaScript SDK. The thing to understand is which <strong>storage engines</strong> survive the trip to WASM:</p>\n<ul>\n<li><code>mem://</code> — in-memory. <strong>Works.</strong> (This is what this blog uses.)</li>\n<li><code>indxdb://</code> — IndexedDB-backed persistence. <strong>Works.</strong></li>\n<li><code>surrealkv://</code> — the embedded Rust key-value store. <strong>Does not work in WASM</strong> — it wants a real filesystem (WAL, value logs) the browser can't give it.</li>\n</ul>\n<p>So in a browser you get an in-memory or IndexedDB-backed SurrealDB speaking full SurrealQL. Whether that's a <em>good idea</em> for a static blog is a question I'll cheerfully tear into in the next post.</p>\n<h2 id=\"a-quick-map-of-maturity\">A quick map of maturity</h2>\n<p>Because \"you can run X in the browser\" and \"you should ship X to readers\" are very different claims:</p>\n<table><thead><tr><th>Engine</th><th>Browser persistence</th><th>Maturity (mid-2026)</th><th>Good for</th></tr></thead><tbody>\n<tr><td>SQLite-WASM</td><td>OPFS / IndexedDB / memory</td><td><strong>Mature</strong></td><td>General embedded storage, offline apps</td></tr>\n<tr><td>DuckDB-WASM</td><td>None (ephemeral)</td><td><strong>Mature</strong></td><td>Analytics over remote Parquet, dashboards</td></tr>\n<tr><td>PGlite</td><td>IndexedDB / memory</td><td><strong>Alpha</strong></td><td>Postgres features client-side, pgvector demos</td></tr>\n<tr><td>SurrealDB-WASM</td><td>IndexedDB / memory</td><td><strong>Mixed</strong> (mem/idb ok)</td><td>SurrealQL without a server; experiments</td></tr>\n</tbody></table>\n<h2 id=\"the-plot-twist-for-a-blog-do-you-even-want-a-database\">The plot twist: for a blog, do you even want a database?</h2>\n<p>Here's the uncomfortable thing I learned by doing this the hard way. If your actual goal is <strong>search on a static site</strong>, a general-purpose database is almost certainly the wrong tool. There's a whole category of software built for exactly this, and it's better at it:</p>\n<ul>\n<li><a rel=\"external\" href=\"https://pagefind.app/\"><strong>Pagefind</strong></a> — builds a <em>chunked</em> static index at build time and downloads only the fragments a query needs. It'll search ten thousand pages while shipping tens of kilobytes over the wire. Zero-config; it crawls your built HTML. For a blog, this is the boring correct answer.</li>\n<li><a rel=\"external\" href=\"https://github.com/oramasearch/orama\"><strong>Orama</strong></a> — full-text <em>and</em> vector search, typo tolerance, the works — but it loads the whole index up front, so it's heavier on bandwidth.</li>\n<li><a rel=\"external\" href=\"https://github.com/nextapps-de/flexsearch\"><strong>FlexSearch</strong></a> / <a rel=\"external\" href=\"https://github.com/lucaong/minisearch\"><strong>MiniSearch</strong></a> — fast in-browser indexing libraries; MiniSearch in particular is a tidy middle ground with fuzzy matching and a small footprint.</li>\n<li><a rel=\"external\" href=\"https://stork-search.net/\"><strong>Stork</strong></a> and <a rel=\"external\" href=\"https://github.com/tinysearch/tinysearch\"><strong>tinysearch</strong></a> — Rust→WASM indexes. Stork is effectively in maintenance-end now; tinysearch produces a sub-100KB payload using an xor-filter, with whole-word-only matching.</li>\n</ul>\n<p>The distinction that matters: <strong>prebuilt index</strong> (Pagefind, Lunr, tinysearch — work happens at build time, the client just looks things up) versus <strong>in-browser indexing</strong> (Orama, FlexSearch, MiniSearch — the client builds the index on load). For a blog, prebuilt wins, because the content only changes when <em>you</em> rebuild the site.</p>\n<p>I'm planting a flag here because it sets up the real question. For <em>search</em>, a database in the browser is the wrong tool — Pagefind wins, and I'll happily concede the point. Which leaves something more interesting hanging in the air: if not search, what <em>is</em> a real database in the browser actually good for? That's where the next post goes.</p>\n<h2 id=\"the-interesting-frontier-semantic-search-client-side\">The interesting frontier: semantic search, client-side</h2>\n<p>There's one place where the database-in-the-browser idea stops being over-engineering and starts being genuinely new: <strong>semantic search with no backend</strong>.</p>\n<p>The pattern is now well-trodden enough to describe crisply:</p>\n<ol>\n<li><strong>At build time</strong>, embed every chunk of your content into vectors using an on-device model. <a rel=\"external\" href=\"https://huggingface.co/docs/transformers.js/index\">Transformers.js</a> (Hugging Face's in-browser ONNX runtime) runs models like <code>all-MiniLM-L6-v2</code> to produce 384-dimensional embeddings. Write them to a static JSON file.</li>\n<li><strong>At runtime</strong>, load the same small model in the browser, embed the <em>query</em>, and compute cosine similarity against the prebuilt vectors. No API key, no server, nothing leaves the tab.</li>\n</ol>\n<p>The vectors can live in plain JSON, or in something fancier: <a rel=\"external\" href=\"https://github.com/asg017/sqlite-vec\"><code>sqlite-vec</code></a> (a tiny vector-search extension that runs anywhere SQLite does, WASM included) or PGlite's <code>pgvector</code>. <a rel=\"external\" href=\"https://supabase.com/blog/in-browser-semantic-search-pglite\">Supabase's in-browser semantic search demo</a> wires PGlite + pgvector + Transformers.js into exactly this stack, and <a rel=\"external\" href=\"https://github.com/do-me/SemanticFinder\">SemanticFinder</a> is a slick frontend-only example you can play with.</p>\n<p>The cost to be honest about: the embedding model is a ~25MB download — the <code>model_quantized.onnx</code> Transformers.js pulls by default; the full-precision <code>model.onnx</code> is ~90MB — cached after first load, but it dwarfs a Pagefind index. So semantic search in the browser is worth it when <em>keyword</em> search genuinely fails you — synonym-heavy domains, \"how do I fix this\" questions that don't share words with the answer — and overkill for a fifty-post blog where Ctrl-F energy is plenty.</p>\n<p>It's a genuinely new capability and a tempting one — but for a blog it's a 25MB answer to a question Pagefind solves in 50KB. I file it under \"things worth trying on a corpus where it would actually matter,\" not \"things this blog needs\" — a rabbit hole for its own future series, not this one.</p>\n<h2 id=\"where-this-series-goes-next\">Where this series goes next</h2>\n<p>So that's the lay of the land: a maturing set of real database engines you can run in a tab, a local-first philosophy giving them a reason to exist, and — for the specific job of blog search — a category of purpose-built tools that quietly outclass all of them.</p>\n<p>Which is exactly why the next post isn't about search. The thing that actually pulled me in — the reason I shipped a real database to your browser instead of a search index — was a different and more interesting idea: getting a genuinely <em>dynamic</em> application out of a <em>static</em> site — no SPA, no server, free hosting — by using a client-side database as the <em>engine of the frontend's application state</em>, in place of a heavy JavaScript state-management library. <a href=\"/posts/surrealdb-wasm-experiment/\">Part 3</a> is that experiment — SurrealDB compiled to WebAssembly, seeded from a SQL dump I smuggle through a <code>&lt;pre&gt;</code> tag, driving page state in the tab — and an honest accounting of what worked, what I never finished, and whether a database can credibly stand in for your state library.</p>\n<p>The fun of doing this in public is that I don't know the answer yet. That's the point.</p>\n<p><em>— Parker Jones, <a rel=\"external\" href=\"https://parkerjones.dev\">parkerjones.dev</a></em></p>\n",
      summary: "<p><a href=\"/posts/static-can-be-dynamic/\">Part 1</a> laid out the heresy: a static site can be genuinely <em>dynamic</em> if you stop reaching for a JavaScript framework and let a real database — running in the reader's browser, with no server at all — do the work.</p>\n<p>This post is the field guide for that idea: the actual landscape of databases you can run in a browser tab, before I show you the experiment where I tried to live by it.</p>",
      date: "2026-06-29T00:00:00Z",
      reading_time: 13,
      metadata: {"series_order":2},
      tags: ["wasm","sqlite","duckdb","surrealdb","local-first","browser","search"],
      categories: ["software"],
      series: ["databases"],
      projects: []
  };



  
  
  
  

  
    
  
  
    
  
  
    
  
  

  CREATE post CONTENT {
      title: "Static Can Be Dynamic: A Real Database in the Browser Instead of an SPA",
      slug: "static-can-be-dynamic",
      path: "https://parkerjones.dev/posts/static-can-be-dynamic/",
      content: "<p>Somewhere along the way we decided that \"dynamic website\" had to mean a few megabytes of JavaScript framework, a build pipeline with opinions, and a server to feed it — all to render a list that changes. This series is about a heresy: what if a <em>static</em> site could be genuinely dynamic, with a real database doing the work in the browser, and no SPA at all?</p>\n<span id=\"continue-reading\"></span>\n<p>I don't mean \"static site with a sprinkle of JavaScript.\" I mean the actual thing: a site that builds to plain files, deploys for free to a dumb host like GitHub Pages, and then — in the reader's tab — boots a real database, runs real queries, and drives real interactive state. No backend. No framework runtime. No <code>node_modules</code> the size of a small moon. Just files, and a database that happens to live in the browser.</p>\n<p>That sounds slightly unhinged if you've spent the last decade in the web's mainline, so let me explain how we got somewhere that makes it sound unhinged — and what quietly changed to make it reasonable again.</p>\n<h2 id=\"how-dynamic-got-so-expensive\">How \"dynamic\" got so expensive</h2>\n<p>The web started server-rendered. You asked for a URL, a server did some work, and you got HTML. Simple, cheap, a little clunky.</p>\n<p>Then we wanted apps, not pages — Gmail, Maps, the whole \"desktop software in a tab\" dream — and the single-page application was born. Render once, then never reload; mutate the DOM in place; keep all the state on the client. Good idea! It solved a real problem.</p>\n<p>But the SPA dragged a tail behind it, and the tail kept growing. If the client holds the state, you need a way to <em>manage</em> that state, so: a state library. If the client renders everything, you need a way to keep the UI in sync with the state, so: a framework with a reconciler. If you're shipping all that JavaScript, you need to bundle, split, tree-shake, and minify it, so: a build toolchain with its own gravitational field. And because the client can't be trusted with the real data, you still need a server and an API to talk to. We set out to escape the server and ended up with the server <em>plus</em> a second, heavier runtime bolted onto the browser.</p>\n<p>The result is a genuinely strange status quo: to put a sortable, filterable list of forty blog posts on a page, the default 2026 move is to reach for a framework, a state manager, a bundler, and probably a hosting platform with a billing relationship. That's a lot of apparatus for \"show me the posts tagged <code>rust</code>, newest first.\"</p>\n<p>None of this is wrong, exactly. For an actual application — collaborative, real-time, deeply interactive — that machinery earns its place. The problem is that we now reach for it <em>by reflex</em>, for things that aren't that, because \"dynamic\" and \"SPA\" fused into the same word in our heads.</p>\n<h2 id=\"what-quietly-changed\">What quietly changed</h2>\n<p>Two things shifted while we weren't looking, and together they reopen a door I thought was closed.</p>\n<p>The first is <strong>WebAssembly grew up</strong>. You can now take a database engine written in C or Rust — SQLite, DuckDB, Postgres, SurrealDB — compile it to WASM, and run the <em>actual engine</em>, query planner and all, inside a browser tab. Not a toy. Not a key-value shim pretending to be a database. The real thing, with a real query language, executing locally with no network round-trip.</p>\n<p>The second is that <strong>static hosting got incredible and free</strong>. GitHub Pages, Cloudflare Pages, Netlify's free tier — a CDN-backed home for your files at zero cost, with none of the operational weight of running a server. The catch was always that \"static\" meant \"inert\": you could serve files fast and free, but the moment you wanted dynamic behavior, you fell off the cliff into SPA-and-server land.</p>\n<p>Put those two together and the cliff has a bridge. A static host can serve any file — including a file that <em>is</em> a database, or the data to seed one. Boot that database in the browser, and the dynamic behavior you wanted lives entirely on the client, querying local data, with the static host none the wiser. You get the interactivity of an app and the deployment story of a brochure.</p>\n<p>The database becomes your state layer. Your \"API\" is a query you run against data that's already in the tab. The framework, the state library, the server — a surprising amount of the apparatus turns out to be optional once the data is local and queryable.</p>\n<h2 id=\"the-bet-of-this-series\">The bet of this series</h2>\n<p>So that's the thesis I want to chase, out loud, across a few posts: <strong>a static site generator plus a database in the browser can do a lot of what we currently throw an SPA at — for free, with no server, and far less machinery.</strong></p>\n<p>I'm not claiming it wins everywhere. I'm claiming the reflex is miscalibrated, and the only way to find out where the line actually is is to build on the idea and report back honestly. This blog itself is the first lab rat: it's a <a rel=\"external\" href=\"https://www.getzola.org/\">Zola</a> static site that ships a real database to your browser. You're standing in the experiment right now.</p>\n<p>Here's where the series goes:</p>\n<ul>\n<li><strong><a href=\"/posts/databses-in-the-browser/\">Part 2 — the field guide.</a></strong> What can you <em>actually</em> run in a browser tab in mid-2026? SQLite, DuckDB, Postgres, and SurrealDB all compile to WASM, and they are not equally sane choices. Plus the local-first movement that gives the whole idea a spine, and the persistence gotchas nobody warns you about.</li>\n<li><strong><a href=\"/posts/surrealdb-wasm-experiment/\">Part 3 — the experiment.</a></strong> What I actually built here: SurrealDB compiled to WebAssembly, seeded from a database dump I smuggle through the static site as a plain file, driving page state in the tab — and an honest accounting of what worked, what I faked, and what I never finished.</li>\n<li><strong>And then the real test</strong>: stop proving it on a blog and build an actual <em>application</em> this way — and, if the pattern holds, pull the reusable machinery out into a small library so it's repeatable instead of a clever one-off.</li>\n</ul>\n<p>A note on how this gets made, since it's a running thread here: I write these with an AI model as a partner, and I'm transparent about it — including the times it leads me confidently down a wrong path, which it does, and which I'll point out when it happens. Part of the fun of doing this in public is that I genuinely don't know how far the heresy holds. Static can be dynamic. Let's find out how dynamic.</p>\n<p><em>— Parker Jones, <a rel=\"external\" href=\"https://parkerjones.dev\">parkerjones.dev</a></em></p>\n",
      summary: "<p>Somewhere along the way we decided that \"dynamic website\" had to mean a few megabytes of JavaScript framework, a build pipeline with opinions, and a server to feed it — all to render a list that changes. This series is about a heresy: what if a <em>static</em> site could be genuinely dynamic, with a real database doing the work in the browser, and no SPA at all?</p>",
      date: "2026-06-29T00:00:00Z",
      reading_time: 6,
      metadata: {"series_order":1},
      tags: ["wasm","databases","static-site","zola","frontend","browser","local-first"],
      categories: ["software"],
      series: ["databases"],
      projects: []
  };



  
  
  
  

  
    
  
  
    
  
  
    
  
  

  CREATE post CONTENT {
      title: "A SurrealDB in Every Tab: Dynamic App State from a Static Site, No SPA",
      slug: "surrealdb-wasm-experiment",
      path: "https://parkerjones.dev/posts/surrealdb-wasm-experiment/",
      content: "<p><a href=\"/posts/databses-in-the-browser/\">Part 2</a> was the survey. This is the experiment — the one that made me care about this whole topic. And the first thing I have to do is correct a misconception I let myself carry for a while, including in an earlier draft of this very post: <strong>this was never really about search.</strong></p>\n<span id=\"continue-reading\"></span>\n<p>It's easy to look at a database running in a browser tab on a blog and conclude \"oh, search.\" But search was the most visible surface of the idea, not the idea. What I was actually probing is a question I still think is interesting: <strong>can a static site generator — Zola, output to plain files on a free GitHub Page — produce a genuinely <em>dynamic</em> application, with an in-browser database as its state engine instead of a JavaScript framework and an SPA?</strong></p>\n<p>Normally \"dynamic app\" means the whole SPA apparatus: React or one of its cousins, a state-management library, a build pipeline, and usually a server somewhere holding the data. The bet here is that you can skip all of it — ship static files, boot a real database in the tab, let queries drive the dynamic parts — and still deploy for free off a dumb static host. The blog is just the cheapest possible place to test that bet.</p>\n<p>This post is me, two years later, warming back up to that question and being honest about what I built, what worked, and what I never finished.</p>\n<p>A caveat on provenance, since transparency about the AI in the loop is a running thread on this blog: I built the first version of this during an o1-era sprint, leaned hard on the model, and wrote it up as if it were more finished than it was. I'm revisiting it now with Claude Opus 4.8 — and the revisiting is the point.</p>\n<h2 id=\"two-different-things-that-only-overlap-at-search\">Two different things that only overlap at \"search\"</h2>\n<p>Let me separate the two ideas cleanly, because conflating them is exactly the mistake I made:</p>\n<ol>\n<li><strong>Client-side page search.</strong> A blog genuinely needs this — type a word, find the post — and it should run in the browser with no backend. It's a real need, and it's a <em>commodity</em>: <a rel=\"external\" href=\"https://pagefind.app/\">Pagefind</a> solves it in 50KB. This is not what the SurrealDB experiment was for.</li>\n<li><strong>Dynamic application state, driven by a database in the tab.</strong> Render the parts of a page that depend on relationships and queries — tags, categories, related content, filtered views — by <em>asking a database</em>, client-side, instead of pre-rendering every permutation at build time or wiring up a state library to hold and mutate it all.</li>\n</ol>\n<p>The blog only ever exercised the boring corner of #2 (its tag and category pages). But #2 is the part with a future, and it has nothing to do with search.</p>\n<h2 id=\"why-a-frontend-database-instead-of-a-state-library\">Why a frontend database instead of a state library</h2>\n<p>The frontend state problem is usually solved with a library: Redux, Zustand, MobX, a pile of signals — a bespoke store, reducers, selectors, and a lot of glue translating between your data's natural shape and the shapes your components want.</p>\n<p>A database in the tab is a different proposition. You get <strong>one mental model</strong> — data plus a query language — instead of N hand-rolled stores. You get <strong>relationships for free</strong>: \"posts in this category, newest first, with their tags\" is a query, not a manually-maintained index. SurrealDB in particular is interesting here because it speaks document, relational, <em>and</em> graph in one engine, with <strong>live queries</strong> — a subscription that pushes you new results when the underlying data changes, which is exactly the reactivity a UI wants. The pitch, if it pans out: your app's state <em>is</em> a database, and your components are just views over queries.</p>\n<p>That's the experiment — and the reason it's interesting <em>on a static site</em> specifically: if the database is your state engine, then a Zola build plus a static host gives you a dynamic app with no SPA framework, no bundler-of-doom, and no server bill. Whether that actually beats reaching for React-plus-a-state-library is genuinely unresolved. But it's a better question than \"did I build search,\" and it's the one I want to come back to.</p>\n<h2 id=\"what-i-built-the-data-delivery-problem\">What I built: the data-delivery problem</h2>\n<p>The hard part of putting a database in the browser isn't the database — it's getting the <em>data</em> into the tab without dragging in a framework or a backend. Zola renders templates to HTML routes; it has no clean \"emit a raw <code>.sql</code> file\" mode. So I did the thing that worked: I made the dump a normal HTML route, <code>/site.sql</code>, with the entire SurrealQL payload inside a <code>&lt;pre&gt;</code> tag.</p>\n<p><code>templates/site.sql.html</code> (trimmed):</p>\n<pre><code data-lang=\"html\">{% block content %}\n&lt;pre&gt;\n-- SurrealDB SQL Export, generated at build time\n{% set section = get_section(path=&quot;posts/_index.md&quot;) %}\n{% for page in section.pages %}\n  CREATE post CONTENT {\n      title:        {{ page.title | json_encode() }},\n      path:         {{ page.permalink | json_encode() }},\n      content:      {{ page.content | json_encode() }},\n      date:         {{ page.date | date(format=&quot;%Y-%m-%dT%H:%M:%SZ&quot;) | json_encode() }},\n      reading_time: {{ page.reading_time | default(value=0) }},\n      tags:         {{ page.taxonomies.tags | json_encode() }},\n      categories:   {{ page.taxonomies.categories | json_encode() }}\n      -- ...series, projects, summary, metadata...\n  };\n{% endfor %}\n&lt;/pre&gt;\n{% endblock %}\n</code></pre>\n<p>So the whole site serializes to database inserts at build time and ships as a plain file from a static host. That's the crux of the whole thing: a static site generator has no concept of a \"database endpoint,\" but it can absolutely render <em>one more file</em> — so the database goes out as build output, free, off the same GitHub Page as everything else. It's a hack, but it's a hack that works <em>with</em> the grain of a static site generator instead of against it, and it's the actual answer to \"how do you get application state onto the client with no server and no framework.\" That question is the real crux — solve it and the dynamic-app-from-a-static-site idea has legs.</p>\n<h2 id=\"the-browser-half-boot-seed-and-a-reactive-seam\">The browser half: boot, seed, and a reactive seam</h2>\n<p><code>themes/consoler-dark/js/main.js</code> boots an in-memory SurrealDB, recovers the SQL from that <code>&lt;pre&gt;</code>, and seeds the database — then announces itself:</p>\n<pre><code data-lang=\"js\">import { Surreal } from &quot;surrealdb&quot;;\nimport { surrealdbWasmEngines } from &quot;@surrealdb/wasm&quot;;\n\nconst db = new Surreal({ engines: surrealdbWasmEngines() });\n\nconst html = await (await fetch(&#39;/site.sql&#39;)).text();\nconst sql  = new DOMParser().parseFromString(html, &#39;text/html&#39;)\n                            .querySelector(&#39;pre&#39;).textContent;\n\nawait db.connect(&quot;mem://&quot;);\nawait db.use({ namespace: &quot;test&quot;, database: &quot;test&quot; });\nawait db.query(sql);\n\nwindow.db = db;\nwindow.dispatchEvent(new CustomEvent(&#39;DatabaseReady&#39;, { detail: { db }, bubbles: true }));\n</code></pre>\n<p>That <code>DatabaseReady</code> event is the interesting seam, and it's the app-state idea in miniature: the rest of the page doesn't <em>import</em> the database, it <em>reacts</em> to it. Pages subscribe; when the data is ready, they query and render. Swap <code>db.query</code> for a SurrealDB <strong>live query</strong> and that same seam becomes genuine reactive state — the UI re-renders when the data changes, with no store and no reducer. I didn't get that far. But you can see the shape from here.</p>\n<h2 id=\"where-it-s-wired-in-today\">Where it's wired in today</h2>\n<p>Two pages — <code>/tags</code> and <code>/categories</code> — render entirely from client-side SurrealQL. The whole dynamic behavior of the tags page:</p>\n<pre><code data-lang=\"html\">&lt;div class=&quot;feed&quot; id=&quot;feed&quot;&gt;&lt;/div&gt;\n&lt;script&gt;\n  window.addEventListener(&#39;DatabaseReady&#39;, ({ detail: { db } }) =&gt; {\n    db.query(&#39;SELECT * FROM post WHERE tags != [] ORDER BY date DESC&#39;)\n      .then((res) =&gt; renderFeed(res[0], document.getElementById(&#39;feed&#39;)));\n  });\n&lt;/script&gt;\n</code></pre>\n<p><code>renderFeed</code> rebuilds the same <code>feed-entry</code> markup the static homepage renders server-side — identical down to the title, date, reading time, and tags, though it drops the excerpt line the server-rendered feed carries. On a blog this is a modest demo — a filtered, sorted list. But it's the same machinery you'd use to drive a genuinely dynamic app view, and <em>that's</em> the part worth taking seriously.</p>\n<h2 id=\"the-vite-gotchas-the-genuinely-transferable-bit\">The Vite gotchas (the genuinely transferable bit)</h2>\n<p>Packaging a WASM database for a static site is where the real engineering lived (<code>vite.config.js</code>):</p>\n<pre><code data-lang=\"js\">export default defineConfig({\n  build: { outDir: &#39;static&#39;, target: &#39;es2020&#39; },          // engine emits BigInt literals\n  optimizeDeps: { exclude: [&#39;@surrealdb/wasm&#39;] },          // or Vite mangles the .wasm binary\n  esbuild: { target: &#39;es2020&#39;, supported: { &#39;top-level-await&#39;: true } },\n});\n</code></pre>\n<p>Three things had to line up: an <code>es2020</code> target (the engine emits <code>BigInt</code> literals), top-level <code>await</code> (the WASM module initializes asynchronously at import), and — the non-obvious one — <strong>excluding <code>@surrealdb/wasm</code> from Vite's dependency pre-bundling</strong>, because the optimizer happily mangles the <code>.wasm</code> into something that won't instantiate. The dev workflow runs Zola and Vite in parallel so content and bundle rebuild together. None of that is in a quickstart; figuring it out is the part of this experiment I'd defend without hesitation.</p>\n<h2 id=\"what-i-planned-vs-what-shipped-honestly\">What I planned vs. what shipped — honestly</h2>\n<p>There's an <a href=\"/posts/zola-surreal-db-site-cache/\">earlier post</a> where I described a much grander version of this: a BM25 full-text index, a <code>page</code> table, a taxonomy graph, a live search box. Going back to it, most of that never shipped — there's no index, the real dump creates <code>post</code> records and a couple of vestigial tables, and the search box doesn't exist. That post documented a <em>plan</em> as if it were a <em>result</em>, which is a very specific failure mode of moving fast with an eager model: it will hand you a complete, confident, internally-consistent design for a system it has never run, and it's on me to notice which parts touched reality.</p>\n<p>I'm flagging that not to flog the old work but because it's the honest backdrop: the experiment was earlier-stage than I let on, and coming back to it means being clear about where the edges actually are.</p>\n<h2 id=\"the-real-limitations-which-are-findings-not-failures\">The real limitations (which are findings, not failures)</h2>\n<ul>\n<li><strong><code>mem://</code> means re-seed on every load.</strong> No persistence — the tab downloads the dump and rebuilds the database from scratch each visit. For these 33 posts that's a ~90 KB gzipped dump that re-seeds in about <strong>160 ms</strong> once the engine is warm — fine at this size; the obvious next step is an OPFS or IndexedDB engine so the state survives navigation, which is table stakes if this is ever going to be real app-state.</li>\n<li><strong>The reactivity isn't wired up.</strong> I'm running one-shot <code>query</code> calls, not live queries, so today it's a database I read once, not a reactive store. That's the single most interesting thing left undone.</li>\n<li><strong>Payload — the numbers I should have led with.</strong> Booting the database ships about <strong>3.6 MB gzipped</strong> on first load: a 3.5 MB <code>@surrealdb/wasm</code> engine (10.5 MB uncompressed), a 20 KB app bundle, and the 90 KB dump. <a rel=\"external\" href=\"https://pagefind.app/\">Pagefind</a> does blog search in roughly 50 KB. So I'm shipping on the order of <strong>70×</strong> the wire cost of the tool that would actually do the search job — which is the most honest possible argument that this was never about search. For an <em>application</em> where you'd ship a framework anyway, the trade looks different; for a blog, the number speaks for itself.</li>\n</ul>\n<h2 id=\"so-where-does-this-leave-things\">So where does this leave things?</h2>\n<p>Two clean takeaways, finally separated:</p>\n<ul>\n<li><strong>The blog needs page search</strong> — and it should be the commodity, client-side kind. That's a small, solved problem I'll just ship, and it was never what this experiment was about.</li>\n<li><strong>The frontend-database-as-app-state idea is worth continuing</strong> — for an actual application, with persistence and live queries doing the work a state library would otherwise do. That's a future post, and a more honest framing of what I was reaching for all along.</li>\n</ul>\n<p>That second takeaway is the one this series is going to chase. The honest version of this experiment isn't \"I added search to my blog\" — it's \"I proved to myself you can serve a working database off a static site, for free, and drive page state with it.\" The blog is the proof of concept. The next step is the actual point: <strong>build a real application this way</strong> — a static generator for the shell, a statically-served database for the state, OPFS persistence so it survives a refresh, and live queries doing the reactive work a store and a pile of reducers otherwise would. And if the pattern holds up, pull the boot-a-database-from-a-static-dump machinery out into a small <strong>library</strong> worth handing to other people — so this stops being a clever one-off and becomes something you can reach for.</p>\n<p>That's where this series goes next: from \"look, a database in a tab\" to \"here's a dynamic app with no SPA, on free static hosting — and here's the library that makes it repeatable.\" Whether \"your state is a database, served statically\" actually beats the JavaScript-heavy status quo is the only version of this question I find genuinely interesting — and the only way to answer it is to build the thing.</p>\n<p><em>— Parker Jones, <a rel=\"external\" href=\"https://parkerjones.dev\">parkerjones.dev</a></em></p>\n",
      summary: "<p><a href=\"/posts/databses-in-the-browser/\">Part 2</a> was the survey. This is the experiment — the one that made me care about this whole topic. And the first thing I have to do is correct a misconception I let myself carry for a while, including in an earlier draft of this very post: <strong>this was never really about search.</strong></p>",
      date: "2026-06-29T00:00:00Z",
      reading_time: 10,
      metadata: {"series_order":3},
      tags: ["wasm","surrealdb","zola","vite","javascript","local-first","browser"],
      categories: ["software"],
      series: ["databases"],
      projects: []
  };



  
  
  
  

  
    
  
  
    
  
  
  
    
  

  CREATE post CONTENT {
      title: "What Six Months of Baby Sleep Data Taught Me About Pandas (and Sleep)",
      slug: "baby-sleep-analysis",
      path: "https://parkerjones.dev/posts/baby-sleep-analysis/",
      content: "<p>New parents get a lot of advice about sleep. What they don't get is a baseline. Is <em>my</em> kid sleeping a normal amount? Is the longest stretch actually getting longer, or do I just feel like it does at 3am? Are we drifting toward an earlier bedtime or am I imagining the trend?</p>\n<p>I had one advantage: we'd been logging every nap and night in <a rel=\"external\" href=\"https://huckleberrycare.com/\">Huckleberry</a> since the first weeks. That export is a CSV, and a CSV is a dataset. So I did what any sleep-deprived engineer does and opened a notebook.</p>\n<p>This post is the write-up. The full analysis — code, charts, and all — is embedded at the bottom and lives at <a href=\"/baby_sleep_analysis.html\"><code>/baby_sleep_analysis.html</code></a>.</p>\n<h2 id=\"the-data\">The data</h2>\n<p>The Huckleberry export came out as <strong>2,776 rows across 8 columns</strong>:</p>\n<pre><code data-lang=\"text\">[&#39;Type&#39;, &#39;Start&#39;, &#39;End&#39;, &#39;Duration&#39;, &#39;Start Condition&#39;,\n &#39;Start Location&#39;, &#39;End Condition&#39;, &#39;Notes&#39;]\n</code></pre>\n<p><code>Type</code> mixes sleep with feedings, diapers, and everything else you tap a button for at 4am. Filtering to just sleep dropped it to <strong>667 sessions</strong> spanning August 2024 through January 2025 — roughly six months.</p>\n<pre><code data-lang=\"python\">df = pd.read_csv(path_to_data)\ndf = df[df[&#39;Type&#39;] == &#39;Sleep&#39;].copy()\n\ndf[&#39;Sleep Start&#39;] = pd.to_datetime(df[&#39;Start&#39;])\ndf[&#39;Sleep End&#39;]   = pd.to_datetime(df[&#39;End&#39;])\ndf[&#39;Sleep Duration&#39;] = (df[&#39;Sleep End&#39;] - df[&#39;Sleep Start&#39;]).dt.total_seconds() / 3600\n</code></pre>\n<p>Two derived columns did most of the analytical heavy lifting later: <code>Is Night Sleep</code> (a flag for evening/overnight sessions) and an age axis computed from date of birth, so I could plot against <code>Days Old</code> / <code>Weeks Old</code> instead of calendar dates. Plotting a baby against <em>its own age</em> rather than the wall calendar is the whole trick — it's what makes the trends legible.</p>\n<p>Tooling was the boring, correct stack: <code>pandas</code> and <code>numpy</code> for wrangling, <code>seaborn</code>/<code>matplotlib</code> for charts, <code>statsmodels</code> and <code>scipy</code> for the regressions.</p>\n<h2 id=\"question-1-is-the-longest-stretch-really-getting-longer\">Question 1: Is the longest stretch really getting longer?</h2>\n<p>This is the question every exhausted parent actually cares about. I grouped by month and pulled the distribution of sleep-session durations:</p>\n<pre><code data-lang=\"text\">Monthly Sleep Statistics (hours):\n         count  mean   std   min    max\n2024-08     15  1.71  0.52  0.35   2.42\n2024-09     23  1.96  1.35  0.25   4.97\n2024-10    200  2.07  1.45  0.18   8.13\n2024-11    182  2.40  2.17  0.15  10.50\n2024-12    198  2.19  2.12  0.08  10.85\n2025-01     49  1.88  1.71  0.28   6.23\n</code></pre>\n<p>Look at the <code>max</code> column, not the <code>mean</code>. The longest single stretch climbs from <strong>2.4 hours in August to 10.85 hours by December</strong> — a newborn who couldn't go two hours turning into a baby who occasionally sleeps nearly eleven. A linear regression on longest-stretch over time confirmed the trend was real and not just a couple of lucky nights.</p>\n<p>The <code>mean</code>, meanwhile, barely moves (1.7 → 2.4 → back to 1.9). That's the trap: averaging a night of 10 hours together with five 20-minute catnaps flattens exactly the signal you care about. <strong>The story was in the tail of the distribution, not its center</strong> — a lesson that generalizes well beyond babies.</p>\n<h2 id=\"question-2-when-does-bedtime-happen-and-is-it-getting-more-consistent\">Question 2: When does bedtime happen, and is it getting more consistent?</h2>\n<p>I filtered to evening sessions (onset after 6pm) and tracked the average start time, week over week:</p>\n<pre><code data-lang=\"text\">Weekly Sleep Onset (hour of day, 24h):\nWeeks Old   mean onset\n14          21.83   (~9:50pm)\n17          22.57   (~10:34pm)\n20          20.16   (~8:10pm)\n21          20.45   (~8:27pm)\n</code></pre>\n<p>Bedtime drifts from nearly 10pm down toward 8pm as the weeks pass — earlier and more civilized. But the more satisfying metric was <em>consistency</em>. I measured the standard deviation of onset time, in minutes:</p>\n<pre><code data-lang=\"text\">Weekly Onset Variability (minutes):\nWeeks Old   std\n15          52.81\n20          80.90\n24          21.74\n</code></pre>\n<p>Outside a noisy patch around week 20, the spread tightens from ~53 minutes down to ~22. The baby wasn't just going to bed earlier — bedtime was becoming <em>predictable</em>. If you've ever clung to the promise of a routine, there it is in the data.</p>\n<h2 id=\"question-3-are-we-normal\">Question 3: Are we normal?</h2>\n<p>I pulled the AAP / National Sleep Foundation guidelines into a small reference frame and compared month by month:</p>\n<pre><code data-lang=\"text\">Month 2:  Total Sleep:  4.8h  (Guideline 14-17h) - BELOW RANGE\nMonth 3:  Total Sleep: 12.0h  (Guideline 14-17h) - BELOW RANGE\nMonth 4:  Total Sleep: 14.0h  (Guideline 14-17h) - WITHIN RANGE\nMonth 5:  Total Sleep: 14.8h  (Guideline 14-17h) - WITHIN RANGE\n</code></pre>\n<p>At face value, months 2 and 3 look alarming — well <em>below</em> the recommended range. This is where being honest about your own data matters more than the chart looks. August had only <strong>15 logged sessions</strong> the entire month. We weren't logging a sleepless infant; we were a brand-new family that hadn't built the habit of tapping the button yet. \"4.8 hours of sleep in month 2\" isn't a finding about the baby — it's a finding about <em>us and the dataset</em>.</p>\n<p>By months 4 and 5, once logging was consistent, totals land squarely in range (14–15 hours/day) and nap counts converge toward the typical 3–4. Overall, across the whole window: <strong>3.79 ± 1.15 naps a day, 4.83 hours of naps, and 8.33 hours of night sleep.</strong></p>\n<p>The biggest analytical lesson of the whole project lives in that \"BELOW RANGE\" line: <strong>missing data and a real signal can look identical on a chart.</strong> Distinguishing the two is the actual job. A plot that doesn't account for collection coverage will confidently tell you something false.</p>\n<h2 id=\"the-pandas-patterns-that-did-the-work\">The pandas patterns that did the work</h2>\n<p>Strip away the domain and this was three idioms, over and over:</p>\n<ul>\n<li><strong><code>groupby</code> + <code>unstack</code></strong> to pivot long event logs into day-by-category matrices (day sleep vs. night sleep per date).</li>\n<li><strong>Datetime-derived features</strong> — <code>.dt.date</code>, <code>.dt.hour</code>, age-since-DOB — so I could regress and group against meaningful axes.</li>\n<li><strong>A boolean classification column</strong> (<code>Is Night Sleep</code>) computed once and reused everywhere, which kept every downstream <code>groupby</code> a one-liner.</li>\n</ul>\n<pre><code data-lang=\"python\">daily_sleep = (df.groupby([df[&#39;Sleep Start&#39;].dt.date, &#39;Is Night Sleep&#39;])\n                 [&#39;Sleep Duration&#39;].sum().unstack())\ndaily_sleep.columns = [&#39;Day Sleep&#39;, &#39;Night Sleep&#39;]\ndaily_sleep[&#39;Total Sleep&#39;] = daily_sleep[&#39;Day Sleep&#39;] + daily_sleep[&#39;Night Sleep&#39;]\n</code></pre>\n<h2 id=\"what-the-data-couldn-t-tell-me\">What the data couldn't tell me</h2>\n<p>Plenty. Quality isn't duration — ten logged hours with three wakeups isn't ten hours of rest, and the export doesn't capture that. Week-over-week change averaged a meaningless <strong>0.01 hours</strong>, with a +1.65h swing one week and a −0.94h drop the next; sleep is noisy and any single week is mostly weather. And <code>n=1</code> is <code>n=1</code> — this is one kid, not a study.</p>\n<p>But that was never the point. The point was to replace anxiety with a baseline, and a baseline is exactly what a CSV and an afternoon of pandas can buy you. The stretches really were getting longer. Bedtime really was settling. Some nights you just have to trust the regression line over how you feel at 3am.</p>\n<h2 id=\"the-full-analysis\">The full analysis</h2>\n<p>The complete notebook — every chart and every cell — is embedded below:</p>\n<div class=\"responsive-iframe\">\n  <iframe src=\"/baby_sleep_analysis.html\" title=\"Baby Sleep Analysis notebook\" loading=\"lazy\"></iframe>\n</div>\n<p>Or <a href=\"/baby_sleep_analysis.html\">open it full-screen</a>. The notebook and data-processing code are on GitHub: <a rel=\"external\" href=\"https://github.com/parallaxisjones/baby_sleep_analysis\"><code>parallaxisjones/baby_sleep_analysis</code></a>.</p>\n<p><em>— Parker Jones, <a rel=\"external\" href=\"https://parkerjones.dev\">parkerjones.dev</a></em></p>\n",
      summary: null,
      date: "2026-06-26T00:00:00Z",
      reading_time: 5,
      metadata: {},
      tags: ["python","pandas","data-analysis","data-viz","statsmodels","parenting"],
      categories: ["software"],
      series: [],
      projects: ["analysis"]
  };



  
  
  
  

  
    
  
  
    
  
  
  
    
  

  CREATE post CONTENT {
      title: "Consoler Dark: A Terminal-Themed Zola Blog with Neofetch Logos and In-Browser SurrealDB",
      slug: "consoler-dark-theme",
      path: "https://parkerjones.dev/posts/consoler-dark-theme/",
      content: "<p>I wanted my blog to feel like a terminal — not in the lazy \"monospace font and a green-on-black palette\" way, but structurally. The model I kept coming back to was <code>neofetch</code>: an ASCII distro logo on the left, a tidy key/value table of system info on the right. That layout <em>is</em> a homepage. So I built one.</p>\n<p>The result is <strong>Consoler Dark</strong>, the Zola theme this site runs on. This is how it works, including the two parts I'm fondest of: a script that generates logos by scraping neofetch itself, and a database that runs entirely in your browser.</p>\n<h2 id=\"starting-from-after-dark\">Starting from after-dark</h2>\n<p>I didn't start from scratch. Consoler Dark is a fork of <a rel=\"external\" href=\"https://git.habd.as/comfusion/after-dark/\">comfusion's after-dark</a>, a minimal Zola theme built on top of <a rel=\"external\" href=\"https://hackcss.egoist.dev/\">hack.css</a>. You can still see hack.css's fingerprints in the SCSS — the <code>.hack</code> utility classes, the no-nonsense defaults. After-dark gave me a clean skeleton; I layered the terminal identity on top.</p>\n<p>The taxonomy setup is where I diverged most. Beyond the usual <code>categories</code> and <code>tags</code>, the site indexes <code>series</code>, <code>projects</code>, and <code>authors</code> — that last one so I can credit the AI models I draft alongside as co-authors (this post included).</p>\n<h2 id=\"the-neofetch-metaphor-in-templates\">The neofetch metaphor, in templates</h2>\n<p>The homepage and section headers render a <code>.neofetch-header</code>: a logo block next to a <code>tech_info</code> table driven straight from config.</p>\n<pre><code data-lang=\"toml\">[extra.tech_info]\nTitle = &quot;parkerjones.dev&quot;\nFramework = &quot;Zola&quot;\nTheme = &quot;Consoler Dark&quot;\nHosting = &quot;Github Pages&quot;\n</code></pre>\n<p>The template loops over those key/value pairs the way neofetch prints <code>OS</code>, <code>Kernel</code>, <code>Uptime</code>. The nice property is that any page can override individual fields via <code>page.extra.tech_info</code>, so a project page can advertise its own \"system specs\" while inheriting the site defaults. It's config-as-content, and it keeps the homepage honest — the chrome describes the actual stack.</p>\n<p>A few smaller touches round out the feel: an <code>intro</code> keyframe fade so content boots in rather than flashing, a 16:9 <code>responsive-iframe</code> helper for embeds, and <code>:target</code> highlighting so deep links land with a visible cursor-style focus.</p>\n<pre><code data-lang=\"scss\">@keyframes intro { 0% { opacity: 0; } 100% { opacity: 1; } }\nmain, footer { animation: intro 0.3s both; animation-delay: 0.15s; }\n</code></pre>\n<h2 id=\"generating-logos-by-scraping-neofetch\">Generating logos by scraping neofetch</h2>\n<p>Here's the part I enjoyed most. Neofetch ships ASCII art for <em>hundreds</em> of distros, embedded right in its shell script as heredoc blocks. Rather than hand-draw anything, I wrote a Python script that fetches the raw neofetch source, parses out every logo, and converts each to an SVG.</p>\n<p>The parser keys off neofetch's own structure — each logo is a <code>read -rd '' ascii_data &lt;&lt;'EOF' … EOF</code> block, and the distro name is in the <code>case</code> line just above it:</p>\n<pre><code data-lang=\"python\">if &quot;read -rd &#39;&#39; ascii_data &lt;&lt;&#39;EOF&#39;&quot; in line:\n    # walk backwards to the case label, e.g.  &quot;Ubuntu&quot;|&quot;ubuntu&quot;)\n    j = i - 1\n    while j &gt;= 0 and not re.search(r&#39;\\)$&#39;, lines[j].strip()):\n        j -= 1\n    distro_name = re.findall(r&#39;&quot;([^&quot;]+)&quot;&#39;, lines[j])[0]\n\n    # collect everything up to the EOF sentinel\n    ascii_block = []\n    i += 1\n    while lines[i].strip() != &quot;EOF&quot;:\n        ascii_block.append(lines[i]); i += 1\n</code></pre>\n<p>Converting ASCII to SVG is just laying each line down as a <code>&lt;tspan&gt;</code> in a monospaced <code>&lt;text&gt;</code> element, sized from the longest line:</p>\n<pre><code data-lang=\"python\">char_width, line_height, font_size = 8, 18, 14\nwidth  = max(len(l) for l in lines) * char_width + 10\nheight = len(lines) * line_height + 10\n# one &lt;text&gt; with a &lt;tspan dy=&quot;line_height&quot;&gt; per line\n</code></pre>\n<p>Run it, and <code>static/logos/</code> fills up with a clean SVG per distro — vector, themeable, no image assets to hand-maintain. When neofetch adds a logo, I re-run the script. The art stays in sync with upstream because <em>upstream is the source of truth</em>. That's the kind of automation that pays for itself the first time you'd otherwise have copied ASCII art by hand.</p>\n<h2 id=\"a-database-that-runs-in-your-browser\">A database that runs in your browser</h2>\n<p>The detail that surprises people: Consoler Dark ships a WebAssembly build of <strong><a rel=\"external\" href=\"https://surrealdb.com/\">SurrealDB</a></strong> and runs it client-side. There's no backend. The theme's <code>js/main.js</code> boots an in-browser SurrealDB engine, fetches a <code>/site.sql</code> file, pulls the SQL out of a <code>&lt;pre&gt;</code> tag, and executes it — all in the visitor's tab.</p>\n<pre><code data-lang=\"js\">import { Surreal } from &quot;surrealdb&quot;;\nimport { surrealdbWasmEngines } from &quot;@surrealdb/wasm&quot;;\n\nconst db = new Surreal({ engines: surrealdbWasmEngines() });\nconst sql = await fetchSQLFromPreTag();   // read /site.sql from a &lt;pre&gt;\n</code></pre>\n<p>This is bundled with <a rel=\"external\" href=\"https://vitejs.dev/\">Vite</a>. A couple of non-obvious settings were required to make a wasm database happy in a static-site context: an <code>es2020</code> target plus <code>top-level-await</code> support (the engine initializes asynchronously at import time), and excluding <code>@surrealdb/wasm</code> from dependency pre-bundling so Vite doesn't mangle the wasm.</p>\n<pre><code data-lang=\"js\">build:    { target: &#39;es2020&#39;, outDir: &#39;static&#39; },\nesbuild:  { target: &#39;es2020&#39;, supported: { &#39;top-level-await&#39;: true } },\noptimizeDeps: { exclude: [&#39;@surrealdb/wasm&#39;] },\n</code></pre>\n<p>Why do this on a blog? Because I write about <a href=\"/posts/databses-in-the-browser/\">databases in the browser</a> and <a href=\"/posts/zola-surreal-db-site-cache/\">using SurrealDB as a site cache</a>, and a theme that <em>is</em> the demo beats one that merely links to it. The dev workflow runs Zola and Vite in parallel so the static site and the wasm bundle rebuild together:</p>\n<pre><code data-lang=\"json\">&quot;dev&quot;: &quot;npm-run-all --parallel vite-serve zola-serve&quot;\n</code></pre>\n<h2 id=\"the-linkify-experiment-i-rolled-back\">The linkify experiment I rolled back</h2>\n<p>Not everything survived. I'd wired up a client-side <a rel=\"external\" href=\"https://linkify.js.org/\">linkify</a> pass that auto-detected <code>#hashtags</code> in post text and rewrote them into links to the matching <code>/tags/</code> page — Twitter-style tagging, for free, with no changes to how I wrote posts.</p>\n<pre><code data-lang=\"js\">formatHref: (href, type) =&gt;\n  type === &#39;hashtag&#39; ? `/tags/${href.slice(1).toLowerCase()}` : href\n</code></pre>\n<p>It worked, and I turned it off. Scanning every <code>&lt;p&gt;</code> and <code>&lt;span&gt;</code> on load and rewriting <code>innerHTML</code> is a lot of DOM churn for a cosmetic feature, it fought with code blocks that legitimately contain <code>#</code>, and \"clever text rewriting at runtime\" is exactly the kind of thing that quietly breaks a year later. The code's still in the repo behind a partial; the lesson is that <em>shipping</em> and <em>keeping</em> are different decisions, and a static site rewards restraint.</p>\n<h2 id=\"takeaways-for-rolling-your-own-zola-theme\">Takeaways for rolling your own Zola theme</h2>\n<ul>\n<li><strong>Fork something minimal.</strong> after-dark + hack.css gave me a correct baseline so I could spend my effort on identity, not resets.</li>\n<li><strong>Drive chrome from config.</strong> The <code>tech_info</code> table means the design describes the real stack and updates itself.</li>\n<li><strong>Automate your assets from their upstream source.</strong> The neofetch scraper means I never hand-edit ASCII art.</li>\n<li><strong>A static site can do more than you think.</strong> A wasm database in the client turned my theme into a live demo of the things I write about.</li>\n<li><strong>Be willing to delete features that work.</strong> Linkify was the most \"impressive\" thing I built and the right call was to switch it off.</li>\n</ul>\n<p>The theme is MIT-licensed and built on <a rel=\"external\" href=\"https://github.com/getzola/after-dark\">after-dark</a>. If you want a blog that boots like a terminal and ships a database to every reader, the pieces — the logo scraper, the wasm SurrealDB bootstrap, the Vite config — are all laid out above.</p>\n<p><em>— Parker Jones, <a rel=\"external\" href=\"https://parkerjones.dev\">parkerjones.dev</a></em></p>\n",
      summary: null,
      date: "2026-06-26T00:00:00Z",
      reading_time: 5,
      metadata: {},
      tags: ["zola","sass","wasm","vite","surrealdb","theming","neofetch"],
      categories: ["web"],
      series: [],
      projects: ["sites"]
  };



  
  
  
  

  
    
  
  
    
  
  
    
  
  
    
  

  CREATE post CONTENT {
      title: "Context Harness: a Local-First RAG Engine in Rust with Lua Extensions and an MCP Server",
      slug: "context-harness-rag-rust",
      path: "https://parkerjones.dev/posts/context-harness-rag-rust/",
      content: "<p>AI tools are only useful when they can see <em>your</em> context — your docs, your code, your notes, the Hacker News thread you read last week. The usual answer is a cloud RAG service: ship your data to someone's vector database, pay per query, hope it's still up. I wanted the opposite — a single binary that ingests my stuff, indexes it locally, and hands it to any AI tool over a standard protocol, with no cloud dependency. So I built <strong><a rel=\"external\" href=\"https://github.com/parallax-labs/context-harness\">Context Harness</a></strong> (<code>ctx</code>).</p>\n<pre><code data-lang=\"text\">$ ctx --help\nContext Harness provides a connector-driven pipeline for ingesting documents\nfrom multiple sources (filesystem, Git repositories, S3 buckets), chunking and\nembedding them, and exposing hybrid search (keyword + semantic) via a CLI and\nMCP-compatible HTTP server.\n</code></pre>\n<p>It's a RAG engine that lives on your laptop. Here's the design.</p>\n<h2 id=\"the-pipeline\">The pipeline</h2>\n<p>The flow is the standard RAG shape, but every stage is local and configurable through one <code>ctx.toml</code>:</p>\n<pre><code data-lang=\"text\">connectors → sync → chunk → embed → SQLite → hybrid search → CLI / MCP\n</code></pre>\n<p><strong>Connectors</strong> define where documents come from — filesystem globs, Git repos, S3 buckets, or custom Lua scripts (more on those below). <code>ctx sync</code> pulls from a connector and runs the rest of the pipeline.</p>\n<p><strong>Chunking and embedding</strong> are configured, not hard-coded:</p>\n<pre><code data-lang=\"toml\">[chunking]\nmax_tokens = 700\noverlap_tokens = 80\n\n[embedding]\nprovider = &quot;openai&quot;\nmodel = &quot;text-embedding-3-small&quot;\ndims = 1536\n</code></pre>\n<p>The <code>provider</code> is pluggable — that line can point at OpenAI's <code>text-embedding-3-small</code>, or at a <strong>fully local</strong> model. My test setup runs <code>fastembed</code> with a quantized <code>all-MiniLM-L6-v2</code> ONNX model, so embeddings happen on-device with no API call at all. That choice is the whole \"local-first\" thesis in one config key: trade a little retrieval quality for zero cloud dependency and zero per-query cost.</p>\n<p><strong>Storage is SQLite.</strong> No vector-database service to stand up, no Docker, no daemon. The index is a file you can copy, back up, or delete. For a single-user knowledge base, a managed vector DB is wildly over-provisioned; SQLite is exactly right.</p>\n<h2 id=\"hybrid-search\">Hybrid search</h2>\n<p>Pure semantic search misses exact terms; pure keyword search misses paraphrase. <code>ctx</code> does both and blends them:</p>\n<pre><code data-lang=\"toml\">[retrieval]\nfinal_limit = 12\nhybrid_alpha = 0.6           # weight toward semantic vs keyword\ncandidate_k_keyword = 80     # pull 80 keyword candidates\ncandidate_k_vector  = 80     # and 80 vector candidates\ngroup_by = &quot;document&quot;        # then dedup/group by document\ndoc_agg = &quot;max&quot;\nmax_chunks_per_doc = 3\n</code></pre>\n<p>It gathers candidates from both a keyword index and a vector index, blends the scores with <code>hybrid_alpha</code>, groups by document so one long file can't flood the results, and returns the top 12. Tuning <code>hybrid_alpha</code> toward 0 or 1 lets you dial between \"find the exact phrase\" and \"find the related idea.\"</p>\n<h2 id=\"the-part-that-makes-it-an-engine-the-mcp-server\">The part that makes it an <em>engine</em>: the MCP server</h2>\n<p>A search CLI is handy. A search CLI that any AI tool can call as a tool is a force multiplier:</p>\n<pre><code data-lang=\"text\">$ ctx serve mcp\n# Exposes search/get over a JSON API for Cursor, Claude, and other\n# MCP-compatible AI tools.\n</code></pre>\n<p><a rel=\"external\" href=\"https://modelcontextprotocol.io/\">MCP</a> is the protocol AI tools use to call external tools. By speaking it, <code>ctx</code> turns your local knowledge base into a tool the model can reach for mid-conversation: \"search my notes for the retry-policy decision,\" and the model queries your SQLite index and gets grounded results — without your notes ever leaving the machine. That's the feature I use every day.</p>\n<h2 id=\"extensibility-connectors-tools-and-agents-in-lua\">Extensibility: connectors, tools, and agents in Lua</h2>\n<p>Built-in connectors cover the common cases, but the interesting data is always somewhere weird. So <code>ctx</code> embeds Lua: you can script <strong>connectors</strong> (new data sources), <strong>tools</strong> (new capabilities), and <strong>agents</strong> (personas with a system prompt and a scoped toolset). Each has <code>init</code>/<code>test</code> scaffolding:</p>\n<pre><code data-lang=\"text\">$ ctx connector init    # scaffold a new Lua connector from a template\n$ ctx connector test    # run it without writing to the DB\n$ ctx agent  init/test/list\n</code></pre>\n<p>The agent system is the one I'm proudest of. An agent is a Lua script that, at resolve time, <em>assembles its own context</em> by querying the knowledge base, then hands the model a system prompt plus pre-loaded research and a scoped set of tools. Here's a real one — <code>hn-writer</code>, which writes Hacker News launch posts:</p>\n<pre><code data-lang=\"lua\">agent = {\n    name = &quot;hn-writer&quot;,\n    description = &quot;Write Hacker News posts by studying top HN content and your product docs&quot;,\n    tools = { &quot;search&quot;, &quot;get&quot; },          -- scoped: this agent can only search and fetch\n    arguments = {\n        { name = &quot;style&quot;, description = &quot;show_hn, launch, ask_hn, or discussion&quot; },\n        { name = &quot;angle&quot;, description = &quot;e.g. &#39;local-first&#39;, &#39;developer tooling&#39;&quot; },\n        { name = &quot;tone&quot;,  description = &quot;technical, conversational, or minimal&quot; },\n    },\n}\n\nfunction agent.resolve(args, config, context)\n    -- pre-load HN trends from the knowledge base\n    for _, q in ipairs({ &quot;Show HN&quot;, &quot;Rust CLI tool&quot;, &quot;local first&quot;, &quot;AI context&quot; }) do\n        local results = context.search(q, { mode = &quot;keyword&quot;, limit = 5 })\n        -- ...fold the top results into the prompt as research...\n    end\n    -- ...also search the project&#39;s own docs, then return:\n    return { system = system_prompt, tools = { &quot;search&quot;, &quot;get&quot; }, messages = preloaded }\nend\n</code></pre>\n<p>What I love about this pattern: the agent does its own retrieval <em>before</em> the model gets involved, so the model starts with both \"what performs well on HN right now\" (from a connector that ingests HN) and \"what this product actually does\" (from a filesystem connector over the docs) already in context. And there's a pleasant recursion to it — I have an agent whose job is to write the Show HN post for the tool the agent runs on. Its prompt even encodes the house style: <em>\"What HN hates: marketing speak, buzzwords, superlatives… technical substance over marketing language.\"</em> Which, not coincidentally, is the ethos of this whole blog.</p>\n<h2 id=\"sharing-extensions-registries\">Sharing extensions: registries</h2>\n<p>Lua scripts are shareable, so <code>ctx</code> supports <strong>registries</strong> — git repos of community connectors, tools, and agents that sync in automatically:</p>\n<pre><code data-lang=\"toml\">[registries.community]\nurl = &quot;https://github.com/parallax-labs/ctx-registry.git&quot;\nauto_update = true\nreadonly = true\n</code></pre>\n<p>A registry is just a versioned directory of <code>.lua</code> files; pointing at one makes its connectors and agents available locally. It's the same \"distribute capability declaratively\" idea I use for <a href=\"/posts/skills-with-nix/\">agent skills</a>, applied to data connectors.</p>\n<h2 id=\"static-site-search-for-free\">Static-site search, for free</h2>\n<p>One more trick. <code>ctx export</code> dumps the whole index to JSON for client-side search:</p>\n<pre><code data-lang=\"text\">$ ctx export\n# Exports documents and chunks to JSON for use with ctx-search.js —\n# client-side search on a static site.\n</code></pre>\n<p>Which means the same engine that grounds my AI tools could also power search on <em>this</em> blog — index the posts, export the JSON, search it in the browser with no backend. (My <a href=\"/posts/consoler-dark-theme/\">terminal theme already runs a database in the browser</a>, so this is a natural next step.)</p>\n<h2 id=\"honest-trade-offs\">Honest trade-offs</h2>\n<p>Context Harness is young, and local-first is a set of trade-offs, not a free lunch:</p>\n<ul>\n<li><strong>Local embeddings are private and free but lower-quality</strong> than the big cloud models. <code>provider</code> lets you choose per use case, but you don't get both at once.</li>\n<li><strong>SQLite scales to a personal knowledge base, not a team's corpus.</strong> That's the design target, not a bug — but know the ceiling.</li>\n<li><strong>Lua is power and rope.</strong> Scriptable connectors mean I can ingest anything; they also mean a bad script can do bad things. <code>connector test</code> (which never writes to the DB) exists for exactly that reason.</li>\n</ul>\n<p>But the core bet has paid off: a single Rust binary, a SQLite file, optional fully-local embeddings, and a standard protocol is enough to give every AI tool I use grounded access to my own context — without renting a vector database to do it.</p>\n<p>Context Harness is open source (AGPL-3.0) at <a rel=\"external\" href=\"https://github.com/parallax-labs/context-harness\"><code>parallax-labs/context-harness</code></a>; docs and prebuilt binaries for macOS and Linux are at <a rel=\"external\" href=\"https://parallax-labs.github.io/context-harness/\">parallax-labs.github.io/context-harness</a>.</p>\n<p><em>— Parker Jones, <a rel=\"external\" href=\"https://parkerjones.dev\">parkerjones.dev</a></em></p>\n",
      summary: null,
      date: "2026-06-26T00:00:00Z",
      reading_time: 5,
      metadata: {"demo":"https://parallax-labs.github.io/context-harness/","featured":true,"project_name":"Context Harness","repo":"https://github.com/parallax-labs/context-harness","series_order":1},
      tags: ["rust","rag","ai","mcp","lua","embeddings","sqlite","local-first"],
      categories: ["software"],
      series: ["context-harness"],
      projects: ["agency"]
  };



  
  
  
  

  
    
  
  
    
  
  
  
    
  

  CREATE post CONTENT {
      title: "Hexagonal Chess in Rust: Bevy, WASM, and Serverless WebRTC Multiplayer",
      slug: "hex-chess-webrtc",
      path: "https://parkerjones.dev/posts/hex-chess-webrtc/",
      content: "<p>Chess assumes a square board. But there's a whole family of <em>hexagonal</em> chess variants — Gliński's, McCooey's, Shafran's, and others — played on a board of hexagons where every piece needs new movement rules and your spatial intuition is useless. I built a Rust implementation of eight of them that runs entirely in the browser via WASM, with multiplayer that needs almost no server at all. This post is about the three architecture decisions that made it tractable.</p>\n<p><strong><a rel=\"external\" href=\"https://parallaxisjones.github.io/hex-chess/\">▶ Play it in your browser</a></strong>  ·  <strong><a rel=\"external\" href=\"https://github.com/parallaxisjones/hex-chess\">source on GitHub</a></strong></p>\n<h2 id=\"decision-1-separate-the-game-from-the-engine-from-the-network\">Decision 1: separate the game from the engine from the network</h2>\n<p>The repo is three crates, and the split is the most important design choice in the project:</p>\n<pre><code data-lang=\"text\">hex-chess/\n├── crates/\n│   ├── core/        # pure game logic — no rendering, no networking\n│   ├── game/        # Bevy WASM app — rendering, input, the actual game\n│   └── signaling/   # minimal WebRTC signaling server\n├── web/             # static web assets\n└── deploy/nixos/    # production deployment\n</code></pre>\n<p><strong><code>core</code> has no dependencies on Bevy, on WASM, or on the network.</strong> It's pure rules: given a board state and a variant, what are the legal moves? That isolation buys two things. First, the rules are testable without a GPU or a browser — <code>cargo test</code> on <code>core</code> exercises move generation directly, which matters enormously when \"is this move legal on a Shafran board?\" has eight different correct answers depending on the variant. Second, the rules are reusable: the same <code>core</code> that the Bevy app calls could back a bot, a server-side validator, or a puzzle generator later, with zero rendering code dragged along.</p>\n<p><code>game</code> is the Bevy app — it owns rendering, input, and animation, and it asks <code>core</code> what's legal. <code>signaling</code> is its own tiny server. Keeping these three apart means a change to how pieces are drawn can't possibly break the rules, and a change to the netcode can't touch either.</p>\n<h2 id=\"decision-2-hex-coordinates-not-row-column\">Decision 2: hex coordinates, not (row, column)</h2>\n<p>The thing that breaks your brain about hex chess is that a square-grid mental model doesn't transfer. On a square board a piece has 8 neighbors and you index by <code>(row, col)</code>. On a hex board a cell has 6 neighbors and <code>(row, col)</code> produces ugly special-casing everywhere — diagonals aren't diagonals, and \"forward\" depends on which of three axes you mean.</p>\n<p>The standard fix (and the right one) is to stop thinking in two axes and use <strong>cube or axial coordinates</strong> — three coordinates <code>(q, r, s)</code> with the constraint <code>q + r + s = 0</code>. Movement directions become clean vector additions, distance is a single formula, and the six variants' wildly different movement rules all reduce to \"which direction vectors does this piece get, and how far.\" Putting that coordinate system in <code>core</code> is what keeps the eight variants from turning into eight piles of special cases. If you take one idea from this project, it's that the coordinate system <em>is</em> the architecture for a hex game — pick it before you write a single movement rule.</p>\n<h2 id=\"decision-3-peer-to-peer-so-i-barely-run-a-server\">Decision 3: peer-to-peer, so I barely run a server</h2>\n<p>Most multiplayer games need a server in the hot path relaying every move. I didn't want to run (or pay for) that, so hex-chess uses <strong>WebRTC for true peer-to-peer play</strong>. The only server is the <code>signaling</code> crate, and it does almost nothing:</p>\n<ul>\n<li>Two players want to connect. WebRTC needs them to exchange connection details (SDP offers/answers and ICE candidates) before they can talk directly.</li>\n<li>The signaling server is just the broker for that initial handshake. It introduces the peers.</li>\n<li><strong>Once the peers connect, game traffic flows directly between the two browsers.</strong> The server is out of the loop entirely — it never sees a move.</li>\n</ul>\n<p>So the \"server infrastructure\" for an arbitrary number of concurrent games is one tiny process whose only job is matchmaking introductions. It's idle the moment a game starts. That's a dramatically cheaper operational story than a stateful game server, and for a turn-based game like chess the direct peer connection is more than fast enough.</p>\n<h2 id=\"shipping-it-wasm-nix-and-ci\">Shipping it: WASM, Nix, and CI</h2>\n<p>The <code>game</code> crate compiles to WebAssembly with <a rel=\"external\" href=\"https://trunkrs.dev/\">Trunk</a>, so the whole thing runs in the browser at near-native speed:</p>\n<pre><code data-lang=\"bash\">nix develop                      # reproducible toolchain\ncd crates/game &amp;&amp; trunk build --release\n</code></pre>\n<p>The entire dev environment and production deploy are defined in Nix — <code>nix develop</code> drops you into a shell with the exact Rust toolchain, <code>wasm</code> target, and Trunk version, and <code>nix build .#game .#signaling</code> produces both artifacts:</p>\n<pre><code data-lang=\"bash\">nix build .#game .#signaling     # build both packages\n</code></pre>\n<p>Deployment lives in <code>deploy/nixos/</code>, and GitHub Actions handles CI. This is the same declarative approach I use across <a href=\"/posts/nix-fleet-one-flake/\">my whole fleet</a> — the game and its signaling server are just two more Nix packages, built and deployed the same way as everything else I run. No \"works on my machine\" between my laptop and the box that serves the game.</p>\n<h2 id=\"what-i-d-tell-someone-starting-a-hex-game\">What I'd tell someone starting a hex game</h2>\n<p>The hard parts of this project weren't the parts I expected. Rendering hexagons is easy; Bevy handles it. The hard parts were <strong>getting the coordinate system right before writing rules</strong> (retrofitting axial coordinates onto a square-grid assumption would have been a rewrite) and <strong>resisting the urge to put a real game server in the middle</strong> (the P2P handshake is more setup up front but far less to operate forever after).</p>\n<p>Pure-logic core, hex-native coordinates, peer-to-peer networking with a near-stateless broker, WASM for reach, Nix for reproducibility. Five decisions, eight playable variants, and a multiplayer game I can run for the cost of a process that sits idle most of the time.</p>\n<p><em>— Parker Jones, <a rel=\"external\" href=\"https://parkerjones.dev\">parkerjones.dev</a></em></p>\n",
      summary: null,
      date: "2026-06-26T00:00:00Z",
      reading_time: 5,
      metadata: {"demo":"https://parallaxisjones.github.io/hex-chess/","featured":true,"project_name":"Hex Chess","repo":"https://github.com/parallaxisjones/hex-chess"},
      tags: ["rust","bevy","wasm","webrtc","games","p2p","nix"],
      categories: ["software"],
      series: [],
      projects: ["games"]
  };



  
  
  
  

  
    
  
  
    
  
  
    
  
  
    
  

  CREATE post CONTENT {
      title: "Nix Fleet, Part 3: Fleet-Wide Secrets with agenix",
      slug: "nix-fleet-agenix-secrets",
      path: "https://parkerjones.dev/posts/nix-fleet-agenix-secrets/",
      content: "<blockquote>\n<p><strong>Nix Fleet, part 3.</strong> <a href=\"/posts/nix-fleet-one-flake/\">Part 1</a> covered the architecture; <a href=\"/posts/nix-fleet-remote-builders/\">Part 2</a> made builds painless. This part handles the thing you must <em>not</em> commit to a public config repo: secrets.</p>\n</blockquote>\n<p>My whole system is declared in a Git repository, and most of it is public. But a fleet needs secrets — API keys, SMB credentials, a WireGuard config — and those obviously can't sit in plaintext next to the rest of the config. The constraint that shapes everything here: I want secrets to be <strong>versioned, shared across every machine, and reproducible</strong>, with the same Git-reviewable discipline as the rest of the system — without any secret ever appearing in plaintext, in the repo, or in the Nix store.</p>\n<p><a rel=\"external\" href=\"https://github.com/ryantm/agenix\">agenix</a> gets me there. Here's the setup.</p>\n<h2 id=\"two-repos-one-boundary\">Two repos, one boundary</h2>\n<p>The cleanest decision I made was to put secrets in their <strong>own separate repo</strong> (<code>nix-secrets</code>) and pull it into the system flake as an input. I keep my real one private, but I've published a <a rel=\"external\" href=\"https://github.com/parallaxisjones/nix-secrets\">public example repo</a> so you can see the shape:</p>\n<pre><code data-lang=\"nix\"># flake.nix\ninputs.secrets = {\n  url = &quot;git+ssh://git@github.com/parallaxisjones/nix-secrets.git&quot;;\n  flake = false;\n};\n</code></pre>\n<p>That boundary pays off operationally: I can lock or revoke access to the secrets repo independently of the (public) config, rotate recipients without touching application code, and grant a machine read-only deploy access to just the secrets it needs. The public config can stay public because the sensitive bits live behind a separate door.</p>\n<h2 id=\"how-agenix-encrypts\">How agenix encrypts</h2>\n<p>agenix is <code>age</code> encryption wired into Nix. You declare, per secret, <em>which identities are allowed to decrypt it</em>. That declaration lives in <code>secrets.nix</code> in the private repo:</p>\n<pre><code data-lang=\"nix\">let\n  # macOS (pjones) and NixOS (parallaxis) decrypt with the\n  # same ~/.ssh/parallaxis identity.\n  pjones = &quot;ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...oqt7Aq&quot;;\n  users  = [ pjones ];\nin\n{\n  &quot;openai-key.age&quot;.publicKeys        = users;\n  &quot;anthropic-api-key.age&quot;.publicKeys = users;\n  &quot;datadog-api-key.age&quot;.publicKeys   = users;\n  &quot;datadog-app-key.age&quot;.publicKeys   = users;\n  &quot;smb-credentials.age&quot;.publicKeys   = users;\n  &quot;protonvpn-wireguard.age&quot;.publicKeys = users;\n}\n</code></pre>\n<p>Each <code>.age</code> file is encrypted to the public keys listed. The matching private identity — and <em>only</em> that identity — can decrypt it. The encrypted files are safe to commit (to the private repo); the identity never is.</p>\n<h2 id=\"one-identity-for-the-whole-fleet\">One identity for the whole fleet</h2>\n<p>Notice that <code>users</code> is a single key. That's deliberate: <strong>both my macOS and NixOS machines decrypt with the same <code>~/.ssh/parallaxis</code> ed25519 identity.</strong> agenix can use an <code>age</code>-native key (<code>age-keygen</code>) or reuse an existing SSH ed25519 key via <code>age-plugin-ssh</code>; I reuse the SSH key so there's exactly one secret-decrypting identity to manage across the fleet.</p>\n<p>The trade-off is real and worth stating: one identity is one thing to protect and one thing to rotate, but it's also a single point of compromise. For a personal fleet that's the right call — fewer keys means I actually keep the rotation story straight instead of letting five per-host keys drift. If this were a team, I'd encrypt each secret to multiple recipients (one per person/host) so revoking one doesn't force re-keying everyone. agenix supports exactly that; it's just a longer <code>publicKeys</code> list.</p>\n<h2 id=\"how-decryption-works-at-runtime\">How decryption works at runtime</h2>\n<p>This is the part that makes agenix better than \"stash a <code>.env</code> somewhere.\" At activation time, NixOS/nix-darwin decrypts each declared secret to a path under <code>/run</code> (tmpfs), owned by the user/service that needs it, with the permissions you specify. The decrypted value:</p>\n<ul>\n<li><strong>never lands in the Nix store</strong> (which is world-readable and gets copied to your binary cache),</li>\n<li><strong>never sits in the repo</strong> in plaintext,</li>\n<li>exists only on the machine authorized to decrypt it, only while the system is running.</li>\n</ul>\n<p>So a service reads its API key from a file at <code>/run/agenix/...</code>, that file only exists because <em>this</em> machine holds the identity that can decrypt it, and the policy for who-can-read-what is reviewable in <code>secrets.nix</code>. Secrets management becomes declarative and auditable, like the rest of the config.</p>\n<h2 id=\"bootstrapping-a-brand-new-machine\">Bootstrapping a brand-new machine</h2>\n<p>The chicken-and-egg problem: a fresh machine needs secrets to be useful, but doesn't yet have the identity to decrypt them. My flake exposes helper apps for exactly this lifecycle:</p>\n<pre><code data-lang=\"text\">nix run .#create-keys          # generate keys on a new machine\nnix run .#copy-keys            # place an identity where agenix expects it\nnix run .#check-keys           # verify the identity can decrypt\nnix run .#install-with-secrets # first install WITH secrets provisioned\n</code></pre>\n<p><code>install-with-secrets</code> is the interesting one. On a first NixOS install it runs <a href=\"/posts/nix-fleet-helios64-disko/\">disko</a> to partition, stages the repo to the new root, and installs via the flake — and crucially, it relies on <strong>SSH agent forwarding</strong> so the installer can fetch the private <code>secrets</code> input without ever copying a key onto the installer media. Nothing persistent is left behind on the installer once it's done. The machine comes up on first boot already matching repo state, with its secrets provisioned where they're declared.</p>\n<h2 id=\"why-this-beats-the-alternatives\">Why this beats the alternatives</h2>\n<ul>\n<li><strong>vs. plaintext <code>.env</code> / committed config:</strong> secrets are encrypted at rest, never in the repo, never in the store.</li>\n<li><strong>vs. a cloud secrets manager:</strong> no external dependency, no per-machine bootstrapping against a third-party API, and the policy is in Git where I review everything else.</li>\n<li><strong>vs. copying keys around by hand:</strong> the recipient list is declarative; adding or removing who-can-decrypt is a reviewable diff, not a tribal-knowledge ritual.</li>\n</ul>\n<p>The throughline of this whole series is \"manage it declaratively, in Git, reproducibly.\" Secrets are the case where people usually give up and do something ad-hoc. agenix is how I kept them inside the same discipline as everything else. Next, in <a href=\"/posts/nix-fleet-helios64-disko/\">Part 4</a>, I put all of this to work standing up an ARM NAS from bare metal — disko for the disks, agenix for the backup credentials, and <code>nixos-anywhere</code> to tie it together.</p>\n<p><em>A public example secrets repo is at <a rel=\"external\" href=\"https://github.com/parallaxisjones/nix-secrets\"><code>parallaxisjones/nix-secrets</code></a>; the fleet config that consumes it is <a rel=\"external\" href=\"https://github.com/parallaxisjones/dotfiles\"><code>parallaxisjones/dotfiles</code></a>.</em></p>\n<p><em>— Parker Jones, <a rel=\"external\" href=\"https://parkerjones.dev\">parkerjones.dev</a></em></p>\n",
      summary: null,
      date: "2026-06-26T00:00:00Z",
      reading_time: 5,
      metadata: {"series_order":3},
      tags: ["nix","nixos","agenix","age","secrets","security","home-manager"],
      categories: ["homelab"],
      series: ["nix-fleet"],
      projects: ["homelab"]
  };



  
  
  
  

  
    
  
  
    
  
  
    
  
  
    
  

  CREATE post CONTENT {
      title: "Nix Fleet, Part 4: Installing a Helios64 NAS from Bare Metal with nixos-anywhere and disko",
      slug: "nix-fleet-helios64-disko",
      path: "https://parkerjones.dev/posts/nix-fleet-helios64-disko/",
      content: "<blockquote>\n<p><strong>Nix Fleet, part 4.</strong> The series so far: <a href=\"/posts/nix-fleet-one-flake/\">the one-flake architecture</a>, <a href=\"/posts/nix-fleet-remote-builders/\">remote builders</a>, and <a href=\"/posts/nix-fleet-agenix-secrets/\">fleet-wide secrets</a>. This part is where it all gets pointed at bare metal — a 5-bay ARM NAS, installed declaratively and reproducibly.</p>\n</blockquote>\n<p>The <a rel=\"external\" href=\"https://wiki.kobol.io/helios64/intro/\">Kobol Helios64</a> is a 5-bay NAS built on a Rockchip RK3399 (<code>aarch64</code>, 4–8 GB RAM). It's a lovely, slightly cantankerous little ARM board, and it's the storage backbone of my homelab. The goal for this host was the same as every other machine in the fleet: <strong>I should be able to reinstall it from the flake and get an identical result, disks and all.</strong> No clicking through an installer, no hand-partitioning, no \"what did I configure last time.\"</p>\n<p>That's what <code>disko</code> and <code>nixos-anywhere</code> make possible.</p>\n<h2 id=\"the-two-tools\">The two tools</h2>\n<ul>\n<li><strong><a rel=\"external\" href=\"https://github.com/nix-community/disko\">disko</a></strong> turns disk layout into declarative Nix. Instead of running <code>fdisk</code> and <code>mkfs</code> by hand, you <em>describe</em> the partitions, filesystems, and pools you want, and disko makes the disks match. Partitioning becomes config you can review, version, and re-apply.</li>\n<li><strong><a rel=\"external\" href=\"https://github.com/numtide/nixos-anywhere\">nixos-anywhere</a></strong> installs a NixOS flake onto a remote machine over SSH — it can kexec into an installer, run disko to lay out the disks, and install the full system, all from my workstation.</li>\n</ul>\n<p>Together they collapse \"install a NAS\" into one command:</p>\n<pre><code data-lang=\"bash\">nix run github:numtide/nixos-anywhere -- --flake .#helios64 &lt;ssh-host&gt;\n</code></pre>\n<p>That partitions the drives per the disko spec, installs the <code>helios64</code> host config from my flake, and reboots into a running NixOS. The disk layout is no longer a one-time manual act I'd have to remember — it's part of the host definition.</p>\n<h2 id=\"the-arm-specific-bits\">The ARM-specific bits</h2>\n<p>An ARM SBC NAS has a few wrinkles a generic x86 box doesn't, and the host config names them explicitly:</p>\n<pre><code data-lang=\"nix\"># hosts/nixos/helios64.nix\n{\n  networking.hostName = &quot;helios64&quot;;\n\n  # The RK3399 needs its device tree to boot correctly\n  hardware.deviceTree.enable = true;\n  hardware.deviceTree.name = &quot;rockchip/rk3399-kobol-helios64.dtb&quot;;\n\n  services.openssh.enable = true;\n  system.stateVersion = &quot;24.11&quot;;\n}\n</code></pre>\n<p>The <strong>device tree</strong> is the load-bearing line — without <code>rk3399-kobol-helios64.dtb</code>, the board doesn't come up right. The boot chain itself is U-Boot/Tow-Boot on SD/eMMC with the NixOS root on SATA. And because this is <code>aarch64-linux</code>, the system image builds on my x86 box via the emulation I set up in <a href=\"/posts/nix-fleet-remote-builders/\">Part 2</a> — I never need a native ARM build host just for the NAS.</p>\n<blockquote>\n<p>A note on honesty: the committed host config is intentionally minimal right now (it even evaluates as a container in CI to bypass boot/filesystem asserts). Storage and services are phased work. So treat this post as the <em>design and install method</em> I'm executing against, not a claim that every service below is already live. The series documents the build as it happens.</p>\n</blockquote>\n<h2 id=\"storage-zfs-and-why\">Storage: ZFS, and why</h2>\n<p>The storage decision got its own <a rel=\"external\" href=\"https://adr.github.io/\">ADR</a> (I keep architecture decision records in the repo — <code>docs/adr/0001-choose-zfs-for-helios64.md</code>, accepted 2025-08-27). The short version:</p>\n<p><strong>Decision: ZFS.</strong> End-to-end checksums, snapshots, <code>send</code>/<code>receive</code>, and self-healing are worth a lot for data I actually care about, and NixOS's ZFS integration is mature. The cost is memory pressure on a low-RAM ARM board and tighter kernel/module version coupling. The alternative considered — <code>mdraid</code> + <code>ext4</code>/<code>btrfs</code> — is simpler and lighter but has weaker integrity guarantees. For a NAS, integrity wins.</p>\n<p>The pool config reflects the board's constraints:</p>\n<pre><code data-lang=\"nix\">boot.supportedFilesystems = [ &quot;zfs&quot; ];\nservices.zfs = {\n  autoScrub.enable = true;\n  trim.enable = true;\n};\n# On a 4 GB board, cap the ARC so ZFS doesn&#39;t starve everything else:\n# boot.kernelParams = [ &quot;zfs.zfs_arc_max=1073741824&quot; ]; # 1 GiB\n</code></pre>\n<p>Sensible defaults for the pool (<code>tank</code>): <code>ashift=12</code>, <code>compression=zstd</code>, <code>autotrim=on</code>, <code>atime=off</code> for bulk data, and a topology chosen by disk count — mirror for 2 disks, RAIDZ1 for 3–5, RAIDZ2 when I want to survive two failures. Datasets split by purpose: <code>tank/data/shares</code> for SMB, <code>tank/data/media</code> for streaming, <code>tank/backups</code> for restic targets (with credentials supplied by <a href=\"/posts/nix-fleet-agenix-secrets/\">agenix</a> — that's the payoff of Part 3).</p>\n<h2 id=\"the-gotchas-this-board-taught-me\">The gotchas this board taught me</h2>\n<p>Real hardware has opinions, and the runbook records them so I don't relearn them at 2am:</p>\n<ul>\n<li><strong>The 2.5GbE port can be flaky</strong> on some Helios64 units. The mitigation is unglamorous: prefer the 1GbE port, lock the link speed at the switch, and capture <code>dmesg</code> for 24h before trusting it. There's an ADR for that decision too (<code>0002-networking-1g-vs-2_5g.md</code>).</li>\n<li><strong>ZFS ARC will eat a 4 GB board alive</strong> if you let it. Cap <code>zfs_arc_max</code> to 1–2 GiB and re-evaluate after watching real workload for a week.</li>\n<li><strong>RAIDZ1 + power events is a risk.</strong> A NAS without tested backups isn't a backup. The drive-replacement playbook (<code>zpool offline</code> → swap → <code>zpool replace</code> → watch the resilver) lives in the design doc, and restore tests are scheduled quarterly.</li>\n</ul>\n<h2 id=\"why-the-ceremony-is-the-point\">Why the ceremony is the point</h2>\n<p>It would be faster, <em>once</em>, to flash an SD card and click through a setup. But \"once\" is the trap. The value of the disko + nixos-anywhere + flake approach is the <strong>second</strong> time — when a disk dies, or I want to rebuild on fresh drives, or I'm reproducing the NAS to test an upgrade. Then the disk layout, the ZFS topology, the device tree, the services, and the secrets are all <em>declared</em>, and the rebuild is a command, not an archaeology project.</p>\n<p>That's the whole thesis of this series, taken to its hardest case. <a href=\"/posts/nix-fleet-one-flake/\">Part 1</a> claimed one flake could configure every machine — laptop, server, and NAS — reproducibly. The NAS is the machine where reproducibility is least convenient and most valuable, and it's the one that proves the model. Same flake, same discipline, all the way down to the spinning rust.</p>\n<p><em>The host config and ADRs referenced here live in <a rel=\"external\" href=\"https://github.com/parallaxisjones/dotfiles\"><code>parallaxisjones/dotfiles</code></a>.</em></p>\n<p><em>— Parker Jones, <a rel=\"external\" href=\"https://parkerjones.dev\">parkerjones.dev</a></em></p>\n",
      summary: null,
      date: "2026-06-26T00:00:00Z",
      reading_time: 5,
      metadata: {"series_order":4},
      tags: ["nix","nixos","disko","nixos-anywhere","zfs","helios64","arm","nas","homelab"],
      categories: ["homelab"],
      series: ["nix-fleet"],
      projects: ["homelab"]
  };



  
  
  
  

  
    
  
  
    
  
  
    
  
  
    
  

  CREATE post CONTENT {
      title: "One Flake, Every Machine: a NixOS + macOS Fleet",
      slug: "nix-fleet-one-flake",
      path: "https://parkerjones.dev/posts/nix-fleet-one-flake/",
      content: "<blockquote>\n<p><strong>Nix Fleet, part 1.</strong> This post is the architecture overview. Later parts go deep on the pieces it depends on: cross-arch remote builders, fleet-wide secrets with agenix, and bare-metal install of a NAS with disko.</p>\n</blockquote>\n<p>Every machine I work on — Linux servers, a NAS, and my M3 MacBook — is configured by <a rel=\"external\" href=\"https://github.com/parallaxisjones/dotfiles\">one Git repository</a>. One <code>flake.nix</code> describes all of them: same shell, same tools, same agent skills, same secrets handling, whether the target is <code>x86_64-linux</code>, <code>aarch64-linux</code>, or <code>aarch64-darwin</code>. Adding a machine means adding a host entry; changing my setup everywhere means one commit and a rebuild.</p>\n<p>This is the \"single source of truth for my computing environment\" that Nix promises and rarely delivers without a fight. Here's how the repo is laid out and the one trick that makes cross-architecture management actually pleasant.</p>\n<h2 id=\"the-shape-of-the-repo\">The shape of the repo</h2>\n<pre><code data-lang=\"text\">nixos-config/\n├── flake.nix          # inputs + outputs: hosts, devShells, apps\n├── hosts/\n│   ├── nixos/         # configuration.nix, hardware-configuration.nix,\n│   │                  # helios64.nix (the NAS), profiles/\n│   └── darwin/        # configuration.nix (the Mac)\n├── modules/\n│   ├── shared/        # config + secrets shared across OSes\n│   ├── nixos/         # homelab services, Linux-only config\n│   └── darwin/        # dock, homebrew casks, home-manager\n├── overlays/          # package customizations\n└── justfile           # deploy / check convenience targets\n</code></pre>\n<p>The split that makes this maintainable: <strong><code>hosts/</code> is per-machine, <code>modules/</code> is shared.</strong> A host file is mostly a list of which modules to import plus its hardware specifics. The actual configuration — my packages, dotfiles, services — lives in modules that any host can pull in. When I want a tool on every machine, it goes in <code>modules/shared</code>; when it's Linux-only, <code>modules/nixos</code>; macOS-only, <code>modules/darwin</code>.</p>\n<h2 id=\"what-s-wired-together\">What's wired together</h2>\n<p>The flake stitches together the whole modern Nix ecosystem:</p>\n<pre><code data-lang=\"nix\">inputs = {\n  nixpkgs.url = &quot;github:nixos/nixpkgs/nixos-unstable&quot;;\n  home-manager.url = &quot;github:nix-community/home-manager&quot;;\n  darwin.url = &quot;github:LnL7/nix-darwin/master&quot;;   # macOS, declaratively\n  nix-homebrew.url = &quot;github:zhaofengli-wip/nix-homebrew&quot;;\n  disko.url = &quot;github:nix-community/disko&quot;;        # declarative partitioning\n  fenix.url = &quot;github:nix-community/fenix&quot;;         # Rust toolchains\n  agenix.url = &quot;github:ryantm/agenix&quot;;              # age-encrypted secrets\n  secrets.url = &quot;git+ssh://git@github.com/parallaxisjones/nix-secrets.git&quot;;\n  my-skills.url = &quot;github:parallaxisjones/skills&quot;;  # my Claude skills\n  # ...\n};\n</code></pre>\n<p>A few deliberate choices in there:</p>\n<ul>\n<li><strong><code>nix-darwin</code> + <code>home-manager</code></strong> mean my Mac is configured by the same mechanisms as my Linux boxes. The dock, defaults, and dotfiles are all declarative.</li>\n<li><strong><code>nix-homebrew</code></strong> is the pragmatic concession: GUI Mac apps still come from Homebrew casks, but managed <em>through</em> Nix so the cask list is in the repo, not in my shell history.</li>\n<li><strong><code>fenix</code></strong> pins my Rust toolchain so <code>cargo</code>/<code>clippy</code> are identical everywhere.</li>\n<li><strong><code>secrets</code></strong> is a <em>separate private repo</em> pulled in as an input — encrypted secrets are versioned and shared across hosts without living in this (public) config. That's its own post.</li>\n<li><strong><code>my-skills</code></strong> wires my <a href=\"/posts/skills-with-nix/\">Claude agent skills</a> into the same declarative system.</li>\n</ul>\n<h2 id=\"the-cross-arch-trick-build-where-you-can\">The cross-arch trick: build where you can</h2>\n<p>Here's the problem a mixed fleet runs into immediately: <strong>my macOS laptop cannot build Linux derivations locally.</strong> So how do I deploy to the NixOS box from the Mac? The answer is to build <em>on the target</em> and just orchestrate from the laptop. My <code>justfile</code> deploy recipe:</p>\n<pre><code data-lang=\"make\"># Build + switch the NixOS host remotely over SSH. Builds ON the server\n# (--build-host) since a darwin laptop can&#39;t build Linux derivations locally.\ndeploy host=nixos_host:\n    nix run nixpkgs#nixos-rebuild -- switch \\\n      --flake .#{{nixos_attr}} \\\n      --target-host parallaxis@{{host}} \\\n      --build-host  parallaxis@{{host}} \\\n      --use-remote-sudo\n</code></pre>\n<p><code>--build-host</code> and <code>--target-host</code> both pointing at the server means the laptop never compiles a Linux package — it hands the flake to the server, the server builds and switches, and <code>--use-remote-sudo</code> handles the privilege step. I get to drive everything from my editor on macOS. (Part 2 covers the inverse — building Linux artifacts <em>from</em> the Mac via a remote builder — for when you do want local orchestration of the build itself.)</p>\n<p>There's a <code>check</code> target too, for a fast feedback loop that doesn't build anything:</p>\n<pre><code data-lang=\"make\">check:\n    nix eval .#nixosConfigurations.{{nixos_attr}}.config.system.build.toplevel.drvPath\n</code></pre>\n<p>Evaluating the derivation path is a near-instant syntax-and-type check of the whole host config — the Nix equivalent of <code>tsc --noEmit</code>.</p>\n<h2 id=\"the-gotcha-that-cost-me-time\">The gotcha that cost me time</h2>\n<p>My <code>nixosConfigurations</code> are keyed by <strong>system string</strong>, not hostname. So the deploy target is <code>.#x86_64-linux</code>, <em>not</em> <code>.#nixos</code>:</p>\n<pre><code data-lang=\"make\">nixos_attr := &quot;x86_64-linux&quot;   # NOT the hostname\n</code></pre>\n<p>If you key your configs this way and then try <code>nixos-rebuild --flake .#nixos</code>, you get a confusing \"attribute not found\" with no hint about why. Worth a comment in your own repo so future-you doesn't lose twenty minutes to it. (I did.)</p>\n<h2 id=\"why-it-s-worth-the-upfront-cost\">Why it's worth the upfront cost</h2>\n<p>Nix has a real learning tax, and a single-machine setup rarely justifies it. A <em>fleet</em> does. The payoffs that keep me here:</p>\n<ul>\n<li><strong>Small, frequent, reversible changes.</strong> Every change is a commit; every rebuild is a new generation I can roll back to atomically. I deploy config changes the way I deploy code.</li>\n<li><strong>New machines reach parity from the flake.</strong> No \"setup day.\" The repo <em>is</em> the setup.</li>\n<li><strong>One mental model across operating systems.</strong> I stopped maintaining a separate pile of macOS hacks and a separate pile of Linux hacks. It's all modules.</li>\n</ul>\n<p>The roadmap from here — and the next posts in this series — is the interesting infrastructure underneath: <a href=\"/posts/nix-fleet-remote-builders/\"><strong>remote builders</strong></a> so cross-compilation stops being a chore (part 2), <a href=\"/posts/nix-fleet-agenix-secrets/\"><strong>agenix</strong></a> for secrets that are versioned and fleet-wide without ever sitting in plaintext (part 3), and standing up a <a href=\"/posts/nix-fleet-helios64-disko/\"><strong>Helios64 NAS</strong></a> from bare metal with <code>disko</code> (part 4). The overview is the boring part. The machinery is where Nix gets fun.</p>\n<p><em>The full fleet configuration lives in <a rel=\"external\" href=\"https://github.com/parallaxisjones/dotfiles\"><code>parallaxisjones/dotfiles</code></a>.</em></p>\n<p><em>— Parker Jones, <a rel=\"external\" href=\"https://parkerjones.dev\">parkerjones.dev</a></em></p>\n",
      summary: null,
      date: "2026-06-26T00:00:00Z",
      reading_time: 4,
      metadata: {"series_order":1},
      tags: ["nix","nixos","nix-darwin","home-manager","flakes","infrastructure","macos"],
      categories: ["homelab"],
      series: ["nix-fleet"],
      projects: ["homelab"]
  };



  
  
  
  

  
    
  
  
    
  
  
    
  
  
    
  

  CREATE post CONTENT {
      title: "Nix Fleet, Part 2: Remote Builders — Never Compile Linux on a Mac Again",
      slug: "nix-fleet-remote-builders",
      path: "https://parkerjones.dev/posts/nix-fleet-remote-builders/",
      content: "<blockquote>\n<p><strong>Nix Fleet, part 2.</strong> <a href=\"/posts/nix-fleet-one-flake/\">Part 1</a> laid out the one-flake-every-machine architecture. This part solves the problem that makes a mixed Linux/macOS fleet painful: my laptop can't build Linux.</p>\n</blockquote>\n<p>When you develop NixOS configs on a Mac, you hit a wall fast: an ARM macOS machine cannot build <code>x86_64-linux</code> or <code>aarch64-linux</code> derivations locally. Nothing about Apple Silicon produces Linux binaries. So either you accept that you can only iterate on Linux configs <em>from</em> a Linux machine, or you set up a remote builder and make the problem disappear.</p>\n<p>I set up the remote builder. Now my M3 laptop offloads every Linux build to a beefy x86_64 NixOS box, and from where I sit it's transparent — <code>nix build</code> just works, the fans on the laptop stay quiet, and the heavy lifting happens on a machine designed for it.</p>\n<h2 id=\"the-shape-of-the-solution\">The shape of the solution</h2>\n<p>There are two roles:</p>\n<ul>\n<li>The <strong>builder</strong> — my always-on x86_64 NixOS box (SSH alias <code>nixos</code>). It has the cores, the RAM, and the right architecture.</li>\n<li>The <strong>controller</strong> — my M3 MacBook. It evaluates the flake and orchestrates, but delegates the actual compilation.</li>\n</ul>\n<p>The trick that makes this clean: configure the controller so that it does <strong>zero</strong> local jobs and ships everything to the builder.</p>\n<h2 id=\"builder-side-accept-work-and-emulate-arm\">Builder side: accept work, and emulate ARM</h2>\n<p>The builder needs to be reachable over SSH and willing to build for the architectures I care about. The one non-obvious piece is ARM: I also run an <code>aarch64-linux</code> machine (a <a href=\"/posts/nix-fleet-helios64-disko/\">Helios64 NAS</a>), and rather than stand up a separate ARM builder, I let the x86 box build ARM derivations through binfmt/QEMU emulation:</p>\n<pre><code data-lang=\"nix\"># on the x86_64 builder\nboot.binfmt.emulatedSystems = [ &quot;aarch64-linux&quot; ];\n</code></pre>\n<p>That one line registers a QEMU user-mode handler so the x86 host transparently runs <code>aarch64</code> build steps. It's slower than native ARM (more on that below), but it means <em>one</em> builder covers my entire Linux fleet across both architectures.</p>\n<h2 id=\"controller-side-offload-everything\">Controller side: offload everything</h2>\n<p>On the Mac, I point Nix at the builder and tell it not to build anything itself:</p>\n<pre><code data-lang=\"text\"># /etc/nix/nix.conf (or the equivalent NixOS/darwin option)\nbuilders = @/etc/nix/machines\nbuilders-use-substitutes = true\nmax-jobs = 0\n</code></pre>\n<pre><code data-lang=\"text\"># /etc/nix/machines\nnixos x86_64-linux / - 8 1 kvm,big-parallel\n</code></pre>\n<p>The magic value is <strong><code>max-jobs = 0</code></strong>. It tells the local machine to run zero build jobs — so every derivation that isn't already in a binary cache <em>must</em> go to a remote builder. There's no \"sometimes it builds locally and melts the laptop\" ambiguity; local builds are simply off. <code>builders-use-substitutes = true</code> lets the builder pull from binary caches directly instead of having the controller ferry everything, which saves a lot of pointless data movement.</p>\n<p>The <code>/etc/nix/machines</code> line reads as: builder <code>nixos</code>, builds <code>x86_64-linux</code>, default SSH key, up to <code>8</code> parallel jobs, speed factor <code>1</code>, and it supports the <code>kvm</code> and <code>big-parallel</code> features (so it'll accept jobs that need KVM or heavy parallelism).</p>\n<h2 id=\"verifying-it-actually-offloads\">Verifying it actually offloads</h2>\n<p>The failure mode here is silent: you think you're offloading and you're not. So verify. First, list what the flake even knows how to build:</p>\n<pre><code data-lang=\"bash\">nix eval .#nixosConfigurations --apply builtins.attrNames\n</code></pre>\n<p>Then kick off a real Linux build from the Mac and watch <em>the builder</em>, not the laptop:</p>\n<pre><code data-lang=\"bash\">nix build .#nixosConfigurations.&lt;host&gt;.config.system.build.toplevel\n</code></pre>\n<p>If it's working, the laptop sits nearly idle while the x86 box lights up. If <code>max-jobs</code> is misconfigured, you'll feel it — the Mac will either try (and fail, for Linux) or grind. Watching <code>nproc</code>/<code>free -h</code>/build load on the builder during a build is the quickest confirmation.</p>\n<h2 id=\"when-builds-go-wrong\">When builds go wrong</h2>\n<p>Offloading introduces its own failure modes, and I keep a short runbook for them:</p>\n<ul>\n<li><strong>OOM on the builder</strong>, especially during ARM-emulated builds, which are memory-hungry. Mitigations: serialize with <code>--option max-jobs 1 --option cores 2</code>, or add temporary <code>zram</code> swap:<pre><code data-lang=\"nix\">{ zramSwap = { enable = true; memoryPercent = 50; }; }\n</code></pre>\n</li>\n<li><strong>Inspecting a failed derivation</strong> without re-running the whole build:<pre><code data-lang=\"bash\">nix log /nix/store/&lt;hash&gt;-&lt;drv&gt;.drv | less -R\n</code></pre>\n</li>\n<li><strong>Checking effective settings</strong> when something isn't offloading as expected:<pre><code data-lang=\"bash\">nix show-config | egrep &#39;^(cores|max-jobs|builders|system-features|substituters)&#39;\n</code></pre>\n</li>\n</ul>\n<h2 id=\"the-honest-trade-off\">The honest trade-off</h2>\n<p>Emulated ARM builds are <em>slow</em> — QEMU user-mode emulation has real overhead, and a big <code>aarch64</code> derivation built on x86 can take noticeably longer than it would native. For my fleet that's an acceptable price: I build ARM rarely (the NAS config doesn't change often), and the alternative is maintaining a second physical ARM builder. If you're iterating heavily on ARM, buy or borrow a native <code>aarch64</code> builder and add it to <code>/etc/nix/machines</code> as a second entry — the controller config doesn't otherwise change. The architecture scales to N builders the same way it handles one.</p>\n<p>The win is the same one Part 1 was about: I get to live entirely in macOS, drive my whole Linux fleet from there, and never once wait on my laptop to compile something it was never meant to compile. <a href=\"/posts/nix-fleet-agenix-secrets/\">Part 3</a> tackles the next fleet-wide concern — secrets — including how a first-boot install fetches them securely over the same SSH trust I just set up here.</p>\n<p><em>The builder/controller config is part of my <a rel=\"external\" href=\"https://github.com/parallaxisjones/dotfiles\"><code>parallaxisjones/dotfiles</code></a> repo.</em></p>\n<p><em>— Parker Jones, <a rel=\"external\" href=\"https://parkerjones.dev\">parkerjones.dev</a></em></p>\n",
      summary: null,
      date: "2026-06-26T00:00:00Z",
      reading_time: 5,
      metadata: {"series_order":2},
      tags: ["nix","nixos","remote-builders","macos","binfmt","cross-compilation","infrastructure"],
      categories: ["homelab"],
      series: ["nix-fleet"],
      projects: ["homelab"]
  };



  
  
  
  

  
    
  
  
    
  
  
  

  CREATE post CONTENT {
      title: "How I Cut My Claude Code Token Usage by 90% with RTK",
      slug: "rtk-token-killer",
      path: "https://parkerjones.dev/posts/rtk-token-killer/",
      content: "<p>I've been running coding agents against real work for a while now, and the thing nobody warns you about is the <em>output tax</em>. Every time the agent runs <code>git status</code>, <code>cargo test</code>, or <code>aws lambda list-functions</code>, the entire wall of text it gets back is fed into the model's context. You pay for those tokens once when they arrive — and then again on every single turn after, because that output sits in the context window and gets re-sent until the conversation ends.</p>\n<p>Most of that text is noise. ANSI color codes. Timestamps. Table columns I don't care about. JSON fields I'll never read. So I started routing every command through <strong><a rel=\"external\" href=\"https://www.rtk-ai.app/\">RTK</a></strong> (Rust Token Killer) — an open-source CLI proxy, written in Rust, built to strip exactly this kind of bloat before it reaches the model. It's a <code>brew install rtk</code> away.</p>\n<p>Here's what it's done across my last few thousand commands:</p>\n<pre><code data-lang=\"text\">RTK Token Savings (Global Scope)\n════════════════════════════════════════════════════════════\nTotal commands:    6283\nInput tokens:      26.9M\nOutput tokens:     2.5M\nTokens saved:      24.5M (90.9%)\nTotal exec time:   462m34s (avg 4.4s)\nEfficiency meter:  ██████████████████████░░ 90.9%\n</code></pre>\n<p>Six thousand commands of <em>my</em> actual usage, <strong>24.5 million tokens saved — 90.9%</strong>. That's not a vendor benchmark; it's my own <code>rtk gain</code> report.</p>\n<h2 id=\"the-hidden-cost-of-agent-tool-output\">The hidden cost of agent tool output</h2>\n<p>When you use a chat model directly, you read its output and the cost stops there. When you put a model in an <em>agent loop</em>, the economics flip. The model runs a tool, the tool's stdout/stderr comes back as a tool result, and that result becomes part of the running transcript. Every subsequent turn re-sends the whole transcript. So a 4,000-token <code>cargo test</code> dump on turn 3 is still being paid for on turn 30.</p>\n<p>The fix isn't \"run fewer commands.\" Agents <em>should</em> poke at the system constantly — that's what makes them useful. The fix is to make each command's output carry only the information the model actually needs to make its next decision.</p>\n<p>That's a filtering problem, and filtering is exactly the kind of thing Rust is good at: fast, streaming, cheap enough that wrapping every command in the agent's hot loop adds no friction. In my usage RTK averages 4.4s per command — and most of that is the underlying command itself, not the proxy.</p>\n<h2 id=\"how-it-works-transparent-hook-based-rewriting\">How it works: transparent, hook-based rewriting</h2>\n<p>What sold me on it is that I never have to think about it. I don't want to teach the agent to \"use rtk\" — I want every command it already runs to get filtered automatically.</p>\n<p>So I wired RTK into Claude Code as a hook. When the agent decides to run <code>git status</code>, the hook rewrites it to <code>rtk git status</code> before execution. RTK runs the real command, reshapes the output, and hands back the lean version. The agent never knows the proxy is there, and the rewrite itself costs zero tokens.</p>\n<pre><code data-lang=\"text\">git status        →   rtk git status      (transparent, 0 tokens overhead)\ncargo test        →   rtk cargo test\naws lambda list…  →   rtk aws lambda list-functions\n</code></pre>\n<p>There are a handful of meta-commands I call directly:</p>\n<pre><code data-lang=\"bash\">rtk gain              # token-savings analytics (the table above)\nrtk gain --history    # per-command history with savings\nrtk discover          # mine Claude Code history for missed opportunities\nrtk proxy &lt;cmd&gt;       # run a command raw, no filtering (escape hatch)\n</code></pre>\n<h2 id=\"where-the-savings-actually-come-from\">Where the savings actually come from</h2>\n<p>Not every command saves the same amount, and that's the interesting part. Breaking down my top commands by impact:</p>\n<pre><code data-lang=\"text\">  #  Command                   Count   Saved    Avg%\n 1.  rtk cargo test --work…        7   10.8M  100.0%\n 2.  rtk aws lambda list-f…        9    3.1M   22.2%\n 3.  rtk read                    602    2.7M   18.0%\n 4.  rtk git push origin …         1    1.8M  100.0%\n 5.  rtk cargo test --work…        1    1.8M  100.0%\n 6.  rtk git push -u origi…        1    1.7M  100.0%\n 7.  rtk aws cloudformatio…        2  324.0K   50.0%\n 8.  rtk:toml ps -ef               6  298.1K   98.7%\n 9.  rtk:toml ps aux               5  288.8K   98.5%\n10.  rtk gh pr diff               18  110.7K   48.1%\n</code></pre>\n<p>Three distinct categories show up here:</p>\n<p><strong>Full suppression (100%).</strong> <code>cargo test --workspace</code> and <code>git push</code> produce enormous progress streams — compiler chatter, per-test lines, transfer counters — and the only thing the model needs is <em>did it pass</em> or <em>what failed</em>. When everything succeeds, the right answer is a one-line summary, and the raw 10.8M tokens of test output never enter context. That single command class is my biggest win by a mile.</p>\n<p><strong>Reshaping (98%).</strong> <code>ps -ef</code> and <code>ps aux</code> are wide, repetitive tables. Run them through RTK's structured <code>:toml</code> output mode and you get a compact, parseable representation — 98%+ smaller, and <em>easier</em> for the model to reason about than a column-aligned ASCII table.</p>\n<p><strong>Trimming (18–50%).</strong> <code>read</code>, <code>gh pr diff</code>, and <code>aws</code> calls don't get gutted — the content matters — but there's still 18–50% of pure formatting cruft to shave. Note <code>read</code> ran <strong>602 times</strong>: small per-call savings on a high-frequency command adds up to 2.7M tokens.</p>\n<p>The lesson I keep relearning: the things you do constantly (<code>read</code>) <em>and</em> the things that dump enormous one-shot payloads (<code>cargo test</code>) both matter. The middle is where most of the volume hides.</p>\n<h2 id=\"structured-output-with-toml-mode\">Structured output with <code>:toml</code> mode</h2>\n<p>The <code>:toml</code> variants above aren't a gimmick. ASCII tables are optimized for <em>human eyes</em> — alignment, separators, headers repeated for readability. A model doesn't need any of that; it needs keys and values. Emitting <code>ps aux</code> as compact TOML drops the token count by ~98% <strong>and</strong> removes the ambiguity of parsing whitespace-aligned columns. Cheaper and more reliable at the same time is a rare trade to win.</p>\n<h2 id=\"finding-what-you-re-missing-rtk-discover\">Finding what you're missing: <code>rtk discover</code></h2>\n<p>The one I reach for when I want to tune things is <code>rtk discover</code>. It reads back through my Claude Code history and flags commands that burned tokens but aren't being proxied yet — the long tail of \"oh, I run <em>that</em> a lot.\" It turns token optimization into a feedback loop instead of a guessing game: see what's expensive, make sure it's routed through RTK, check <code>gain</code> a week later, repeat.</p>\n<h2 id=\"when-not-to-filter\">When <em>not</em> to filter</h2>\n<p>Filtering is lossy by definition, and sometimes I genuinely need the raw bytes — debugging a tool whose exact output format is the thing I'm investigating. That's what <code>rtk proxy &lt;cmd&gt;</code> is for: run it completely untouched. Having a clean escape hatch is what makes aggressive default filtering safe. If a trim ever hides something I needed, I'm one command away from the truth.</p>\n<p>One footgun worth flagging: there's a name collision out there. If <code>rtk gain</code> errors with \"command not found,\" you've probably got <code>reachingforthejack/rtk</code> (a Rust Type Kit) on your <code>PATH</code> instead of the token killer. <code>which rtk</code> sorts it out — the one you want is the <a rel=\"external\" href=\"https://formulae.brew.sh/formula/rtk\">Homebrew <code>rtk</code> formula</a>.</p>\n<h2 id=\"what-i-learned\">What I learned</h2>\n<p>I started using RTK thinking of it as a cost optimization, and it is — but the bigger payoff turned out to be <strong>context window hygiene</strong>. Tokens are cheap-ish; <em>context is scarce</em>. Every line of noise it strips is a line that isn't crowding out something the model actually needs to remember three turns from now. The 90% I'm not paying for is nice. The 90% that isn't diluting the agent's attention is the real win.</p>\n<p>If you're running agents against a noisy CLI, the output tax is real and you're almost certainly paying it — and it's very killable. RTK lives at <a rel=\"external\" href=\"https://www.rtk-ai.app/\">rtk-ai.app</a>; <code>brew install rtk</code> and <code>rtk gain</code> will tell you your own number.</p>\n<p><em>— Parker Jones, <a rel=\"external\" href=\"https://parkerjones.dev\">parkerjones.dev</a></em></p>\n",
      summary: null,
      date: "2026-06-26T00:00:00Z",
      reading_time: 6,
      metadata: {},
      tags: ["claude-code","ai","agents","cli","tokens","rust","tooling"],
      categories: ["software"],
      series: [],
      projects: []
  };



  
  
  
  

  
    
  
  
    
  
  
  
    
  

  CREATE post CONTENT {
      title: "Building a Second Brain for Engineers: an LLM Capture-and-Synthesis Pipeline",
      slug: "second-brain-llm-pipeline",
      path: "https://parkerjones.dev/posts/second-brain-llm-pipeline/",
      content: "<p>As an engineer, the context you need to do your job is scattered across a dozen systems: decisions made in chat threads, the <em>why</em> behind a change buried in a pull request, design rationale in a wiki page, a commitment made in a meeting nobody wrote down. Most of it decays. Six months later you're re-deriving a decision you already made because the only record was a Slack thread that scrolled into oblivion.</p>\n<p>I built a pipeline to fix that — an LLM-assisted system that <em>captures</em> raw material from those systems and <em>synthesizes</em> it into a durable, searchable knowledge base. This post is the design, generalized. I'm deliberately keeping it tool- and employer-neutral; the value is in the architecture, which ports to any stack.</p>\n<h2 id=\"the-three-layer-model\">The three-layer model</h2>\n<p>The system has three layers, and keeping them separate is the whole game:</p>\n<pre><code data-lang=\"text\">  Layer 1: sources/    raw captures, one file per artifact, lightly structured\n  Layer 2: wiki/       synthesized pages — the compressed, durable knowledge\n  Layer 3: schema      the protocol: conventions, frontmatter, ingest rules\n</code></pre>\n<ul>\n<li><strong>Sources</strong> are raw. A captured chat thread, a PR snapshot, a meeting transcript — verbatim, with metadata, written to a file. Cheap and lossless.</li>\n<li><strong>The wiki</strong> is synthesized. A synthesis agent reads new sources and folds them into durable pages: a glossary entry, an incident postmortem, a reusable pattern. The wiki's job is <em>compression</em>, not mirroring.</li>\n<li><strong>The schema</strong> is the contract both layers obey — filename conventions, frontmatter shape, the ingest protocol. It changes rarely and deliberately.</li>\n</ul>\n<p>The insight that made this work: <strong>capture and synthesis are different problems with different failure modes, so decouple them.</strong> Capture must be fast and reliable (you're stealing a moment to save something). Synthesis can be slow and batched (it runs later, reads everything new, thinks hard). Couple them and a slow synthesis step blocks you from capturing at all. Decouple them and capture is instant; synthesis happens on its own cadence and reads whatever has accumulated.</p>\n<h2 id=\"what-to-capture-per-source\">What to capture, per source</h2>\n<p>Every source type — chat, issue tracker, code host, docs/wiki, meeting transcripts, ad-hoc paste-ins — gets the same five-part treatment:</p>\n<ol>\n<li><strong>Capture-worthy signals.</strong> What's actually worth keeping. For a chat platform: threads where a decision is announced (\"let's go with…\", \"approved\", \"ship it\"), threads where you're mentioned and haven't replied, high-engagement threads in channels you watch.</li>\n<li><strong>Noise to skip.</strong> Bot notifications. Your own status pings. Reaction-only activity. The stuff that would bury the signal.</li>\n<li><strong>Output shape.</strong> The frontmatter and filename the capture writes — so the synthesis layer can consume it without guessing.</li>\n<li><strong>Cadence.</strong> Daily sweep? On-demand? Weekly?</li>\n<li><strong>Quirks.</strong> The source-specific gotchas (pagination, permalink expiry, HTML-to-markdown fidelity).</li>\n</ol>\n<p>A captured issue-tracker ticket, for instance, lands as structured frontmatter plus a body:</p>\n<pre><code data-lang=\"yaml\">---\nsource: issue-tracker\nkey: PROJ-1234\ncaptured: 2026-06-26T08:00:00-04:00\nstatus: Released\nlast_event: 2026-06-25T16:22:00-04:00\n---\n</code></pre>\n<p>That <code>last_event</code> field is the high-water mark: on the next sweep, the capturer fetches only events newer than it, instead of re-pulling the whole ticket.</p>\n<h2 id=\"capture-mechanisms-and-the-trap-everyone-hits\">Capture mechanisms — and the trap everyone hits</h2>\n<p>There are three mechanism families, and choosing the right one per source is most of the design:</p>\n<table><thead><tr><th>Mechanism</th><th>Best for</th><th>Trade-off</th></tr></thead><tbody>\n<tr><td><strong>Skills</strong> (slash commands)</td><td>On-demand, mid-conversation capture</td><td>You have to remember to invoke them</td></tr>\n<tr><td><strong>Hooks</strong> (event-driven)</td><td>Local events you control</td><td>Narrower than the name suggests — see below</td></tr>\n<tr><td><strong>Scheduled tasks</strong> (periodic sweeps)</td><td>External-system polling</td><td>Latency up to one cadence; needs dedup state</td></tr>\n</tbody></table>\n<p>Here's the trap, and it's worth stating loudly because it cost me an afternoon: <strong>AI-agent hooks fire on <em>conversation</em> events</strong> — the agent stopped, a tool is about to run, the user submitted a prompt — <strong>not on external events.</strong> There is no \"hook on PR opened\" or \"hook on Slack message.\" If you want event-driven capture from an external system, the real path is something outside the agent's hook system entirely: a git <code>post-merge</code> hook on your own machine, or a CI action that writes to a synced directory. Conflating \"agent hook\" with \"webhook\" sends you building something that can't exist.</p>\n<p>So the actual recommendation per source is mostly: <strong>scheduled sweep for external systems</strong> (poll daily, dedup against state), <strong>on-demand skill for user-initiated capture</strong> (<code>/capture &lt;url&gt;</code> when you decide something matters), and hooks only in the narrow spots where a <em>local</em> event is the trigger.</p>\n<h2 id=\"dedup-and-freshness-via-filename-conventions\">Dedup and freshness via filename conventions</h2>\n<p>State is the enemy of reliability, so I push dedup into filenames instead of a database. Two shapes:</p>\n<ul>\n<li><strong>Time-stamped capture</strong> — <code>&lt;source&gt;-&lt;id&gt;-&lt;YYYY-MM-DD&gt;.md</code> — for artifacts with no stable identity (a daily chat digest, an ad-hoc article). Each capture is its own file.</li>\n<li><strong>Stable-entity capture</strong> — <code>&lt;source&gt;-&lt;id&gt;.md</code> — for things with a canonical identity, recaptured <em>in place</em> (a ticket, a PR, a wiki page).</li>\n</ul>\n<p>Recapture semantics depend on the source's own model:</p>\n<ul>\n<li><strong>Append-only, newest-first</strong> for sources with event timelines (tickets, PRs): each recapture prepends a dated section; old content stays.</li>\n<li><strong>Versioned-replace with diff preservation</strong> for edit-replace sources (wiki pages): replace the body, but keep the prior version under a <code>## Previous version</code> heading so the diff isn't lost.</li>\n</ul>\n<p>The filename <em>is</em> the dedup key. Two captures of the same ticket can never land under two different names — which, before I imposed this, is exactly what happened.</p>\n<h2 id=\"when-does-a-raw-source-become-a-wiki-page\">When does a raw source become a wiki page?</h2>\n<p>Not everything earns synthesis. A source gets promoted to its own durable page when <strong>any</strong> of these holds:</p>\n<ol>\n<li><strong>Recurrence</strong> — the concept shows up 3+ times across captures and notes. Recurring relevance means compression pays off.</li>\n<li><strong>Decision-of-record</strong> — it documents a choice that'll be referenced later (an architecture decision, an incident root cause). Decisions get a page on first capture; the value is findability.</li>\n<li><strong>Reusable pattern</strong> — it describes a technique that applies beyond its origin. A pattern extracted from one incident belongs in the wiki because it'll apply to the next one.</li>\n</ol>\n<p>Everything else stays raw. A one-off thread that resolved itself, a ticket shipped without revisiting — those live in <code>sources/</code> as a searchable archive but never clutter the synthesized layer. <strong>Synthesis over enumeration:</strong> one wiki page can cite ten sources. The wiki compresses; it doesn't mirror.</p>\n<h2 id=\"the-one-rule-you-can-t-skip-sensitive-content\">The one rule you can't skip: sensitive content</h2>\n<p>The moment you point automated capture at chat and meetings, you're one bad sweep away from archiving someone's DM, an HR conversation, or customer-confidential material. The rule has to be <strong>default-deny for the sensitive class</strong>: skip private channels, skip DMs, require explicit confirmation before persisting a meeting transcript. It's safer to under-capture and manually add than to over-capture and have to scrub. Design this in from line one, not after the first incident.</p>\n<h2 id=\"why-this-is-worth-the-ceremony\">Why this is worth the ceremony</h2>\n<p>\"Why this much machinery for personal notes?\" is a fair question, and the honest answer is: because the alternative — capturing by hand, when you remember, in whatever tool is open — doesn't scale past a few weeks of good intentions. The pipeline's entire purpose is to make <em>capture cheaper than not capturing</em>, and to make synthesis happen whether or not you feel like it that day.</p>\n<p>The tools are interchangeable — your chat platform, your agent runner, your note format. The architecture is the durable part: <strong>raw capture decoupled from batched synthesis, dedup encoded in filenames, a promotion rule that keeps the synthesized layer small, and default-deny on anything sensitive.</strong> Build that, and the context you need stops decaying.</p>\n<p><em>— Parker Jones, <a rel=\"external\" href=\"https://parkerjones.dev\">parkerjones.dev</a></em></p>\n",
      summary: null,
      date: "2026-06-26T00:00:00Z",
      reading_time: 7,
      metadata: {},
      tags: ["ai","agents","knowledge-management","second-brain","claude-code","systems-design"],
      categories: ["software"],
      series: [],
      projects: ["agency"]
  };



  
  
  
  

  
    
  
  
    
  
  
  
    
  

  CREATE post CONTENT {
      title: "Shipping Claude Skills with Nix: a Reproducible Agent Toolkit Across My Fleet",
      slug: "skills-with-nix",
      path: "https://parkerjones.dev/posts/skills-with-nix/",
      content: "<p>A coding agent is only as good as the procedures you hand it. Out of the box, a model improvises every task from scratch; give it a <em>skill</em> — a written procedure for \"do TDD,\" \"diagnose a hard bug,\" \"turn this into issues\" — and it stops guessing and starts following a method you trust. I've built up a collection of these, and once you have more than a handful the real problem isn't writing them, it's <strong>distribution</strong>: getting the same skills onto every machine I work from, version-pinned, without copy-pasting Markdown around.</p>\n<p>I solved that with Nix. Here's the setup.</p>\n<h2 id=\"what-a-skill-is\">What a skill is</h2>\n<p>My skills live in a <a rel=\"external\" href=\"https://github.com/parallaxisjones/skills\">repo</a> (a fork of <a rel=\"external\" href=\"https://github.com/mattpocock/skills\">Matt Pocock's <code>skills</code></a>, credit where it's due), organized into buckets — <code>engineering</code>, <code>qa</code>, <code>productivity</code>, <code>personal</code>, <code>misc</code>. Each skill is a directory with a <code>SKILL.md</code>: YAML frontmatter naming it and describing <em>when</em> to use it, then the procedure itself.</p>\n<pre><code data-lang=\"markdown\">---\nname: diagnose\ndescription: Disciplined diagnosis loop for hard bugs and performance\n  regressions. Reproduce → minimise → hypothesise → instrument → fix →\n  regression-test. Use when user says &quot;diagnose this&quot; / &quot;debug this&quot;...\n---\n\n# Diagnose\n...\n</code></pre>\n<p>The <code>description</code> is load-bearing — it's what the agent matches against to decide whether the skill is relevant. A few I reach for constantly: <code>tdd</code> (red-green-refactor), <code>diagnose</code> (the loop above), <code>to-prd</code> / <code>to-issues</code> / <code>triage</code> (turning a vague ask into tracked work), and <code>write-a-skill</code> (the skill that writes more skills). Small, composable, model-agnostic. No framework owning my process — just procedures I can read, edit, and trust.</p>\n<h2 id=\"three-ways-to-install-them\">Three ways to install them</h2>\n<p>The repo supports three distribution paths, in increasing order of how much I actually rely on them.</p>\n<p><strong>1. <code>npx</code>, for anyone.</strong> The zero-commitment path:</p>\n<pre><code data-lang=\"bash\">npx skills@latest add parallaxisjones/skills\n</code></pre>\n<p>Pick the skills and agents you want, and you're set. Great for trying them on a machine that isn't mine.</p>\n<p><strong>2. A symlink script, for a local clone.</strong> If I've cloned the repo, <code>link-skills.sh</code> symlinks every <code>SKILL.md</code> into <code>~/.claude/skills/</code> so the CLI picks them up directly. It's idempotent — re-run after pulling — and it specifically guards against the footgun where <code>~/.claude/skills</code> is <em>itself</em> a symlink back into the repo, which would write per-skill symlinks into my own working copy:</p>\n<pre><code data-lang=\"bash\">if [ -L &quot;$DEST&quot; ]; then\n  resolved=&quot;$(readlink -f &quot;$DEST&quot;)&quot;\n  case &quot;$resolved&quot; in\n    &quot;$REPO&quot;/*) echo &quot;refusing to pollute the repo&quot;; exit 1 ;;\n  esac\nfi\n</code></pre>\n<p>That defensive check is the kind of thing you write <em>after</em> the first time a script eats its own tail.</p>\n<p><strong>3. Nix, for my actual fleet.</strong> This is the one that matters. My skills repo is a flake input in my <a href=\"/posts/nix-fleet-one-flake/\">system config</a>, and a home-manager module materializes them declaratively on every machine.</p>\n<h2 id=\"the-nix-wiring\">The Nix wiring</h2>\n<p>Two inputs do the work — my skills repo, and <a rel=\"external\" href=\"https://github.com/Kyure-A/agent-skills-nix\"><code>agent-skills-nix</code></a>, a home-manager module that knows how to turn a skills repo into materialized files:</p>\n<pre><code data-lang=\"nix\"># flake.nix\ninputs = {\n  agent-skills-nix = {\n    url = &quot;github:Kyure-A/agent-skills-nix&quot;;\n    inputs.nixpkgs.follows = &quot;nixpkgs&quot;;\n    inputs.home-manager.follows = &quot;home-manager&quot;;\n  };\n  my-skills.url = &quot;github:parallaxisjones/skills&quot;;\n};\n</code></pre>\n<p>Then in home-manager I point the module at my repo, filter to the buckets I want live, and enable everything:</p>\n<pre><code data-lang=\"nix\">agent-skills = {\n  enable = true;\n  sources.mine = {\n    input  = &quot;my-skills&quot;;\n    subdir = &quot;skills&quot;;\n    filter.nameRegex = &quot;^(engineering|misc|personal|productivity)/.*&quot;;\n  };\n  skills.enableAll = true;\n  targets.claude.enable = true;\n};\n</code></pre>\n<p>That's the whole thing. On <code>nixos-rebuild switch</code> (or <code>darwin-rebuild</code> on the Mac), every <code>SKILL.md</code> matching the regex gets written into Claude's skills directory. The <code>deprecated/</code> bucket is excluded by the filter, so retiring a skill is a one-line regex change, not a manual delete on five machines.</p>\n<h2 id=\"why-bother-what-nix-actually-buys-here\">Why bother — what Nix actually buys here</h2>\n<p>You could argue the symlink script is simpler, and for one machine it is. The fleet is where Nix earns it:</p>\n<ul>\n<li><strong>Pinning.</strong> <code>flake.lock</code> records the exact skills commit each machine is on. \"Which version of my <code>triage</code> skill is the laptop running?\" has an answer, not a shrug.</li>\n<li><strong>Parity from scratch.</strong> A fresh machine reaches full skill parity as a side effect of building its system config. There's no separate \"and don't forget to install your skills\" step — it's the same <code>switch</code> that installs my shell and my packages.</li>\n<li><strong>Atomic rollback.</strong> If a skill edit makes the agent behave worse, rolling back the generation rolls back the skills with it.</li>\n</ul>\n<p>There's a framing I lean on for deciding what to manage this way — think of three zones. <em>Zone 1</em> is stable, declarative config (the home-manager module enabling skills). <em>Zone 3</em> is runtime state that should never touch Nix (per-conversation agent memory). Skills sit in <strong>Zone 2</strong>: authored content, edited as plain files in their repo, but <em>materialized</em> onto each machine by Nix. Nix owns where they land and which version; I own what they say.</p>\n<h2 id=\"the-honest-caveat\">The honest caveat</h2>\n<p>Nix doesn't validate skill <em>content</em> — a <code>SKILL.md</code> with a bad <code>description</code> will deploy just as reliably as a good one. This pipeline guarantees distribution and pinning, not quality. The quality comes from treating skills like code: review them, iterate on the descriptions when the agent picks the wrong one, and delete the ones that stop earning their place (that's what <code>deprecated/</code> is for).</p>\n<p>But that's the right division of labor. Writing a good procedure is human work. Making sure that procedure is <em>identically present on every machine I touch</em> is exactly the kind of toil Nix exists to kill. Treat your agent's skills as part of your declarative system, not as dotfiles you sync by hand.</p>\n<p><em>— Parker Jones, <a rel=\"external\" href=\"https://parkerjones.dev\">parkerjones.dev</a></em></p>\n",
      summary: null,
      date: "2026-06-26T00:00:00Z",
      reading_time: 5,
      metadata: {"featured":true,"project_name":"Claude Skills × Nix","repo":"https://github.com/parallaxisjones/skills"},
      tags: ["nix","claude-code","ai","agents","skills","home-manager","reproducibility"],
      categories: ["software"],
      series: [],
      projects: ["agency"]
  };



  
  
  
  

  
    
  
  
    
  
  
  
    
  

  CREATE post CONTENT {
      title: "Reproducible, Secret-Safe AI Agents with Nix Flakes, agenix, and Magentic-One",
      slug: "lab-agency",
      path: "https://parkerjones.dev/posts/lab-agency/",
      content: "<p>Most \"I built an AI agent\" posts skip the unglamorous parts: how the environment gets set up identically on another machine, where the API key actually lives, and what breaks when you try to make it reproducible. Those are the parts I find interesting, so this post is about the <em>plumbing</em> of <a rel=\"external\" href=\"https://github.com/parallax-labs/lab-agency\">Lab Agency</a> — a multi-agent app wired together with Microsoft's <a rel=\"external\" href=\"https://www.microsoft.com/en-us/research/articles/magentic-one-a-generalist-multi-agent-system-for-solving-complex-tasks/\">Magentic-One</a> — built on a Nix flake with secrets managed by <a rel=\"external\" href=\"https://github.com/ryantm/agenix\">agenix</a>.</p>\n<p>The agents are the fun part. Getting them to start the same way every time, with a decrypted key and the right Python deps, is the part that actually took the work.</p>\n<h2 id=\"the-flake-one-entry-point-several-jobs\">The flake: one entry point, several jobs</h2>\n<p>The whole project is defined by a <code>flake.nix</code> with a few inputs and a <code>flake-utils</code> wrapper so it builds across systems:</p>\n<pre><code data-lang=\"nix\">inputs = {\n  nixpkgs.url    = &quot;github:NixOS/nixpkgs/nixos-unstable&quot;;\n  flake-utils.url = &quot;github:numtide/flake-utils&quot;;\n  agenix.url     = &quot;github:ryantm/agenix&quot;;\n};\n</code></pre>\n<p>From there it exposes four targets, each a different way into the same project:</p>\n<ul>\n<li><strong><code>devShell</code></strong> — an interactive shell for hacking on the code.</li>\n<li><strong><code>run-surfer</code></strong> (default) — boots the Chainlit app.</li>\n<li><strong><code>inspect-embeddings</code></strong> — pokes at the vector store.</li>\n<li><strong><code>index-documents</code></strong> — loads documents into the knowledge base.</li>\n</ul>\n<p>The point of doing it this way is that \"clone and run\" is <em>true</em>. There's no README step that says \"first, set these five environment variables and install these packages.\" The flake target is the setup.</p>\n<h2 id=\"secrets-that-decrypt-themselves-in-the-shell\">Secrets that decrypt themselves, in the shell</h2>\n<p>The piece I'm proudest of is how the OpenAI key is handled: it's never on disk in plaintext and never pasted into a shell. It lives age-encrypted at <code>secrets/openai.txt</code>, and the flake decrypts it into the environment as the shell starts up:</p>\n<pre><code data-lang=\"nix\">devShell = pkgs.mkShell {\n  buildInputs = [ agenixCli pkgs.python3 ];\n  shellHook = &#39;&#39;\n    echo &quot;Decrypting OpenAI secret...&quot;\n    export OPENAI_API_KEY=$(agenix --decrypt secrets/openai.txt \\\n      --identity ~/.ssh/parallaxis)\n\n    if [ ! -d venv ]; then\n      python3 -m venv venv\n      source venv/bin/activate\n      pip install --upgrade pip\n      pip install -r requirements.txt\n    else\n      source venv/bin/activate\n    fi\n  &#39;&#39;;\n};\n</code></pre>\n<p><code>agenixCli</code> here is just <code>agenix.packages.${system}.default</code> pulled from the input. Decryption keys off my SSH identity (<code>~/.ssh/parallaxis</code>), so the encrypted secret can sit in the repo and only someone holding the right key can read it. The encrypted file is safe to commit; the key never is.</p>\n<p>The <code>run-surfer</code> target repeats the same decrypt-and-venv dance inside a <code>writeShellScriptBin</code> so the app launches with one command:</p>\n<pre><code data-lang=\"nix\">runSurfer = pkgs.writeShellScriptBin &quot;run-surfer&quot; &#39;&#39;\n  # ...ensure venv, then:\n  export OPENAI_API_KEY=$(agenix --decrypt secrets/openai.txt \\\n    --identity ~/.ssh/parallaxis)\n  exec chainlit run clapp.py -w\n&#39;&#39;;\n</code></pre>\n<p><strong>An honest caveat:</strong> there's a Python <code>venv</code> living <em>inside</em> a Nix flake here, which is not pure Nix and a purist would wince. I made that trade deliberately — the AI ecosystem moves fast and pinning everything through nixpkgs would mean fighting the toolchain instead of building the product. Nix gives me a reproducible <em>shell</em> (right Python, right <code>agenix</code>, right secret); <code>pip install -r requirements.txt</code> handles the fast-moving libraries. It's a pragmatic seam, not a principled one, and I'd reconsider it the moment the dependency set stabilizes.</p>\n<p>Magentic-One's CLI does get the full Nix treatment, though — it's packaged in its own <code>autogen-flake/magentic-one-cli.nix</code> rather than left to pip.</p>\n<h2 id=\"the-agents-a-magentic-one-team-plus-a-rag-layer\">The agents: a Magentic-One team plus a RAG layer</h2>\n<p>The app itself (<code>clapp.py</code>) is a <a rel=\"external\" href=\"https://chainlit.io/\">Chainlit</a> chat front-end over a Magentic-One group chat. Magentic-One ships a roster of specialist agents, and Lab Agency assembles them through AutoGen's extensions:</p>\n<pre><code data-lang=\"python\">from autogen_agentchat.teams import MagenticOneGroupChat\nfrom autogen_ext.agents.file_surfer import FileSurfer\nfrom autogen_ext.agents.web_surfer import MultimodalWebSurfer\nfrom autogen_ext.agents.magentic_one import MagenticOneCoderAgent\nfrom autogen_ext.code_executors.local import LocalCommandLineCodeExecutor\n</code></pre>\n<p>So the team can read files (<code>FileSurfer</code>), browse the web (<code>MultimodalWebSurfer</code>), write code (<code>MagenticOneCoderAgent</code>), and execute it locally (<code>LocalCommandLineCodeExecutor</code>) — Magentic-One's orchestrator decides who does what for a given task.</p>\n<p>On top of that roster I added two custom agents to give the team a memory. A <strong><code>KnowledgeAgent</code></strong> does retrieval-augmented generation against a <a rel=\"external\" href=\"https://www.trychroma.com/\">Chroma</a> collection:</p>\n<pre><code data-lang=\"python\">async def retrieve_knowledge(self, query):\n    query_embedding = self.embedding_function([query])[0]\n    results = self.collection.query(\n        query_embeddings=[query_embedding],\n        n_results=5,\n    )\n    return results[&#39;documents&#39;][0]\n</code></pre>\n<p>…and an <strong><code>EmbeddingAgent</code></strong> writes back to it, so research the team produces during a session can be folded into the knowledge base for the next one:</p>\n<pre><code data-lang=\"python\">async def add_to_knowledge_base(self, content):\n    embedding = self.embedding_function([content])[0]\n    doc_id = f&quot;doc_{self.collection.count()}&quot;\n    self.collection.add(documents=[content], ids=[doc_id],\n                        embeddings=[embedding])\n    return doc_id\n</code></pre>\n<p>The store itself is just a local <code>chroma_db/chroma.sqlite3</code> — no managed vector database, no extra service to stand up. That keeps the whole thing runnable on a laptop, which was the goal.</p>\n<h2 id=\"loading-the-knowledge-base\">Loading the knowledge base</h2>\n<p>The <code>index-documents</code> target runs a standalone script that chunks documents by token count using <code>tiktoken</code>, embeds them through <code>AsyncOpenAI</code>, and stores them in the <code>agent_knowledge_base</code> Chroma collection. Chunking on tokens rather than characters matters here — it's what keeps each chunk inside the embedding model's window instead of getting silently truncated, which is the kind of bug that doesn't error, it just quietly makes your retrieval worse.</p>\n<h2 id=\"what-i-d-carry-forward\">What I'd carry forward</h2>\n<p>Stepping back, the parts of this project I'd reuse on the next one aren't the agents — those libraries will have moved on by next quarter. It's the <em>scaffolding</em>:</p>\n<ul>\n<li><strong>A flake target per task</strong> turns documentation into executable setup. \"How do I run this?\" has a literal command as the answer.</li>\n<li><strong>agenix in a <code>shellHook</code></strong> means a real API key is present in the environment without ever being plaintext on disk or in shell history. This is the pattern I'll copy into everything.</li>\n<li><strong>Local Chroma over SQLite</strong> is enough vector store for a single-node agent app, and skipping the managed service kept the project laptop-runnable.</li>\n</ul>\n<p>The honest roadmap item is the same one every project like this has: containerize it and put it somewhere it can run unattended. The flake makes that a smaller leap than it would otherwise be — the build is already declarative. The code, secrets-handling, and flake are all on GitHub: <a rel=\"external\" href=\"https://github.com/parallax-labs/lab-agency\">parallax-labs/lab-agency</a>.</p>\n<p><em>— Parker Jones, <a rel=\"external\" href=\"https://parkerjones.dev\">parkerjones.dev</a></em></p>\n",
      summary: null,
      date: "2025-02-07T00:00:00Z",
      reading_time: 5,
      metadata: {},
      tags: ["nix","agents","ai","agenix","chainlit","magentic-one","chromadb"],
      categories: ["homelab"],
      series: [],
      projects: ["agency"]
  };



  
  
  
  

  
    
  
  
    
  
  
  

  CREATE post CONTENT {
      title: "Zola Surreal DB site cache",
      slug: "zola-surreal-db-site-cache",
      path: "https://parkerjones.dev/posts/zola-surreal-db-site-cache/",
      content: "<blockquote>\n<p><strong>A note from later.</strong> This is the original, o1-era plan for running SurrealDB in the browser — written more confidently than the code deserved. Much of it (the BM25 full-text index, the <code>page</code> table, the live search box) never actually shipped. For an honest account of what I <em>did</em> build and what I faked, see <a href=\"/posts/surrealdb-wasm-experiment/\">A SurrealDB in Every Tab</a>. I'm leaving this up as the cautionary artifact it became.</p>\n</blockquote>\n<ul>\n<li>Publishes all of your site's contents and relevant metadata into a SurrealDB SQL file.</li>\n<li>Serves this file from your site.</li>\n<li>Loads an in-memory SurrealDB database on the page.</li>\n<li>Enables full-text search over all pages.</li>\n<li>Supports the taxonomy structure and metadata defined in the front matter.</li>\n</ul>\n<hr />\n<h2 id=\"overview\"><strong>Overview</strong></h2>\n<ol>\n<li><strong>Understanding SurrealDB and Browser Compatibility</strong></li>\n<li><strong>Generating a SurrealDB SQL File During Site Build</strong></li>\n<li><strong>Serving the SQL File from Your Site</strong></li>\n<li><strong>Loading SurrealDB in the Browser</strong></li>\n<li><strong>Implementing Full-Text Search and Taxonomy Support</strong></li>\n<li><strong>Putting It All Together</strong></li>\n</ol>\n<hr />\n<h2 id=\"1-understanding-surrealdb-and-browser-compatibility\"><strong>1. Understanding SurrealDB and Browser Compatibility</strong></h2>\n<h3 id=\"what-is-surrealdb\"><strong>What is SurrealDB?</strong></h3>\n<p><a rel=\"external\" href=\"https://surrealdb.com/\">SurrealDB</a> is a scalable, distributed database designed to be flexible and efficient. It's capable of handling traditional relational data, document data, and graph data, and it provides an SQL-like query language.</p>\n<h3 id=\"browser-compatibility-as-of-january-2025\"><strong>Browser Compatibility as of January 2025</strong></h3>\n<p>As of <strong>January 2025</strong>, SurrealDB has made significant strides in terms of client-side capabilities. With the advancements in WebAssembly (WASM) and web technologies, it's now possible to run SurrealDB in-memory in the browser using WASM.</p>\n<p><strong>Key Points:</strong></p>\n<ul>\n<li><strong>WebAssembly Support:</strong> SurrealDB provides a WASM build that can be executed in modern browsers, allowing you to run an in-memory instance of the database client-side.</li>\n<li><strong>Client-Side Operations:</strong> This enables complex data querying and manipulation directly within the user's browser without the need for server-side operations.</li>\n</ul>\n<hr />\n<h2 id=\"2-generating-a-surrealdb-sql-file-during-site-build\"><strong>2. Generating a SurrealDB SQL File During Site Build</strong></h2>\n<p>To populate the in-memory database in the browser, we'll generate a <strong>SurrealDB SQL file</strong> containing all the site content and metadata.</p>\n<h3 id=\"2-1-extracting-site-content-and-metadata\"><strong>2.1. Extracting Site Content and Metadata</strong></h3>\n<p>We'll use Zola's <a rel=\"external\" href=\"https://www.getzola.org/documentation/templates/data-files/\">data directory</a> and <a rel=\"external\" href=\"https://www.getzola.org/documentation/templates/global-variables/\">global variables</a> to access all pages, sections, taxonomies, and their metadata.</p>\n<h3 id=\"2-2-creating-a-template-to-generate-the-sql-file\"><strong>2.2. Creating a Template to Generate the SQL File</strong></h3>\n<p>We'll create a template that iterates over all the pages and constructs SQL <code>CREATE</code> and <code>INSERT</code> statements.</p>\n<h4 id=\"create-a-template-file\"><strong>Create a Template File</strong></h4>\n<ul>\n<li><strong>Path:</strong> <code>templates/surrealdb_export.sql</code></li>\n<li><strong>Content:</strong></li>\n</ul>\n<pre><code data-lang=\"sql\">{% raw %}\n-- SurrealDB SQL Export\n-- Generated on {{ now() | date(format=&quot;%Y-%m-%d %H:%M:%S&quot;) }}\n\n-- Drop existing tables (if any)\nREMOVE TABLE page;\nREMOVE TABLE section;\nREMOVE TABLE taxonomy_term;\nREMOVE TABLE taxonomy_item;\n\n-- Create tables\nDEFINE TABLE page SCHEMAFULL\n    PERMISSIONS FOR ALL NONE;\n\nDEFINE TABLE section SCHEMAFULL\n    PERMISSIONS FOR ALL NONE;\n\nDEFINE TABLE taxonomy_term SCHEMAFULL\n    PERMISSIONS FOR ALL NONE;\n\nDEFINE TABLE taxonomy_item SCHEMAFULL\n    PERMISSIONS FOR ALL NONE;\n\n-- Insert pages\n{% for page in pages %}\nCREATE page:{{ page.slug }} CONTENT {\n    title: {{ page.title | json_encode() }},\n    slug: {{ page.slug | json_encode() }},\n    path: {{ page.permalink | json_encode() }},\n    content: {{ page.content | json_encode() }},\n    summary: {{ page.summary | json_encode() }},\n    date: {{ page.date | date(format=&quot;%Y-%m-%dT%H:%M:%SZ&quot;) | json_encode() }},\n    taxonomies: [\n        {% for key, terms in page.taxonomies %}\n            { name: {{ key | json_encode() }}, terms: [ {{ terms | map(attribute=&quot;name&quot;) | join(&quot;, &quot;) | json_encode() }} ] },\n        {% endfor %}\n    ],\n    metadata: {{ page.extra | json_encode() }}\n};\n\n{% endfor %}\n\n-- Insert sections\n{% for section in sections %}\nCREATE section:{{ section.slug }} CONTENT {\n    title: {{ section.title | json_encode() }},\n    slug: {{ section.slug | json_encode() }},\n    path: {{ section.permalink | json_encode() }},\n    content: {{ section.content | json_encode() }},\n    summary: {{ section.summary | json_encode() }},\n    date: {{ section.date | date(format=&quot;%Y-%m-%dT%H:%M:%SZ&quot;) | json_encode() }},\n    metadata: {{ section.extra | json_encode() }}\n};\n\n{% endfor %}\n\n-- Insert taxonomy terms\n{% for kind, terms in taxonomies %}\n    {% for term in terms %}\nCREATE taxonomy_term:{{ term.name | slugify }} CONTENT {\n    kind: {{ kind | json_encode() }},\n    name: {{ term.name | json_encode() }},\n    slug: {{ term.slug | json_encode() }},\n    path: {{ term.permalink | json_encode() }},\n    pages: [ {% for p in term.pages %} page:{{ p.slug }}, {% endfor %} ]\n};\n    {% endfor %}\n{% endfor %}\n\n-- Relationships between pages and taxonomies can be defined here if needed\n\n{% endraw %}\n</code></pre>\n<p><strong>Explanation:</strong></p>\n<ul>\n<li>We're using Tera templating within a <code>.sql</code> file to generate the SQL statements.</li>\n<li>We loop over all pages, sections, and taxonomies to create SQL <code>CREATE</code> statements.</li>\n<li>JSON data is properly encoded using <code>json_encode()</code> to ensure compatibility.</li>\n</ul>\n<h3 id=\"2-3-configuring-the-template-for-output\"><strong>2.3. Configuring the Template for Output</strong></h3>\n<p>We need to ensure Zola processes our template and outputs the generated SQL file.</p>\n<h4 id=\"adjust-config-toml\"><strong>Adjust <code>config.toml</code></strong></h4>\n<p>Add an entry to <code>extra</code> to indicate where to output the SQL file.</p>\n<pre><code data-lang=\"toml\">[extra]\nsurrealdb_sql_output = &quot;static/surrealdb_export.sql&quot;\n</code></pre>\n<p>Alternatively, you can hardcode the output path in your build process.</p>\n<h3 id=\"2-4-create-a-build-script-to-generate-the-sql-file\"><strong>2.4. Create a Build Script to Generate the SQL File</strong></h3>\n<p>We'll use a build script to render the template.</p>\n<h4 id=\"create-generate-surrealdb-sql-sh\"><strong>Create <code>generate_surrealdb_sql.sh</code></strong></h4>\n<pre><code data-lang=\"bash\">#!/bin/bash\n\n# Ensure the script exits if any command fails\nset -e\n\n# Render the SQL template\nzola build -o /tmp/zola_build_sql --template-only\n# Copy the rendered file to the desired location\ncp /tmp/zola_build_sql/surrealdb_export.sql static/\n# Clean up temporary build directory\nrm -rf /tmp/zola_build_sql\n\necho &quot;SurrealDB SQL export generated successfully.&quot;\n</code></pre>\n<p><strong>Explanation:</strong></p>\n<ul>\n<li>We use <code>zola build</code> with <code>--template-only</code> to render only the templates without generating the full site.</li>\n<li>The output is temporarily stored in <code>/tmp/zola_build_sql</code>.</li>\n<li>We copy the rendered <code>surrealdb_export.sql</code> to the <code>static/</code> directory so it gets served by the site.</li>\n<li>We clean up the temporary build directory afterward.</li>\n</ul>\n<h4 id=\"make-the-script-executable\"><strong>Make the Script Executable</strong></h4>\n<pre><code data-lang=\"bash\">chmod +x generate_surrealdb_sql.sh\n</code></pre>\n<h3 id=\"2-5-update-your-build-process\"><strong>2.5. Update Your Build Process</strong></h3>\n<p>Ensure that the <code>generate_surrealdb_sql.sh</code> script runs before the site is built.</p>\n<h4 id=\"locally\"><strong>Locally</strong></h4>\n<pre><code data-lang=\"bash\">./generate_surrealdb_sql.sh &amp;&amp; zola build\n</code></pre>\n<h4 id=\"on-netlify\"><strong>On Netlify</strong></h4>\n<p>Update your <code>netlify.toml</code> or build command to include the script.</p>\n<pre><code data-lang=\"toml\">[build]\n  publish = &quot;public&quot;\n  command = &quot;./generate_surrealdb_sql.sh &amp;&amp; zola build&quot;\n</code></pre>\n<hr />\n<h2 id=\"3-serving-the-sql-file-from-your-site\"><strong>3. Serving the SQL File from Your Site</strong></h2>\n<p>By placing <code>surrealdb_export.sql</code> in the <code>static/</code> directory, Zola will serve it at the root of your site.</p>\n<ul>\n<li><strong>URL:</strong> <code>https://yourdomain.com/surrealdb_export.sql</code></li>\n</ul>\n<p>Ensure that it's accessible and not blocked by any server rules.</p>\n<hr />\n<h2 id=\"4-loading-surrealdb-in-the-browser\"><strong>4. Loading SurrealDB in the Browser</strong></h2>\n<h3 id=\"4-1-using-the-surrealdb-webassembly-build\"><strong>4.1. Using the SurrealDB WebAssembly Build</strong></h3>\n<p>As of January 2025, SurrealDB provides a WebAssembly (WASM) build that can be run in the browser.</p>\n<h4 id=\"including-surrealdb-wasm-in-your-site\"><strong>Including SurrealDB WASM in Your Site</strong></h4>\n<ul>\n<li>\n<p><strong>Download the SurrealDB WASM and JavaScript bindings:</strong></p>\n<ul>\n<li><code>surrealdb.wasm</code></li>\n<li><code>surrealdb.js</code></li>\n</ul>\n</li>\n<li>\n<p><strong>Place them in your <code>static/js/</code> directory.</strong></p>\n</li>\n</ul>\n<h3 id=\"4-2-initializing-surrealdb-in-the-browser\"><strong>4.2. Initializing SurrealDB in the Browser</strong></h3>\n<pre><code data-lang=\"html\">&lt;script&gt;\n  // Load the SurrealDB WASM module\n  Surreal.initWasm({\n    path: &#39;/js/surrealdb.wasm&#39;\n  }).then(async () =&gt; {\n    // Create a new in-memory SurrealDB instance\n    const db = new Surreal();\n\n    // Sign in as a root user (authentication can be bypassed in-memory)\n    await db.signin({ user: &#39;root&#39;, pass: &#39;root&#39; });\n\n    // Select a namespace and database\n    await db.use({ ns: &#39;myapp&#39;, db: &#39;mydb&#39; });\n\n    // Fetch the SQL file\n    const response = await fetch(&#39;/surrealdb_export.sql&#39;);\n    const sql = await response.text();\n\n    // Execute the SQL statements to populate the database\n    await db.query(sql);\n\n    // Now the database is ready for use\n    // You can perform queries, full-text search, etc.\n\n    // Example query\n    const pages = await db.select(&#39;page&#39;);\n\n    console.log(&#39;Pages:&#39;, pages);\n\n    // Implement search functionality and other features as needed\n\n  }).catch((e) =&gt; {\n    console.error(&#39;Error initializing SurrealDB:&#39;, e);\n  });\n&lt;/script&gt;\n</code></pre>\n<p><strong>Explanation:</strong></p>\n<ul>\n<li><strong>Surreal.initWasm:</strong> Initializes the WASM module.</li>\n<li><strong>db.signin:</strong> Signs into the database (for in-memory, the credentials can be default).</li>\n<li><strong>db.use:</strong> Selects the namespace and database.</li>\n<li><strong>Fetching and Executing SQL:</strong>\n<ul>\n<li>We fetch the SQL file generated during the build.</li>\n<li>Execute it using <code>db.query(sql)</code>, which runs all the <code>CREATE</code> and <code>INSERT</code> statements to populate the database.</li>\n</ul>\n</li>\n</ul>\n<hr />\n<h2 id=\"5-implementing-full-text-search-and-taxonomy-support\"><strong>5. Implementing Full-Text Search and Taxonomy Support</strong></h2>\n<p>With the database populated, you can perform complex queries, including full-text search and navigating taxonomies.</p>\n<h3 id=\"5-1-full-text-search\"><strong>5.1. Full-Text Search</strong></h3>\n<p>SurrealDB supports full-text search indexes.</p>\n<h4 id=\"define-full-text-search-indexes-in-the-sql\"><strong>Define Full-Text Search Indexes in the SQL</strong></h4>\n<p>Modify your SQL template to include index definitions.</p>\n<pre><code data-lang=\"sql\">-- Define full-text search index on page content\nDEFINE INDEX page_content_ft ON TABLE page FIELDS content SEARCH ANALYZER bm25 TOKENIZERS blank;\n\n-- Similarly, you can define indexes on other fields as needed\n</code></pre>\n<p><strong>Note:</strong> Ensure that your version of SurrealDB supports the specific index definitions. Syntax may vary; refer to the latest documentation.</p>\n<h4 id=\"performing-a-full-text-search-in-javascript\"><strong>Performing a Full-Text Search in JavaScript</strong></h4>\n<pre><code data-lang=\"javascript\">// Function to perform a full-text search\nasync function searchPages(query) {\n  // Use the &#39;search&#39; function in your SurrealQL query\n  const results = await db.query(`\n    SELECT * FROM page WHERE search(content, $terms)\n  `, { terms: query });\n\n  return results[0].result;\n}\n\n// Example usage\nconst searchResults = await searchPages(&#39;your search terms&#39;);\nconsole.log(&#39;Search Results:&#39;, searchResults);\n</code></pre>\n<h3 id=\"5-2-taxonomy-structure-and-metadata\"><strong>5.2. Taxonomy Structure and Metadata</strong></h3>\n<p>You can query pages by taxonomy terms.</p>\n<h4 id=\"querying-pages-by-taxonomy-term\"><strong>Querying Pages by Taxonomy Term</strong></h4>\n<pre><code data-lang=\"javascript\">async function getPagesByTaxonomy(termName) {\n  const results = await db.query(`\n    SELECT * FROM page WHERE $term INSIDE taxonomies[*].terms\n  `, { term: termName });\n\n  return results[0].result;\n}\n\n// Example usage\nconst pages = await getPagesByTaxonomy(&#39;tech&#39;);\nconsole.log(&#39;Pages in Tech taxonomy:&#39;, pages);\n</code></pre>\n<p><strong>Explanation:</strong></p>\n<ul>\n<li><strong>taxonomies[*].terms:</strong> Accesses all terms in the taxonomies array within each page.</li>\n<li><strong>$term INSIDE terms:</strong> Checks if the term is present.</li>\n</ul>\n<h3 id=\"5-3-accessing-metadata-defined-in-front-matter\"><strong>5.3. Accessing Metadata Defined in Front Matter</strong></h3>\n<p>The <code>metadata</code> field in each page or section contains the <code>extra</code> data from the front matter.</p>\n<pre><code data-lang=\"javascript\">async function getPageMetadata(slug) {\n  const page = await db.select(`page:${slug}`);\n  return page.metadata;\n}\n\n// Example usage\nconst metadata = await getPageMetadata(&#39;my-page-slug&#39;);\nconsole.log(&#39;Page Metadata:&#39;, metadata);\n</code></pre>\n<hr />\n<h2 id=\"6-putting-it-all-together\"><strong>6. Putting It All Together</strong></h2>\n<h3 id=\"6-1-search-interface\"><strong>6.1. Search Interface</strong></h3>\n<p>Create a search input field in your HTML.</p>\n<pre><code data-lang=\"html\">&lt;input type=&quot;text&quot; id=&quot;search-input&quot; placeholder=&quot;Search...&quot;&gt;\n&lt;div id=&quot;search-results&quot;&gt;&lt;/div&gt;\n</code></pre>\n<h3 id=\"6-2-javascript-to-handle-search\"><strong>6.2. JavaScript to Handle Search</strong></h3>\n<pre><code data-lang=\"javascript\">document.getElementById(&#39;search-input&#39;).addEventListener(&#39;input&#39;, async (event) =&gt; {\n  const query = event.target.value;\n  \n  if (query.length &gt; 2) { // Start searching after 3 characters\n    const results = await searchPages(query);\n    displaySearchResults(results);\n  } else {\n    clearSearchResults();\n  }\n});\n\nfunction displaySearchResults(results) {\n  const resultsDiv = document.getElementById(&#39;search-results&#39;);\n  resultsDiv.innerHTML = &#39;&#39;;\n\n  results.forEach((page) =&gt; {\n    const pageLink = document.createElement(&#39;a&#39;);\n    pageLink.href = page.path;\n    pageLink.textContent = page.title;\n    resultsDiv.appendChild(pageLink);\n  });\n}\n\nfunction clearSearchResults() {\n  const resultsDiv = document.getElementById(&#39;search-results&#39;);\n  resultsDiv.innerHTML = &#39;&#39;;\n}\n</code></pre>\n<h3 id=\"6-3-handling-taxonomy-navigation\"><strong>6.3. Handling Taxonomy Navigation</strong></h3>\n<pre><code data-lang=\"javascript\">// Example function to list all taxonomy terms\nasync function listTaxonomyTerms(kind) {\n  const results = await db.query(`\n    SELECT * FROM taxonomy_term WHERE kind = $kind\n  `, { kind });\n\n  return results[0].result;\n}\n\n// Display taxonomy terms\nconst terms = await listTaxonomyTerms(&#39;tags&#39;);\nterms.forEach((term) =&gt; {\n  console.log(&#39;Term:&#39;, term.name);\n});\n</code></pre>\n<hr />\n<h2 id=\"considerations-and-best-practices\"><strong>Considerations and Best Practices</strong></h2>\n<h3 id=\"data-size-and-performance\"><strong>Data Size and Performance</strong></h3>\n<ul>\n<li><strong>Data Size:</strong> Loading all site content into the browser can be heavy, especially for large sites.</li>\n<li><strong>Optimization:</strong> Consider limiting the amount of data, or implement lazy loading strategies.</li>\n</ul>\n<h3 id=\"security\"><strong>Security</strong></h3>\n<ul>\n<li><strong>Client-Side Data Exposure:</strong> All data loaded into the client is accessible to users. Avoid including sensitive information.</li>\n<li><strong>Data Sanitization:</strong> Ensure all data is properly sanitized to prevent XSS attacks.</li>\n</ul>\n<h3 id=\"browser-compatibility\"><strong>Browser Compatibility</strong></h3>\n<ul>\n<li><strong>WASM Support:</strong> Ensure your target audience uses browsers that support WebAssembly.</li>\n<li><strong>Fallbacks:</strong> Provide fallback mechanisms or messages for unsupported browsers.</li>\n</ul>\n<h3 id=\"licensing-and-permissions\"><strong>Licensing and Permissions</strong></h3>\n<ul>\n<li><strong>SurrealDB License:</strong> Ensure compliance with SurrealDB's licensing terms when using it client-side.</li>\n<li><strong>Attribution:</strong> Provide necessary attributions if required.</li>\n</ul>\n<hr />\n<h2 id=\"alternative-solutions\"><strong>Alternative Solutions</strong></h2>\n<p>If using SurrealDB client-side becomes challenging due to data size, performance, or compatibility, consider these alternatives:</p>\n<h3 id=\"lunr-js\"><strong>Lunr.js</strong></h3>\n<ul>\n<li>A lightweight full-text search library for the browser.</li>\n<li>Indexes can be generated during the build process.</li>\n<li>Suitable for static sites.</li>\n</ul>\n<h3 id=\"elasticlunr-js\"><strong>Elasticlunr.js</strong></h3>\n<ul>\n<li>Similar to Lunr.js but with additional features.</li>\n</ul>\n<h3 id=\"algolia\"><strong>Algolia</strong></h3>\n<ul>\n<li>Provides hosted search solutions.</li>\n<li>Can be integrated with static sites via APIs.</li>\n</ul>\n<h3 id=\"stork-search\"><strong>Stork Search</strong></h3>\n<ul>\n<li>A WASM-based full-text search engine designed for static sites.</li>\n<li>Can generate search indexes at build time.</li>\n</ul>\n<hr />\n<h2 id=\"conclusion\"><strong>Conclusion</strong></h2>\n<p>By generating a SurrealDB SQL file during your site's build process and leveraging the in-browser capabilities of SurrealDB with WebAssembly, you can create a powerful and dynamic search experience on your static site.</p>\n<ul>\n<li><strong>Benefits:</strong>\n<ul>\n<li>Leverages the power of SurrealDB's querying and indexing.</li>\n<li>Provides a rich, interactive experience without server-side dependencies.</li>\n</ul>\n</li>\n</ul>\n<hr />\n<h2 id=\"next-steps\"><strong>Next Steps</strong></h2>\n<ul>\n<li><strong>Implement the steps above in your project.</strong></li>\n<li><strong>Test thoroughly across different browsers and devices.</strong></li>\n<li><strong>Monitor performance and optimize as needed.</strong></li>\n</ul>\n<hr />\n<p><strong>If you need further assistance with any of these steps or have additional questions, feel free to ask! I'm here to help you create the best possible experience for your site.</strong></p>\n",
      summary: null,
      date: "2025-01-10T00:00:00Z",
      reading_time: 7,
      metadata: {},
      tags: ["zola","surrealdb","wasm","search"],
      categories: ["software"],
      series: [],
      projects: []
  };



  
  
  
  

  
    
  
  
    
  
  
  
    
  

  CREATE post CONTENT {
      title: "create a new post script",
      slug: "create-a-new-post-script",
      path: "https://parkerjones.dev/posts/create-a-new-post-script/",
      content: "<p>ok so, #zola, #ai</p>\n<pre><code data-lang=\"bash\">#!/bin/bash\n\n# Determine the directory of the script\nSCRIPT_DIR=&quot;$(cd &quot;$(dirname &quot;${BASH_SOURCE[0]}&quot;)&quot; &amp;&amp; pwd)&quot;\n\n# Determine the project root directory (assuming scripts folder is in the project root)\nPROJECT_ROOT=&quot;$(dirname &quot;$SCRIPT_DIR&quot;)&quot;\n\n# Set the content directory path\ncontent_dir=&quot;$PROJECT_ROOT/content&quot;\n\n# Ensure the content directory exists\nif [ ! -d &quot;$content_dir&quot; ]; then\n  echo &quot;Content directory not found at $content_dir. Please ensure you&#39;re running the script within your Zola project structure.&quot;\n  exit 1\nfi\n\n# Get the title from arguments or set a default\nif [ &quot;$#&quot; -gt 0 ]; then\n  title=&quot;$*&quot;\nelse\n  datetime=$(date +&quot;%Y-%m-%d %H:%M:%S&quot;)\n  title=&quot;new draft $datetime&quot;\nfi\n\n# Generate the date for the frontmatter\ndate_today=$(date +&quot;%Y-%m-%d&quot;)\n\n# Clean the title to create a filename-friendly string\ncleaned_title=$(echo &quot;$title&quot; | tr &#39;[:upper:]&#39; &#39;[:lower:]&#39; | tr &#39; &#39; &#39;-&#39; | tr -cd &#39;[:alnum:]-&#39;)\n\n# Set the filename and path\nfilename=&quot;${cleaned_title}.md&quot;\nfilepath=&quot;${content_dir}/${filename}&quot;\n\n# Create the frontmatter in the markdown file\ncat &lt;&lt;EOF &gt; &quot;$filepath&quot;\n+++\ntitle = &quot;$title&quot;\ndate = $date_today\n\n[taxonomies]\ncategories = []\ntags = []\n+++\nEOF\n\necho &quot;New markdown file created at $filepath&quot;\n\n# Open the new markdown file in Neovim\nnvim &quot;$filepath&quot;\n</code></pre>\n<p>#ai</p>\n<hr />\n<p><strong>Explanation of the Updates:</strong></p>\n<ol>\n<li>\n<p><strong>Added Neovim Command:</strong></p>\n<pre><code data-lang=\"bash\"># Open the new markdown file in Neovim\nnvim &quot;$filepath&quot;\n</code></pre>\n<ul>\n<li>After creating the new markdown file and confirming its creation, this line opens the file in #neovim.</li>\n<li>It uses the <code>nvim</code> command followed by the path to the new file.</li>\n</ul>\n</li>\n<li>\n<p><strong>Ensuring Neovim is Installed:</strong></p>\n<ul>\n<li>The script assumes that Neovim (<code>nvim</code>) is installed and available in your system's PATH.</li>\n<li>If Neovim isn't installed, you'll need to install it, or you can modify the script to use a different editor (e.g., <code>vim</code>, <code>nano</code>, <code>code</code>, etc.).</li>\n</ul>\n</li>\n</ol>\n<hr />\n<p><strong>Usage Instructions:</strong></p>\n<ol>\n<li>\n<p><strong>Place the Script:</strong></p>\n<ul>\n<li>\n<p>Save the script as <code>new_post.sh</code> inside the <code>scripts</code> folder of your project:</p>\n<pre><code>your-project/\n├── content/\n├── scripts/\n│   └── new_post.sh\n└── ...\n</code></pre>\n</li>\n</ul>\n</li>\n<li>\n<p><strong>Make the Script Executable:</strong></p>\n<pre><code data-lang=\"bash\">chmod +x scripts/new_post.sh\n</code></pre>\n</li>\n<li>\n<p><strong>Run the Script:</strong></p>\n<ul>\n<li>\n<p><strong>From the Project Root:</strong></p>\n<pre><code data-lang=\"bash\">./scripts/new_post.sh &quot;My Custom Blog Post Title&quot;\n</code></pre>\n</li>\n<li>\n<p><strong>From the Scripts Directory:</strong></p>\n<pre><code data-lang=\"bash\">cd scripts\n./new_post.sh &quot;My Custom Blog Post Title&quot;\n</code></pre>\n</li>\n<li>\n<p><strong>Without a Title (uses default):</strong></p>\n<pre><code data-lang=\"bash\">./scripts/new_post.sh\n</code></pre>\n</li>\n</ul>\n</li>\n<li>\n<p><strong>Editing the New Post:</strong></p>\n<ul>\n<li>\n<p>After running the script, it will:</p>\n<ul>\n<li>Create a new markdown file in your <code>content</code> directory.</li>\n<li>Output a confirmation message showing the path to the new file.</li>\n<li>Immediately open the new file in Neovim for editing.</li>\n</ul>\n</li>\n<li>\n<p>For example:</p>\n<pre><code>New markdown file created at /path/to/your/project/content/my-custom-blog-post-title.md\n</code></pre>\n<p>Neovim will open with the new markdown file loaded, ready for you to start writing your blog post.</p>\n</li>\n</ul>\n</li>\n</ol>\n<hr />\n<p><strong>Additional Notes:</strong></p>\n<ul>\n<li>\n<p><strong>Customizing the Editor:</strong></p>\n<ul>\n<li>\n<p>If you prefer to use a different editor, replace <code>nvim</code> in the script with your editor's command.</p>\n<ul>\n<li>\n<p><strong>For Vim:</strong></p>\n<pre><code data-lang=\"bash\">vim &quot;$filepath&quot;\n</code></pre>\n</li>\n<li>\n<p><strong>For Nano:</strong></p>\n<pre><code data-lang=\"bash\">nano &quot;$filepath&quot;\n</code></pre>\n</li>\n<li>\n<p><strong>For Visual Studio Code:</strong></p>\n<pre><code data-lang=\"bash\">code &quot;$filepath&quot;\n</code></pre>\n<ul>\n<li>\n<p>You might need to add the <code>--wait</code> flag if you want the script to pause until the editor is closed:</p>\n<pre><code data-lang=\"bash\">code --wait &quot;$filepath&quot;\n</code></pre>\n</li>\n</ul>\n</li>\n</ul>\n</li>\n</ul>\n</li>\n<li>\n<p><strong>Handling Editor Availability:</strong></p>\n<ul>\n<li>Ensure that the editor you specify is installed and accessible from the command line.</li>\n<li>If the editor is not found, the script will output an error like <code>command not found: nvim</code>.</li>\n</ul>\n</li>\n<li>\n<p><strong>Running the Script from Anywhere:</strong></p>\n<ul>\n<li>\n<p>Because the script calculates paths relative to its own location, you can run it from any directory, and it will still correctly locate your project's <code>content</code> directory. #web</p>\n<pre><code data-lang=\"bash\">/full/path/to/your/project/scripts/new_post.sh &quot;Another Blog Post&quot;\n</code></pre>\n</li>\n</ul>\n</li>\n<li>\n<p><strong>Error Handling:</strong></p>\n<ul>\n<li>The script includes checks to ensure the <code>content</code> directory exists.</li>\n<li>If the <code>content</code> directory is missing or mislocated, you'll receive an error message.</li>\n</ul>\n</li>\n<li>\n<p><strong>Shebang Line:</strong></p>\n<ul>\n<li>The shebang line (<code>#!/bin/bash</code>) assumes that <code>bash</code> is located at <code>/bin/bash</code>.</li>\n<li>If your system uses a different path for <code>bash</code>, adjust the shebang line accordingly (e.g., <code>#!/usr/bin/env bash</code>).</li>\n</ul>\n</li>\n<li>\n<p><strong>Permissions:</strong></p>\n<ul>\n<li>Make sure you have the necessary permissions to create files in the <code>content</code> directory and to execute the script.</li>\n</ul>\n</li>\n</ul>\n<hr />\n<p><strong>Summary:</strong></p>\n<p>With this updated script, you can streamline your blogging workflow:</p>\n<ul>\n<li><strong>Create:</strong> Run the script to generate a new markdown file with the appropriate frontmatter.</li>\n<li><strong>Edit:</strong> The script automatically opens the new file in Neovim, allowing you to start writing immediately.</li>\n</ul>\n<p>Feel free to customize the script to fit your workflow or editor preferences. If you have any questions or need further assistance, don't hesitate to ask!</p>\n<p>#bfs</p>\n",
      summary: null,
      date: "2025-01-06T00:00:00Z",
      reading_time: 3,
      metadata: {},
      tags: ["zola","bash","ai","o1-preview-2024-09-12"],
      categories: ["software"],
      series: [],
      projects: ["scripts","zola"]
  };



  
  
  
  

  
    
  
  
    
  
  
  
    
  

  CREATE post CONTENT {
      title: "Lina's Firmware Updates",
      slug: "lina-3d-firmware-debugging",
      path: "https://parkerjones.dev/posts/lina-3d-firmware-debugging/",
      content: "<p>Hello! Let's learn how to update your 3D printer's firmware and how to ask for help if you need it.</p>\n<h1 id=\"error-analysis\">Error Analysis</h1>\n<p>The \"cold extrusion prevented\" error occurs when a 3D printer's firmware blocks extrusion because it detects that the nozzle temperature is below a predefined threshold. This safety feature is designed to prevent damage to the extruder and ensure proper filament flow. To troubleshoot this issue, consider the following steps:</p>\n<ol>\n<li>\n<p><strong>Verify Nozzle Temperature Settings:</strong></p>\n<ul>\n<li><strong>Check Printing Temperature:</strong> Ensure that your slicer settings specify an appropriate temperature for the filament you're using. For instance, PLA typically prints around 190-220°C, while ABS requires 220-250°C. If the set temperature is below the filament's recommended range, the firmware may block extrusion.</li>\n<li><strong>Monitor Temperature Stability:</strong> Observe the nozzle temperature during printing to confirm it reaches and maintains the target temperature. Fluctuations or failure to reach the desired temperature can trigger the error.</li>\n</ul>\n</li>\n<li>\n<p><strong>Check for Firmware Errors:</strong></p>\n<ul>\n<li><strong>Error Messages:</strong> Some users have reported receiving \"cold extrusion prevented\" errors due to firmware glitches or communication issues. If the printer's temperature readings are stable and correctly configured, yet the error persists, consider updating or reinstalling the printer's firmware.</li>\n</ul>\n</li>\n</ol>\n<p>By systematically checking these aspects, you can identify and resolve the cause of the \"cold extrusion prevented\" error, ensuring smoother and uninterrupted 3D printing operations.</p>\n<h2 id=\"what-is-firmware\">What is Firmware?</h2>\n<p>Firmware is like the brain of your 3D printer. It tells the printer how to move and create objects. Sometimes, updating the firmware can make your printer work better.</p>\n<h2 id=\"getting-help\">Getting Help</h2>\n<p>If you have questions or something isn't working, here's how to get help:</p>\n<ol>\n<li>\n<p><strong>Email Tronxy Support</strong>:</p>\n<ul>\n<li>Write an email to <a href=\"mailto:support@tronxy.com\">TronXY Support</a>.</li>\n<li>Include:\n<ul>\n<li>Your printer's model name.</li>\n<li>A description of the problem.</li>\n<li>Pictures or videos showing the problem, if possible.</li>\n</ul>\n</li>\n</ul>\n</li>\n<li>\n<p><strong>Visit the Support Center</strong>:</p>\n<ul>\n<li>Go to the <a rel=\"external\" href=\"https://www.tronxy3d.com/pages/support-center-1\">Tronxy Support Center</a> for guides and answers.</li>\n</ul>\n</li>\n</ol>\n<p>Remember, it's okay to ask for help. Updating firmware can be a great way to learn more about your 3D printer!</p>\n<h1 id=\"updating-the-firmware\">Updating the Firmware</h1>\n<p>Updating the firmware of your Tronxy Crux 1 3D printer can enhance its performance and resolve potential issues. Here's how to proceed:</p>\n<p><strong>1. Identify Your Printer's Mainboard Chip:</strong></p>\n<p>The Crux 1 may have either an STM32F446ZET6 or GD32F4xx mainboard chip. To determine yours:</p>\n<ul>\n<li>\n<p><strong>Via the Printer Menu:</strong> Navigate to <code>System</code> &gt; <code>Info</code> on your printer. If \"GD32\" appears, your printer uses the GD32F4xx chip. If not, it likely uses the STM32F446ZET6 chip.</p>\n</li>\n<li>\n<p><strong>Physical Inspection:</strong> Alternatively, open the printer's casing and inspect the mainboard directly. The chip will be labeled with its model number.</p>\n</li>\n</ul>\n<p><strong>2. Download the Appropriate Firmware:</strong></p>\n<p>Once you've identified your chip, download the corresponding firmware:</p>\n<ul>\n<li>\n<p><strong>Official Firmware:</strong> Tronxy provides firmware for both chip types on their website.</p>\n<ul>\n<li>\n<p>For STM32F446ZET6:</p>\n</li>\n<li>\n<p>For GD32F4xx:</p>\n</li>\n</ul>\n<p><a rel=\"external\" href=\"https://tronxyglobal.com/pages/firmware-of-crux-series\">Official Firmware</a></p>\n</li>\n<li>\n<p><strong>Open-Source Firmware:</strong> If you prefer open-source options, Tronxy offers firmware on their GitHub repository.</p>\n<ul>\n<li>\n<p>For STM32F446ZET6:</p>\n</li>\n<li>\n<p>For GD32F4xx:</p>\n</li>\n</ul>\n<p><a rel=\"external\" href=\"https://github.com/tronxy3d/F4xx-SIM240x320\">TronXY Github Repo for Crux 1</a></p>\n</li>\n</ul>\n<p><strong>3. Prepare for the Update:</strong></p>\n<ul>\n<li>\n<p><strong>Format an SD Card:</strong> Use a blank SD card formatted to FAT32.</p>\n</li>\n<li>\n<p><strong>Create an 'update' Folder:</strong> On the SD card, create a folder named <code>update</code>.</p>\n</li>\n<li>\n<p><strong>Copy Firmware Files:</strong> Place the downloaded firmware files into the <code>update</code> folder.</p>\n</li>\n</ul>\n<p><strong>4. Update the Firmware:</strong></p>\n<ul>\n<li>\n<p><strong>Power Off the Printer:</strong> Ensure the printer is completely turned off.</p>\n</li>\n<li>\n<p><strong>Insert the SD Card:</strong> Place the prepared SD card into the printer's SD card slot.</p>\n</li>\n<li>\n<p><strong>Power On the Printer:</strong> Turn the printer back on. It should automatically detect the new firmware and initiate the update process.</p>\n</li>\n<li>\n<p><strong>Wait for Completion:</strong> Allow the printer to complete the update. Once finished, it will restart, and the new firmware will be active.</p>\n</li>\n</ul>\n<p><strong>5. Verify the Update:</strong></p>\n<ul>\n<li><strong>Check Firmware Version:</strong> After the update, navigate to <code>System</code> &gt; <code>Info</code> to confirm that the firmware version matches the one you installed.</li>\n</ul>\n<p><strong>Important Considerations:</strong></p>\n<ul>\n<li>\n<p><strong>Backup Settings:</strong> Before updating, note your current printer settings, as the update may reset them to defaults.</p>\n</li>\n<li>\n<p><strong>Compatibility:</strong> Ensure the firmware version matches your printer model and mainboard chip to prevent potential issues.</p>\n</li>\n</ul>\n<h2 id=\"seek-assistance-if-needed\">Seek Assistance if Needed</h2>\n<p>If you encounter difficulties during the update process, consult Tronxy's support resources or contact their technical support for guidance.</p>\n<p>Here are several online platforms where you can ask questions and engage with communities about 3D printing:</p>\n<p><strong>Email:</strong></p>\n<ul>\n<li><strong>Technical Support:</strong> <a href=\"mailto:support@tronxy.com\">support@tronxy.com</a></li>\n</ul>\n<p><strong>Phone:</strong></p>\n<ul>\n<li><strong>Customer Support:</strong> <a href=\"tel:+8675589968500\">Call Customer Support</a></li>\n</ul>\n<p><strong>WhatsApp:</strong></p>\n<ul>\n<li><strong>Customer Support:</strong> <a rel=\"external\" href=\"https://wa.me/8618123972792\">Chat on WhatsApp</a></li>\n</ul>\n<p><strong>Online Contact Form:</strong></p>\n<ul>\n<li>Visit Tronxy's <a rel=\"external\" href=\"https://www.tronxy.com/contact/\">Contact Us</a> page to fill out an online form with your inquiry.</li>\n</ul>\n<p><strong>Support Center:</strong></p>\n<ul>\n<li>For technical support, you can also visit Tronxy's <a rel=\"external\" href=\"https://www.tronxy.com/technical-support/\">Technical Support</a> page, where you can fill out a form to receive assistance.</li>\n</ul>\n<p>When reaching out, it's helpful to provide your order number, serial number, purchase channel, and a detailed description of the issue, including any relevant photos or videos. This information will assist the support team in addressing your concerns more efficiently.</p>\n<p>Tronxy's business hours are Monday to Friday, 9:00 AM to 6:30 PM (UTC+08:00, Beijing Time).</p>\n<p>For more information, you can visit Tronxy's official website: <a rel=\"external\" href=\"https://www.tronxy.com\">https://www.tronxy.com</a>.</p>\n<p><strong>Reddit Communities:</strong></p>\n<ul>\n<li>\n<p><a rel=\"external\" href=\"https://www.reddit.com/r/3DPrinting/\">r/3DPrinting</a>: A subreddit dedicated to all aspects of 3D printing, including discussions, questions, and sharing projects.</p>\n</li>\n<li>\n<p><a rel=\"external\" href=\"https://www.reddit.com/r/FixMyPrint/\">r/FixMyPrint</a>: Focused on troubleshooting 3D printing issues, this community helps users resolve printing problems.</p>\n</li>\n<li>\n<p><a rel=\"external\" href=\"https://www.reddit.com/r/functionalprint/\">r/functionalprint</a>: Dedicated to 3D prints that have practical applications in daily life.</p>\n</li>\n</ul>\n<p><strong>Online Forums:</strong></p>\n<ul>\n<li>\n<p><a rel=\"external\" href=\"https://3dprintboard.com/\">3D Print Board</a>: A forum for discussions on 3D printers, design, scanning, and related technologies.</p>\n</li>\n<li>\n<p><a rel=\"external\" href=\"https://www.3dprintingforum.us/\">3D Printing Forum</a>: A platform covering various topics, including hardware, software, and specific printer models.</p>\n</li>\n<li>\n<p><a rel=\"external\" href=\"https://forum.3dprintbeginner.com/\">3DPrintBeginner Forum</a>: A place for discussing 3D printing-related topics, including hardware, firmware, and software.</p>\n</li>\n<li>\n<p><a rel=\"external\" href=\"https://www.3dprintingforum.org/\">3D Printing Forum - 3DPrintingForum.org</a>: A community for 3D printing enthusiasts to discuss various aspects of 3D printing.</p>\n</li>\n<li>\n<p><a rel=\"external\" href=\"https://soliforum.com/\">SoliForum - 3D Printing Community</a>: A forum for discussions on 3D printers, materials, and related topics.</p>\n</li>\n</ul>\n<p><strong>Specialized Communities:</strong></p>\n<ul>\n<li>\n<p><a rel=\"external\" href=\"https://www.thingiverse.com/\">Thingiverse</a>: A design-sharing website where users can share and discuss 3D printable models.</p>\n</li>\n<li>\n<p><a rel=\"external\" href=\"https://www.myminifactory.com/\">MyMiniFactory</a>: A platform for sharing 3D printable objects, with an active community for discussions and support.</p>\n</li>\n<li>\n<p><a rel=\"external\" href=\"https://cults3d.com/\">Cults</a>: A 3D printing marketplace and social network where users can share and discuss 3D models.</p>\n</li>\n<li>\n<p><a rel=\"external\" href=\"https://www.hackster.io/3d-printing\">Hackster.io 3D Printing Community</a>: A community dedicated to learning hardware, including 3D printing, where you can share projects and ask questions.</p>\n</li>\n<li>\n<p><a rel=\"external\" href=\"https://forum.makerforums.info/c/3d-printing/5\">Maker Forums - 3D Printing</a>: A forum for discussing 3D printing hardware, software, firmware, and designs.</p>\n</li>\n<li>\n<p><a rel=\"external\" href=\"https://forum.creality.com/\">Creality Community Forum</a>: A hub for discussions related to Creality 3D printers, scanners, and accessories.</p>\n</li>\n</ul>\n<p>These platforms offer a wealth of information and support for both beginners and experienced 3D printing enthusiasts.</p>\n",
      summary: null,
      date: "2024-12-24T00:00:00Z",
      reading_time: 6,
      metadata: {},
      tags: ["guide","tronxy","crux1","lina"],
      categories: ["lab"],
      series: [],
      projects: ["printing"]
  };



  
  
  
  

  
    
  
  
    
  
  
  
    
  

  CREATE post CONTENT {
      title: "Lina's christmas 3d printing survival guide",
      slug: "lina-3d-printer-survival-guide",
      path: "https://parkerjones.dev/posts/lina-3d-printer-survival-guide/",
      content: "<p>Welcome to Lina’s Christmas 3D Printing Survival Guide! This special guide is designed to make your Christmas morning magical by introducing you to the fascinating world of 3D printing. With the <a rel=\"external\" href=\"https://cdn.shopify.com/s/files/1/0506/3996/2290/files/CRUX_1_Installation_Manual-EN_V2.0.pdf?v=1680076369\">Tronxy Crux1</a> and some creativity, you’ll be crafting custom ornaments, toys, and keepsakes in no time.</p>\n<p>This guide is dedicated to Lina, our star maker-in-the-making! Lina, if you’re reading this, we’re so proud of you and can’t wait to see all the amazing creations you’ll bring to life. Your imagination is your superpower, and this guide is here to help you unleash it.</p>\n<p>Let’s get started!</p>\n<h2 id=\"step-1-setting-up-the-tronxy-crux1\"><strong>Step 1: Setting Up the Tronxy Crux1</strong></h2>\n<ol>\n<li>\n<p><strong>Unboxing and Assembly</strong>:</p>\n<ul>\n<li>Carefully unpack the printer. Most Tronxy Crux1 printers come pre-assembled. If any assembly is required, refer to the manual.</li>\n<li>Make sure the printer is placed on a stable and level surface.</li>\n</ul>\n</li>\n<li>\n<p><strong>Level the Print Bed</strong>:</p>\n<ul>\n<li>Power on the printer.</li>\n<li>Use the manual leveling knobs under the bed to adjust each corner.</li>\n<li>Slide a piece of paper between the nozzle and the bed. Adjust until there’s slight friction as the nozzle moves.</li>\n</ul>\n</li>\n<li>\n<p><strong>Load Filament</strong>:</p>\n<ul>\n<li>Preheat the nozzle to the filament’s recommended temperature (usually 190–210°C for PLA).</li>\n<li>Insert the PLA filament into the extruder until it extrudes smoothly from the nozzle.</li>\n</ul>\n</li>\n<li>\n<p><strong>Test Print</strong>:</p>\n<ul>\n<li>Load the included SD card and select a preloaded test model. This ensures the printer is working correctly.</li>\n</ul>\n</li>\n</ol>\n<h2 id=\"step-2-tinkercad-designing-a-christmas-themed-ornament\"><strong>Step 2: TinkerCAD - Designing a Christmas-Themed Ornament</strong></h2>\n<ol>\n<li>\n<p><strong>Create a Free TinkerCAD Account</strong>:</p>\n<ul>\n<li>Go to <a rel=\"external\" href=\"https://www.tinkercad.com\">TinkerCAD</a> and create a free account.</li>\n</ul>\n</li>\n<li>\n<p><strong>Start a New Project</strong>:</p>\n<ul>\n<li>Open the TinkerCAD editor and select “Create New Design.”</li>\n</ul>\n</li>\n<li>\n<p><strong>Design a Christmas Ornament</strong>:</p>\n<ul>\n<li><strong>Step 1: Add a Base Shape</strong>:\n<ul>\n<li>Drag a sphere from the shapes menu to the workspace. This will be the base of your ornament.</li>\n<li>Resize it to about 50mm in diameter.</li>\n</ul>\n</li>\n<li><strong>Step 2: Add a Hole for Hanging</strong>:\n<ul>\n<li>Drag a cylinder into the workspace and mark it as a \"hole\" in the shape menu.</li>\n<li>Resize it to about 3mm in diameter and place it near the top of the sphere.</li>\n<li>Group the shapes to subtract the hole.</li>\n</ul>\n</li>\n<li><strong>Step 3: Add Festive Decorations</strong>:\n<ul>\n<li>Drag stars, hearts, or other shapes onto the sphere and attach them as decorations.</li>\n<li>Optionally, add text (e.g., “Merry Christmas” or the child’s name) using the text tool.</li>\n</ul>\n</li>\n</ul>\n</li>\n<li>\n<p><strong>Export the Design</strong>:</p>\n<ul>\n<li>Once the design is complete, click <strong>Export</strong> &gt; <strong>STL</strong>.</li>\n</ul>\n</li>\n</ol>\n<h2 id=\"step-3-printing-the-ornament\"><strong>Step 3: Printing the Ornament</strong></h2>\n<ol>\n<li>\n<p><strong>Prepare the File in a Slicer</strong>:</p>\n<ul>\n<li>Install slicing software (e.g., Ultimaker Cura).</li>\n<li>Import the STL file into Cura and select the Tronxy Crux1 as the printer model.</li>\n<li>Adjust settings:\n<ul>\n<li>Material: PLA</li>\n<li>Layer height: 0.2mm</li>\n<li>Infill: 20%</li>\n<li>Support: None (unless your design requires it)</li>\n</ul>\n</li>\n<li>Save the file as a G-code file.</li>\n</ul>\n</li>\n<li>\n<p><strong>Print the Ornament</strong>:</p>\n<ul>\n<li>Transfer the G-code to the printer via an SD card.</li>\n<li>Start the print and watch the magic happen!</li>\n</ul>\n</li>\n</ol>\n<h2 id=\"step-4-decorating-the-ornament\"><strong>Step 4: Decorating the Ornament</strong></h2>\n<ol>\n<li>\n<p><strong>Paint or Add Glitter</strong>:</p>\n<ul>\n<li>Use acrylic paints or markers to add color.</li>\n<li>Apply a small amount of glue and sprinkle glitter for extra sparkle.</li>\n</ul>\n</li>\n<li>\n<p><strong>Add a String</strong>:</p>\n<ul>\n<li>Thread a piece of string or ribbon through the hole and tie it into a loop for hanging.</li>\n</ul>\n</li>\n</ol>\n<h2 id=\"step-5-create-a-keepsake-memory\"><strong>Step 5: Create a Keepsake Memory</strong></h2>\n<ul>\n<li>Write the date and the child’s name on the ornament for a lifelong keepsake.</li>\n<li>Capture photos of the process to commemorate the fun Christmas morning activity.</li>\n</ul>\n<h3 id=\"safety-tips\"><strong>Safety Tips</strong></h3>\n<ul>\n<li>Supervise the child when handling the printer.</li>\n<li>Keep the nozzle area away from small hands as it can get extremely hot.</li>\n<li>Ensure good ventilation while printing.</li>\n</ul>\n<h1 id=\"3d-printing-vocabulary-with-links-and-references\">3D Printing Vocabulary with Links and References</h1>\n<p><strong>3D Printer</strong><br />\nA machine that creates three-dimensional objects by building them layer by layer from a digital design.<br />\n<a rel=\"external\" href=\"https://en.wikipedia.org/wiki/3D_printing\">Learn more about 3D printing</a></p>\n<p><strong>Filament</strong><br />\nThe \"ink\" for the 3D printer, usually made of plastic like PLA or ABS.<br />\n<a rel=\"external\" href=\"https://all3dp.com/1/3d-printer-filament-types-3d-printing-3d-filament/\">What is filament in 3D printing?</a></p>\n<p><strong>PLA</strong><br />\nA common type of filament made from plants. It's safe and easy to use for beginners.<br />\n<a rel=\"external\" href=\"https://all3dp.com/1/pla-filament-3d-printing-what-is-it/\">PLA filament explained</a></p>\n<p><strong>Extruder</strong><br />\nThe part of the printer that heats up the filament and pushes it out to create layers.<br />\n<a rel=\"external\" href=\"https://all3dp.com/2/3d-printer-extruder-basics/\">How does a 3D printer extruder work?</a></p>\n<p><strong>Nozzle</strong><br />\nThe small opening at the end of the extruder where the melted filament comes out.<br />\n<a rel=\"external\" href=\"https://all3dp.com/2/3d-printer-nozzle-size-comparison-best-guide/\">Choosing a nozzle for 3D printing</a></p>\n<p><strong>Build Plate (Bed)</strong><br />\nThe flat surface where the printer creates the object.<br />\n<a rel=\"external\" href=\"https://all3dp.com/2/3d-printer-bed-adhesion/\">How to maintain a 3D printer bed</a></p>\n<p><strong>Leveling</strong><br />\nAdjusting the printer bed to make sure it is flat and even with the nozzle.<br />\n<a rel=\"external\" href=\"https://all3dp.com/2/3d-printer-bed-leveling-best-tips/\">A guide to bed leveling</a></p>\n<p><strong>Layer Height</strong><br />\nThe thickness of each layer of filament that the printer lays down.<br />\n<a rel=\"external\" href=\"https://all3dp.com/2/layer-height-3d-printing-guide/\">Understanding layer height</a></p>\n<p><strong>Infill</strong><br />\nThe pattern and density of material inside the object. Makes the object strong without using too much filament.<br />\n<a rel=\"external\" href=\"https://all3dp.com/2/3d-printing-infill-guide/\">What is infill in 3D printing?</a></p>\n<p><strong>Slicer</strong><br />\nSoftware that turns a 3D design into instructions (G-code) the printer can understand.<br />\n<a rel=\"external\" href=\"https://all3dp.com/1/best-3d-slicer-software-3d-printer/\">Best slicer software for 3D printing</a></p>\n<p><strong>G-code</strong><br />\nThe file type that tells the 3D printer how to move and what to do.<br />\n<a rel=\"external\" href=\"https://www.matterhackers.com/articles/g-code-explained\">Introduction to G-code</a></p>\n<p><strong>STL File</strong><br />\nA type of file used to store 3D designs.<br />\n<a rel=\"external\" href=\"https://all3dp.com/what-is-stl-file-format-extension-3d-printing/\">What is an STL file?</a></p>\n<p><strong>Support Material</strong><br />\nExtra material printed to support parts of the design that hang in the air. Removed after printing.<br />\n<a rel=\"external\" href=\"https://all3dp.com/2/3d-printing-supports-all-you-need-to-know/\">How to use support materials</a></p>\n<p><strong>Raft/Brim</strong><br />\nA base layer of material that helps the object stick to the print bed.<br />\n<a rel=\"external\" href=\"https://all3dp.com/2/raft-vs-brim-vs-skirt-3d-printing/\">Difference between raft and brim</a></p>\n<p><strong>Overhang</strong><br />\nParts of a 3D print that stick out and may need support to print properly.<br />\n<a rel=\"external\" href=\"https://all3dp.com/2/3d-printing-overhang-all-you-need-to-know/\">Tips for handling overhangs</a></p>\n<p><strong>Cooling Fan</strong><br />\nA fan that cools the filament as it comes out of the nozzle to make it set faster.<br />\n<a rel=\"external\" href=\"https://all3dp.com/2/3d-printer-cooling-fan-guide/\">Cooling fans in 3D printing</a></p>\n<h3 id=\"tinkercad-vocabulary-with-links-and-references\">TinkerCAD Vocabulary with Links and References</h3>\n<p><strong>3D Design</strong><br />\nA digital model of something you want to print in 3D.<br />\n<a rel=\"external\" href=\"https://www.tinkercad.com/learn\">Getting started with TinkerCAD</a></p>\n<p><strong>Shape</strong><br />\nBasic building blocks like cubes, spheres, and cylinders used to create designs.<br />\n<a rel=\"external\" href=\"https://blog.tinkercad.com/shape-generator\">Creating with shapes in TinkerCAD</a></p>\n<p><strong>Workspace</strong><br />\nThe area where you build your 3D designs.<br />\n<a rel=\"external\" href=\"https://www.instructables.com/Tinkercad-Tutorial-Basics/\">TinkerCAD workspace basics</a></p>\n<p><strong>Resize</strong><br />\nChanging the size of a shape by dragging its edges or corners.<br />\n<a rel=\"external\" href=\"https://blog.tinkercad.com/tinkertip-precision-resize\">How to resize shapes in TinkerCAD</a></p>\n<p><strong>Align</strong><br />\nA tool to make shapes line up neatly.<br />\n<a rel=\"external\" href=\"https://blog.tinkercad.com/tinkertip-align-tool\">Aligning shapes in TinkerCAD</a></p>\n<p><strong>Group</strong><br />\nA tool that combines two or more shapes into one.<br />\n<a rel=\"external\" href=\"https://blog.tinkercad.com/tinkertip-grouping-shapes\">Using the group tool in TinkerCAD</a></p>\n<p><strong>Hole</strong><br />\nA shape used to create empty spaces or cut-outs in your design.<br />\n<a rel=\"external\" href=\"https://blog.tinkercad.com/tinkertip-holes\">Creating holes in TinkerCAD</a></p>\n<p><strong>Text Tool</strong><br />\nA feature that lets you add words or letters to your design.<br />\n<a rel=\"external\" href=\"https://blog.tinkercad.com/tinkertip-adding-text\">Adding text in TinkerCAD</a></p>\n<p><strong>Export</strong><br />\nSaving your 3D design as a file (like an STL) so it can be printed.<br />\n<a rel=\"external\" href=\"https://www.tinkercad.com/learn/exporting-your-design\">How to export files in TinkerCAD</a></p>\n<p>You can copy this directly into your notes for quick access! The links provide additional detailed resources for deeper learning.</p>\n<h3 id=\"free-3d-design-repositories\"><strong>Free 3D Design Repositories</strong></h3>\n<ol>\n<li>\n<p><strong><a rel=\"external\" href=\"https://www.thingiverse.com/\">Thingiverse</a></strong></p>\n<ul>\n<li>One of the largest repositories of free 3D models.</li>\n<li>Great for beginners and features a variety of categories.</li>\n</ul>\n</li>\n<li>\n<p><strong><a rel=\"external\" href=\"https://www.printables.com/\">Printables by Prusa</a></strong></p>\n<ul>\n<li>Community-driven platform by Prusa.</li>\n<li>Offers high-quality and curated designs.</li>\n</ul>\n</li>\n<li>\n<p><strong><a rel=\"external\" href=\"https://www.myminifactory.com/\">MyMiniFactory</a></strong></p>\n<ul>\n<li>Focuses on verified and tested models.</li>\n<li>Ideal for high-quality prints.</li>\n</ul>\n</li>\n<li>\n<p><strong><a rel=\"external\" href=\"https://cults3d.com/\">Cults</a></strong></p>\n<ul>\n<li>Offers free and premium designs.</li>\n<li>Features unique and artistic models.</li>\n</ul>\n</li>\n<li>\n<p><strong><a rel=\"external\" href=\"https://www.youmagine.com/\">YouMagine</a></strong></p>\n<ul>\n<li>Free designs shared by a collaborative community.</li>\n<li>Focuses on simplicity and easy downloads.</li>\n</ul>\n</li>\n<li>\n<p><strong><a rel=\"external\" href=\"https://www.stlfinder.com/\">STLFinder</a></strong></p>\n<ul>\n<li>A search engine for STL files across multiple repositories.</li>\n<li>Helps discover designs from various sources in one place.</li>\n</ul>\n</li>\n<li>\n<p><strong><a rel=\"external\" href=\"https://pinshape.com/\">Pinshape</a></strong></p>\n<ul>\n<li>Free and paid designs.</li>\n<li>User-friendly interface with ratings for designs.</li>\n</ul>\n</li>\n<li>\n<p><strong><a rel=\"external\" href=\"https://3dexport.com/free-3d-models/\">3DExport</a></strong></p>\n<ul>\n<li>A mix of free and paid models, including STL files for printing.</li>\n<li>Good for detailed and professional models.</li>\n</ul>\n</li>\n</ol>\n<h3 id=\"premium-3d-design-platforms\"><strong>Premium 3D Design Platforms</strong></h3>\n<ol>\n<li>\n<p><strong><a rel=\"external\" href=\"https://www.turbosquid.com/\">TurboSquid</a></strong></p>\n<ul>\n<li>High-quality models for professionals.</li>\n<li>Offers some free options alongside premium designs.</li>\n</ul>\n</li>\n<li>\n<p><strong><a rel=\"external\" href=\"https://www.cgtrader.com/\">CGTrader</a></strong></p>\n<ul>\n<li>A marketplace for 3D designs, including print-ready files.</li>\n<li>Both free and paid options available.</li>\n</ul>\n</li>\n<li>\n<p><strong><a rel=\"external\" href=\"https://www.gambody.com/\">Gambody</a></strong></p>\n<ul>\n<li>Specializes in high-detail collectible models, especially for gaming.</li>\n<li>Premium designs with intricate details.</li>\n</ul>\n</li>\n</ol>\n<h3 id=\"niche-3d-model-repositories\"><strong>Niche 3D Model Repositories</strong></h3>\n<ol>\n<li>\n<p><strong><a rel=\"external\" href=\"https://nasa3d.arc.nasa.gov/\">NASA 3D Resources</a></strong></p>\n<ul>\n<li>Free 3D models related to space and astronomy.</li>\n<li>Perfect for educational or thematic projects.</li>\n</ul>\n</li>\n<li>\n<p><strong><a rel=\"external\" href=\"https://3d.si.edu/\">Smithsonian 3D Digitization</a></strong></p>\n<ul>\n<li>Free models of artifacts from museums.</li>\n<li>Focuses on cultural and historical objects.</li>\n</ul>\n</li>\n<li>\n<p><strong><a rel=\"external\" href=\"https://3dwarehouse.sketchup.com/\">3D Warehouse</a></strong></p>\n<ul>\n<li>A repository for SketchUp designs.</li>\n<li>Includes many 3D-printable models.</li>\n</ul>\n</li>\n</ol>\n<h3 id=\"community-platforms-for-collaboration\"><strong>Community Platforms for Collaboration</strong></h3>\n<ol>\n<li>\n<p><strong><a rel=\"external\" href=\"https://grabcad.com/library\">GrabCAD</a></strong></p>\n<ul>\n<li>Features engineering-focused designs.</li>\n<li>Community-driven platform with CAD and STL files.</li>\n</ul>\n</li>\n<li>\n<p><strong><a rel=\"external\" href=\"https://www.tinkercad.com/gallery/\">TinkerCAD Community Gallery</a></strong></p>\n<ul>\n<li>Browse designs shared by other TinkerCAD users.</li>\n<li>Perfect for simple and beginner-friendly projects.</li>\n</ul>\n</li>\n</ol>\n<h3 id=\"educational-resources-for-learning-and-customization\"><strong>Educational Resources for Learning and Customization</strong></h3>\n<ol>\n<li>\n<p><strong><a rel=\"external\" href=\"https://www.openscad.org/\">OpenSCAD Libraries</a></strong></p>\n<ul>\n<li>Great for learning to create parametric designs.</li>\n<li>Models that can be customized with code.</li>\n</ul>\n</li>\n<li>\n<p><strong><a rel=\"external\" href=\"https://www.instructables.com/3D-Printing/\">Instructables 3D Printing Projects</a></strong></p>\n<ul>\n<li>A collection of step-by-step guides with downloadable files.</li>\n</ul>\n</li>\n<li>\n<p><strong><a rel=\"external\" href=\"https://sketchfab.com/\">Sketchfab</a></strong></p>\n<ul>\n<li>Focused on viewing and downloading 3D models.</li>\n<li>Some models are free for 3D printing.</li>\n</ul>\n</li>\n</ol>\n<h3 id=\"search-aggregators\"><strong>Search Aggregators</strong></h3>\n<ol>\n<li>\n<p><strong><a rel=\"external\" href=\"https://www.yeggi.com/\">Yeggi</a></strong></p>\n<ul>\n<li>Search engine for 3D printable models from multiple platforms.</li>\n<li>Filters for free and paid designs.</li>\n</ul>\n</li>\n<li>\n<p><strong><a rel=\"external\" href=\"https://thangs.com/\">Thangs</a></strong></p>\n<ul>\n<li>Search, share, and collaborate on 3D designs.</li>\n<li>Includes a geometric search feature for finding similar designs.</li>\n</ul>\n</li>\n</ol>\n<h1 id=\"more-project-ideas\">More Project Ideas</h1>\n<h3 id=\"christmas-themed-3d-printable-designs\">Christmas-Themed 3D Printable Designs</h3>\n<ol>\n<li>\n<p><strong>Cults3D - Christmas Ornaments</strong><br />\nA collection of free 3D models for Christmas ornaments, including snowflakes, baubles, and festive decorations.<br />\n<a rel=\"external\" href=\"https://cults3d.com/en/tags/christmas%2Bornaments?only_free=true\">Explore designs</a></p>\n</li>\n<li>\n<p><strong>Printables - Winter &amp; Christmas &amp; New Year's</strong><br />\nOffers various decorations like snowflakes, bells, and Santa Claus miniatures.<br />\n<a rel=\"external\" href=\"https://www.printables.com/model?category=70\">Browse models</a></p>\n</li>\n<li>\n<p><strong>Thingiverse - Christmas Collection</strong><br />\nA diverse range of Christmas-themed designs, from ornaments to festive gadgets.<br />\n<a rel=\"external\" href=\"https://www.thingiverse.com/search?q=christmas&amp;dwh=0e8b1b\">View collection</a></p>\n</li>\n<li>\n<p><strong>Yeggi - Free Christmas Ornaments</strong><br />\nAggregates free Christmas ornament designs from various sources.<br />\n<a rel=\"external\" href=\"https://www.yeggi.com/q/free%2Bchristmas%2Bornaments/\">Find models</a></p>\n</li>\n</ol>\n<h3 id=\"3d-printable-toys-for-kids\">3D Printable Toys for Kids</h3>\n<ol>\n<li>\n<p><strong>Cults3D - Toys</strong><br />\nA vast selection of free toy designs, including action figures, puzzles, and educational models.<br />\n<a rel=\"external\" href=\"https://cults3d.com/en/tags/toys?only_free=true\">Explore toys</a></p>\n</li>\n<li>\n<p><strong>Printables - Toys &amp; Games</strong><br />\nFeatures a variety of 3D printed games and toys for both indoor and outdoor play.<br />\n<a rel=\"external\" href=\"https://www.printables.com/model?category=30\">Browse toys</a></p>\n</li>\n<li>\n<p><strong>Yeggi - Free Kid Toy Models</strong><br />\nSearches multiple repositories for free 3D printable toy designs suitable for children.<br />\n<a rel=\"external\" href=\"https://www.yeggi.com/q/free%2Bkid%2Btoy/\">Find toys</a></p>\n</li>\n<li>\n<p><strong>All3DP - 3D Printed Toys</strong><br />\nAn article featuring a curated list of 3D printed toys that can engage and entertain kids.<br />\n<a rel=\"external\" href=\"https://all3dp.com/2/3d-printed-toys-distract-kids/\">Read more</a></p>\n</li>\n</ol>\n<h3 id=\"tips-for-printing\">Tips for Printing</h3>\n<ul>\n<li>\n<p><strong>Check Print Settings</strong>: Ensure your printer settings match the requirements of the chosen design, including layer height, infill, and support structures.</p>\n</li>\n<li>\n<p><strong>Material Selection</strong>: PLA is a common and safe filament choice for printing toys and ornaments.</p>\n</li>\n<li>\n<p><strong>Supervision</strong>: Always supervise children when handling 3D printed toys, especially if they contain small parts.</p>\n</li>\n</ul>\n<p>These resources should provide a great starting point for creating festive decorations and fun toys with your 3D printer. Happy printing!</p>\n",
      summary: null,
      date: "2024-12-24T00:00:00Z",
      reading_time: 11,
      metadata: {},
      tags: ["guide","tronxy","crux1","lina"],
      categories: ["lab"],
      series: [],
      projects: ["printing"]
  };



  
  
  
  

  
    
  
  
    
  
  
  

  CREATE post CONTENT {
      title: "Rust Memory Cheatsheet",
      slug: "rust-memory-cheatsheet",
      path: "https://parkerjones.dev/posts/rust-memory-cheatsheet/",
      content: "<p><strong>Rust Memory Container Cheat Sheet</strong> and the <strong>Rust Container Cheat Sheet</strong>. These visual aids provide concise and clear representations of Rust's memory containers, aiding in understanding their structures and relationships.</p>\n<h2 id=\"rust-memory-container-cheat-sheet\">Rust Memory Container Cheat Sheet</h2>\n\n\n<figure class=\"post-image\">\n  <img src=\"https://parkerjones.dev/processed_images/rust-memory-container-cs.99dfaf6a7b485902.webp\" width=\"1600\" height=\"900\"\n       alt=\"Rust Memory Container Cheat-sheet decision tree, by Usagi Ito\" loading=\"lazy\" decoding=\"async\">\n  \n</figure>\n<p><em>Image Source: <a rel=\"external\" href=\"https://github.com/usagi/rust-memory-container-cs\">Rust Memory Container Cheat Sheet by Usagi Ito</a></em></p>\n<p>This cheat sheet, created by Usagi Ito, offers a comprehensive overview of Rust's memory containers, illustrating their layouts and how they manage memory. It's an excellent reference for both beginners and seasoned Rustaceans aiming to deepen their understanding of Rust's memory management.</p>\n<p>For more details and additional formats, visit the <a rel=\"external\" href=\"https://github.com/usagi/rust-memory-container-cs\">GitHub repository</a>.</p>\n<h2 id=\"rust-container-cheat-sheet\">Rust Container Cheat Sheet</h2>\n\n\n<figure class=\"post-image\">\n  <img src=\"https://parkerjones.dev/processed_images/rust-container-cheat-sheet.287aac9ed73de51b.webp\" width=\"1600\" height=\"885\"\n       alt=\"Rust Container Cheat Sheet memory layouts, by Raph Levien\" loading=\"lazy\" decoding=\"async\">\n  \n</figure>\n<p><em>Image Source: <a rel=\"external\" href=\"https://handbook.dataland.engineering/assets/rust-container-cheat-sheet.pdf\">Rust Container Cheat Sheet by Raph Levien</a></em></p>\n<p>This cheat sheet, designed by Raph Levien, provides a detailed look at Rust's container types, their memory layouts, and associated methods. It's a handy tool for developers seeking to grasp the nuances of Rust's standard library containers.</p>\n<p>You can download the PDF version <a rel=\"external\" href=\"https://handbook.dataland.engineering/assets/rust-container-cheat-sheet.pdf\">here</a>.</p>\n<p><em>Note: All credit for these cheat sheets goes to their respective authors. Please refer to the original sources for the most up-to-date versions and additional information.</em></p>\n<h2 id=\"rust-memory-container-types\">Rust Memory Container Types</h2>\n<p>Rust's standard library offers a variety of memory container types, each designed for specific use cases. Here are some commonly used containers:</p>\n<ul>\n<li>\n<p><strong>Vectors (<code>Vec&lt;T&gt;</code>):</strong> A dynamic array that can grow or shrink in size. <a rel=\"external\" href=\"https://doc.rust-lang.org/std/vec/\">Documentation</a>.</p>\n</li>\n<li>\n<p><strong>Strings (<code>String</code>):</strong> A growable, mutable, UTF-8 encoded string. <a rel=\"external\" href=\"https://doc.rust-lang.org/std/string/struct.String.html\">Documentation</a>.</p>\n</li>\n<li>\n<p><strong>Hash Maps (<code>HashMap&lt;K, V&gt;</code>):</strong> A hash map implemented with quadratic probing and SIMD lookup. <a rel=\"external\" href=\"https://doc.rust-lang.org/std/collections/struct.HashMap.html\">Documentation</a>.</p>\n</li>\n<li>\n<p><strong>B-Trees (<code>BTreeMap&lt;K, V&gt;</code>):</strong> An ordered map based on a B-Tree. <a rel=\"external\" href=\"https://doc.rust-lang.org/std/collections/struct.BTreeMap.html\">Documentation</a>.</p>\n</li>\n<li>\n<p><strong>Hash Sets (<code>HashSet&lt;T&gt;</code>):</strong> A hash set implemented as a <code>HashMap</code> where the value is <code>()</code>. <a rel=\"external\" href=\"https://doc.rust-lang.org/std/collections/struct.HashSet.html\">Documentation</a>.</p>\n</li>\n<li>\n<p><strong>B-Tree Sets (<code>BTreeSet&lt;T&gt;</code>):</strong> An ordered set based on a B-Tree. <a rel=\"external\" href=\"https://doc.rust-lang.org/std/collections/struct.BTreeSet.html\">Documentation</a>.</p>\n</li>\n<li>\n<p><strong>Linked Lists (<code>LinkedList&lt;T&gt;</code>):</strong> A doubly-linked list. <a rel=\"external\" href=\"https://doc.rust-lang.org/std/collections/struct.LinkedList.html\">Documentation</a>.</p>\n</li>\n<li>\n<p><strong>Vec Deques (<code>VecDeque&lt;T&gt;</code>):</strong> A double-ended queue implemented with a growable ring buffer. <a rel=\"external\" href=\"https://doc.rust-lang.org/std/collections/struct.VecDeque.html\">Documentation</a>.</p>\n</li>\n<li>\n<p><strong>Binary Heaps (<code>BinaryHeap&lt;T&gt;</code>):</strong> A priority queue implemented with a binary heap. <a rel=\"external\" href=\"https://doc.rust-lang.org/std/collections/struct.BinaryHeap.html\">Documentation</a>.</p>\n</li>\n<li>\n<p><strong>Reference Counting (<code>Rc&lt;T&gt;</code>):</strong> A single-threaded reference-counting pointer. <a rel=\"external\" href=\"https://doc.rust-lang.org/std/rc/struct.Rc.html\">Documentation</a>.</p>\n</li>\n<li>\n<p><strong>Atomic Reference Counting (<code>Arc&lt;T&gt;</code>):</strong> A thread-safe reference-counting pointer. <a rel=\"external\" href=\"https://doc.rust-lang.org/std/sync/struct.Arc.html\">Documentation</a>.</p>\n</li>\n<li>\n<p><strong>Boxes (<code>Box&lt;T&gt;</code>):</strong> A pointer type for heap allocation. <a rel=\"external\" href=\"https://doc.rust-lang.org/std/boxed/struct.Box.html\">Documentation</a>.</p>\n</li>\n<li>\n<p><strong>Cells (<code>Cell&lt;T&gt;</code>):</strong> A mutable memory location with 'interior mutability'. <a rel=\"external\" href=\"https://doc.rust-lang.org/std/cell/struct.Cell.html\">Documentation</a>.</p>\n</li>\n<li>\n<p><strong>RefCells (<code>RefCell&lt;T&gt;</code>):</strong> A single-threaded mutable memory location with dynamic borrow checking. <a rel=\"external\" href=\"https://doc.rust-lang.org/std/cell/struct.RefCell.html\">Documentation</a>.</p>\n</li>\n<li>\n<p><strong>Mutexes (<code>Mutex&lt;T&gt;</code>):</strong> A mutual exclusion primitive useful for protecting shared data. <a rel=\"external\" href=\"https://doc.rust-lang.org/std/sync/struct.Mutex.html\">Documentation</a>.</p>\n</li>\n<li>\n<p><strong>Read-Write Locks (<code>RwLock&lt;T&gt;</code>):</strong> A reader-writer lock allowing multiple readers or one writer. <a rel=\"external\" href=\"https://doc.rust-lang.org/std/sync/struct.RwLock.html\">Documentation</a>.</p>\n</li>\n</ul>\n<h2 id=\"identifying-performance-issues-from-unnecessary-clones\">Identifying Performance Issues from Unnecessary Clones</h2>\n<p>Unnecessary <code>clone</code> operations can lead to performance degradation due to redundant memory allocations. To detect and address these issues:</p>\n<ol>\n<li>\n<p><strong>Profiling Tools:</strong></p>\n<ul>\n<li>\n<p><strong><code>cargo flamegraph</code>:</strong> Generates flamegraphs to visualize CPU usage, helping identify performance bottlenecks, including excessive cloning. <a rel=\"external\" href=\"https://github.com/flamegraph-rs/flamegraph\">GitHub Repository</a>.</p>\n</li>\n<li>\n<p><strong><code>perf</code>:</strong> A powerful performance analyzing tool on Linux that can profile Rust applications to detect inefficient code paths. <a rel=\"external\" href=\"https://nnethercote.github.io/perf-book/profiling.html\">Rust Performance Book - Profiling</a>.</p>\n</li>\n</ul>\n</li>\n<li>\n<p><strong>Static Analysis:</strong></p>\n<ul>\n<li><strong><code>cargo clippy</code>:</strong> A linter that provides warnings about common mistakes, including unnecessary clones. Running <code>cargo clippy</code> can highlight instances where cloning is avoidable. <a rel=\"external\" href=\"https://rust-unofficial.github.io/patterns/anti_patterns/borrow_clone.html\">Rust Design Patterns - Clone to Satisfy the Borrow Checker</a>.</li>\n</ul>\n</li>\n<li>\n<p><strong>Code Review:</strong></p>\n<ul>\n<li><strong>Manual Inspection:</strong> Review your codebase to identify <code>clone</code> calls. Assess whether ownership transfer or borrowing (<code>&amp;T</code> or <code>&amp;mut T</code>) is more appropriate.</li>\n</ul>\n</li>\n<li>\n<p><strong>Benchmarking:</strong></p>\n<ul>\n<li><strong><code>criterion.rs</code>:</strong> A benchmarking tool to measure and compare the performance of Rust code, useful for assessing the impact of removing unnecessary clones. <a rel=\"external\" href=\"https://github.com/bheisler/criterion.rs\">Criterion.rs</a>.</li>\n</ul>\n</li>\n</ol>\n<p>By utilizing these tools and practices, you can effectively identify and mitigate performance issues arising from unnecessary cloning in your Rust applications.</p>\n",
      summary: null,
      date: "2024-12-23T00:00:00Z",
      reading_time: 4,
      metadata: {},
      tags: ["rust","memory","systems"],
      categories: ["misc"],
      series: [],
      projects: []
  };



  
  
  
  

  
    
  
  
    
  
  
    
  
  

  CREATE post CONTENT {
      title: "Mastering C with Effective C: Introduction to Makefiles",
      slug: "mastering-c-makefiles",
      path: "https://parkerjones.dev/posts/mastering-c-makefiles/",
      content: "<h3 id=\"mastering-c-with-effective-c-introduction-to-makefiles\">Mastering C with Effective C: Introduction to Makefiles</h3>\n<h4 id=\"introduction\">Introduction</h4>\n<p>Welcome to the first post in the <a href=\"/mastering-c\">\"Mastering C with Effective C\" series</a>! In this post, we'll explore the basics of Makefiles and how they can help you manage and automate the build process of your C projects. Makefiles are an essential tool for any C programmer, simplifying the compilation process, managing dependencies, and ensuring efficient builds.</p>\n<h4 id=\"what-is-a-makefile\">What is a Makefile?</h4>\n<p>A Makefile is a special file, containing a set of directives used by the <code>make</code> build automation tool to compile and link a program. It defines rules on how to build different parts of your project, making it easier to manage larger projects with multiple source files.</p>\n<h4 id=\"benefits-of-using-makefiles\">Benefits of Using Makefiles</h4>\n<ul>\n<li><strong>Automation</strong>: Automates the compilation process, reducing the chances of human error.</li>\n<li><strong>Efficiency</strong>: Only rebuilds the parts of the project that have changed, saving time during development.</li>\n<li><strong>Organization</strong>: Keeps build instructions in a single, easy-to-read file.</li>\n<li><strong>Portability</strong>: Ensures that your project can be built consistently across different environments.</li>\n</ul>\n<h4 id=\"basic-structure-of-a-makefile\">Basic Structure of a Makefile</h4>\n<p>A Makefile typically consists of rules. Each rule defines how to build a target from its dependencies. The general structure is:</p>\n<pre><code data-lang=\"makefile\">target: dependencies\n    command\n</code></pre>\n<ul>\n<li><strong>target</strong>: The file to be generated (e.g., executable or object file).</li>\n<li><strong>dependencies</strong>: The files that the target depends on (e.g., source files).</li>\n<li><strong>command</strong>: The command to generate the target from the dependencies (e.g., compile command).</li>\n</ul>\n<h4 id=\"example-a-simple-makefile\">Example: A Simple Makefile</h4>\n<p>Let's start with a simple example. Suppose we have a project with three source files: <code>main.c</code>, <code>file1.c</code>, and <code>file2.c</code>. Here's a basic Makefile to compile these files into a single executable:</p>\n<pre><code data-lang=\"makefile\"># the compiler to use\nCC = clang\n\n# compiler flags:\n#  -g    adds debugging information to the executable file\n#  -Wall turns on most, but not all, compiler warnings\nCFLAGS  = -g -Wall\n  \n# files to link:\nLFLAGS = #-lcs50\n  \n# the name to use for the output file:\nTARGET = my_program\n  \n# the list of source files\nSRCS = main.c file1.c file2.c\n  \n# the list of object files (derived from the source files)\nOBJS = $(SRCS:.c=.o)\n  \nall: $(TARGET)\n  \n$(TARGET): $(OBJS)\n\t$(CC) $(CFLAGS) -o $(TARGET) $(OBJS) $(LFLAGS)\n  \n%.o: %.c\n\t$(CC) $(CFLAGS) -c $&lt; -o $@\n  \nclean:\n\trm -f $(OBJS) $(TARGET)\n</code></pre>\n<h4 id=\"breaking-down-the-makefile\">Breaking Down the Makefile</h4>\n<ul>\n<li><strong>Compiler and Flags</strong>: We specify the compiler (<code>clang</code>) and the compiler flags (<code>CFLAGS</code>).</li>\n<li><strong>Source and Object Files</strong>: <code>SRCS</code> lists the source files, and <code>OBJS</code> lists the corresponding object files.</li>\n<li><strong>Build Rules</strong>:\n<ul>\n<li><code>all: $(TARGET)</code>: The default target, which builds the executable.</li>\n<li><code>$(TARGET): $(OBJS)</code>: The rule to link the object files into the final executable.</li>\n<li><code>%.o: %.c</code>: A pattern rule to compile source files into object files.</li>\n</ul>\n</li>\n<li><strong>Clean Rule</strong>: The <code>clean</code> rule removes the generated object files and the executable.</li>\n</ul>\n<h4 id=\"using-the-makefile\">Using the Makefile</h4>\n<p>To use the Makefile, simply run the <code>make</code> command in the terminal within the project directory. This will compile and link the source files into the <code>my_program</code> executable.</p>\n<pre><code data-lang=\"sh\">make\n</code></pre>\n<p>To clean up the generated files, run:</p>\n<pre><code data-lang=\"sh\">make clean\n</code></pre>\n<h4 id=\"additional-resources\">Additional Resources</h4>\n<p>For more information and deeper dives into Makefiles, check out these resources:</p>\n<ul>\n<li><a rel=\"external\" href=\"https://www.gnu.org/software/make/manual/make.html\">GNU Make Manual</a></li>\n<li><a rel=\"external\" href=\"https://en.wikipedia.org/wiki/Make_(software)\">GNU Make on Wikipedia</a></li>\n<li><a rel=\"external\" href=\"https://www.tutorialspoint.com/makefile/index.htm\">Makefile Tutorial - TutorialsPoint</a></li>\n<li><a rel=\"external\" href=\"https://makefiletutorial.com/\">Makefile Tutorial by Example</a></li>\n<li><a rel=\"external\" href=\"https://cmake.org/documentation/\">CMake Documentation</a></li>\n</ul>\n<h4 id=\"conclusion\">Conclusion</h4>\n<p>Makefiles are a powerful tool for managing the build process of your C projects. By automating compilation and linking, they save time and reduce errors. In this post, we covered the basics of Makefiles, their benefits, and how to create a simple Makefile for a C project.</p>\n<h4 id=\"next-up\">Next Up</h4>\n<p>Stay tuned for the next post in the \"Mastering C with Effective C\" series, where we'll dive into data types and variables in C. We will explore different data types, how to declare and use variables, and the importance of understanding data representation.</p>\n<hr />\n<p>Happy coding!</p>\n",
      summary: null,
      date: "2024-06-14T00:00:00Z",
      reading_time: 3,
      metadata: {"series_order":2},
      tags: ["make","tutorial","ai","series","learning","c"],
      categories: ["software"],
      series: ["mastering-c"],
      projects: []
  };



  
  
  
  

  
    
  
  
    
  
  
    
  
  

  CREATE post CONTENT {
      title: "Mastering C: Building a Project with Nix Flakes",
      slug: "mastering-c-nix-flake",
      path: "https://parkerjones.dev/posts/mastering-c-nix-flake/",
      content: "<h1 id=\"upgrading-your-c-project-build-to-use-nix-flakes-a-modern-guide\">Upgrading Your C Project Build to Use Nix Flakes: A Modern Guide</h1>\n<p>Hello again, fellow C enthusiasts! In our previous blog post, we explored building a C program with external library dependencies using Nix. Today, we’re taking it a step further by upgrading our Nix setup to use Nix flakes, a more structured and modern way to manage dependencies and ensure reproducibility. This post will guide you through updating your existing Nix build to a Nix flake and explain each component in detail. Let’s dive in!</p>\n<h2 id=\"what-are-nix-flakes\">What Are Nix Flakes?</h2>\n<p>Nix flakes introduce a standardized way to define Nix projects, providing better dependency management, versioning, and reproducibility. They offer:</p>\n<ul>\n<li><strong>Consistency</strong>: Ensures builds are reproducible across different environments.</li>\n<li><strong>Ease of Use</strong>: Simplifies the setup and management of dependencies.</li>\n<li><strong>Modularity</strong>: Allows sharing and composing Nix expressions easily.</li>\n</ul>\n<p>For more details, you can check out the <a rel=\"external\" href=\"https://nixos.wiki/wiki/Flakes\">Nix Flakes documentation</a>.</p>\n<h2 id=\"transitioning-from-a-classic-nix-setup-to-nix-flakes\">Transitioning from a Classic Nix Setup to Nix Flakes</h2>\n<p>We’ll start with our <code>simple-parse-example</code> C program, which uses the <code>jansson</code> library for JSON parsing. We’ll convert our existing <code>default.nix</code> to a <code>flake.nix</code> file.</p>\n<h3 id=\"original-default-nix\">Original <code>default.nix</code></h3>\n<p>Here’s a quick look at our original <code>default.nix</code>:</p>\n<pre><code data-lang=\"nix\">{ pkgs ? import &lt;nixpkgs&gt; {} }:\n\nlet\n  jansson = pkgs.jansson;\nin\npkgs.stdenv.mkDerivation {\n  pname = &quot;simple-parse-example&quot;;\n  version = &quot;1.0&quot;;\n\n  src = ./.;\n\n  nativeBuildInputs = [ pkgs.clang ];\n  buildInputs = [ jansson ];\n\n  buildPhase = &#39;&#39;\n    clang -g -Wall -o simple-parse-example simple-parse-example.c -ljansson\n  &#39;&#39;;\n\n  installPhase = &#39;&#39;\n    mkdir -p $out/bin\n    cp simple-parse-example $out/bin/\n  &#39;&#39;;\n\n  meta = with pkgs.lib; {\n    description = &quot;A simple program using jansson&quot;;\n    license = licenses.mit;\n    maintainers = [ maintainers.yourname ];\n    platforms = platforms.unix;\n  };\n}\n</code></pre>\n<h3 id=\"converting-to-flake-nix\">Converting to <code>flake.nix</code></h3>\n<p>We’ll now convert this setup to use Nix flakes.</p>\n<h4 id=\"step-1-creating-flake-nix\">Step 1: Creating <code>flake.nix</code></h4>\n<p>Create a file named <code>flake.nix</code> in your project directory with the following content:</p>\n<pre><code data-lang=\"nix\">{\n  description = &quot;A simple C program using jansson built with Nix flakes&quot;;\n\n  inputs = {\n    nixpkgs.url = &quot;github:NixOS/nixpkgs/nixpkgs-unstable&quot;;\n  };\n\n  outputs = { self, nixpkgs }: {\n    packages = nixpkgs.lib.genAttrs [ &quot;aarch64-darwin&quot; &quot;x86_64-linux&quot; ] (system:\n    let\n      pkgs = import nixpkgs { inherit system; };\n    in\n    rec {\n      simple-parse-example = pkgs.stdenv.mkDerivation {\n        pname = &quot;simple-parse-example&quot;;\n        version = &quot;1.0&quot;;\n\n        src = ./.;\n\n        nativeBuildInputs = [ pkgs.clang ];\n        buildInputs = [ pkgs.jansson ];\n\n        buildPhase = &#39;&#39;\n          clang -g -Wall -o simple-parse-example simple-parse-example.c -ljansson\n        &#39;&#39;;\n\n        installPhase = &#39;&#39;\n          mkdir -p $out/bin\n          cp simple-parse-example $out/bin/\n        &#39;&#39;;\n\n        meta = with pkgs.lib; {\n          description = &quot;A simple program using jansson&quot;;\n          license = licenses.mit;\n          maintainers = [ maintainers.yourname ];\n          platforms = platforms.unix;\n        };\n      };\n    });\n\n    defaultPackage = {\n      aarch64-darwin = self.packages.aarch64-darwin.simple-parse-example;\n      x86_64-linux = self.packages.x86_64-linux.simple-parse-example;\n    };\n\n    defaultApp = {\n      forAllSystems = nixpkgs.lib.mapAttrs&#39; (system: pkg: {\n        inherit system;\n        defaultApp = {\n          type = &quot;app&quot;;\n          program = &quot;${pkg.simple-parse-example}/bin/simple-parse-example&quot;;\n        };\n      }) self.packages;\n    };\n  };\n}\n</code></pre>\n<h3 id=\"breaking-down-the-flake-configuration\">Breaking Down the Flake Configuration</h3>\n<p>Let’s break down each component of the <code>flake.nix</code> file to understand what it does.</p>\n<h4 id=\"1-description\">1. <strong>description</strong></h4>\n<pre><code data-lang=\"nix\">description = &quot;A simple C program using jansson built with Nix flakes&quot;;\n</code></pre>\n<p>This provides a brief description of the flake. It’s useful for documentation and understanding the purpose of the flake.</p>\n<h4 id=\"2-inputs\">2. <strong>inputs</strong></h4>\n<pre><code data-lang=\"nix\">inputs = {\n  nixpkgs.url = &quot;github:NixOS/nixpkgs/nixpkgs-unstable&quot;;\n};\n</code></pre>\n<p>The <code>inputs</code> section defines dependencies for the flake. Here, we are specifying that we want to use the unstable branch of the Nixpkgs repository. This is where we’ll get our packages like <code>clang</code> and <code>jansson</code>.</p>\n<ul>\n<li><strong>Documentation</strong>: <a rel=\"external\" href=\"https://nixos.wiki/wiki/Nixpkgs\">Nixpkgs Input</a></li>\n</ul>\n<h4 id=\"3-outputs\">3. <strong>outputs</strong></h4>\n<pre><code data-lang=\"nix\">outputs = { self, nixpkgs }: {\n  packages = nixpkgs.lib.genAttrs [ &quot;aarch64-darwin&quot; &quot;x86_64-linux&quot; ] (system:\n  let\n    pkgs = import nixpkgs { inherit system; };\n  in\n  rec {\n    simple-parse-example = pkgs.stdenv.mkDerivation {\n      pname = &quot;simple-parse-example&quot;;\n      version = &quot;1.0&quot;;\n\n      src = ./.;\n\n      nativeBuildInputs = [ pkgs.clang ];\n      buildInputs = [ pkgs.jansson ];\n\n      buildPhase = &#39;&#39;\n        clang -g -Wall -o simple-parse-example simple-parse-example.c -ljansson\n      &#39;&#39;;\n\n      installPhase = &#39;&#39;\n        mkdir -p $out/bin\n        cp simple-parse-example $out/bin/\n      &#39;&#39;;\n\n      meta = with pkgs.lib; {\n        description = &quot;A simple program using jansson&quot;;\n        license = licenses.mit;\n        maintainers = [ maintainers.yourname ];\n        platforms = platforms.unix;\n      };\n    };\n  });\n\n  defaultPackage = {\n    aarch64-darwin = self.packages.aarch64-darwin.simple-parse-example;\n    x86_64-linux = self.packages.x86_64-linux.simple-parse-example;\n  };\n\n  defaultApp = {\n    forAllSystems = nixpkgs.lib.mapAttrs&#39; (system: pkg: {\n      inherit system;\n      defaultApp = {\n        type = &quot;app&quot;;\n        program = &quot;${pkg.simple-parse-example}/bin/simple-parse-example&quot;;\n      };\n    }) self.packages;\n  };\n};\n</code></pre>\n<ul>\n<li>\n<p><strong>packages</strong>: This section uses <code>nixpkgs.lib.genAttrs</code> to define packages for multiple systems (<code>aarch64-darwin</code> and <code>x86_64-linux</code>). For each system, we import the appropriate version of <code>nixpkgs</code> and define a derivation for <code>simple-parse-example</code>.</p>\n</li>\n<li>\n<p><strong>defaultPackage</strong>: Specifies the default package for each system. This is used to tell Nix which package to build by default for the current system.</p>\n</li>\n<li>\n<p><strong>defaultApp</strong>: Specifies the default application for each system using <code>mapAttrs'</code> to iterate over systems and create default apps.</p>\n</li>\n<li>\n<p><strong>Documentation</strong>:</p>\n<ul>\n<li><a rel=\"external\" href=\"https://nixos.org/manual/nixpkgs/stable/#sec-functions-lib-genAttrs\">genAttrs</a></li>\n<li><a rel=\"external\" href=\"https://nixos.org/manual/nixpkgs/stable/#chap-stdenv\">mkDerivation</a></li>\n</ul>\n</li>\n</ul>\n<h3 id=\"building-and-running-with-nix-flakes\">Building and Running with Nix Flakes</h3>\n<p>To ensure that everything works as expected, follow these steps:</p>\n<ol>\n<li>\n<p><strong>Navigate to your project directory</strong>:</p>\n<pre><code data-lang=\"sh\">cd ~/dev/learning-c/parsing-json\n</code></pre>\n</li>\n<li>\n<p><strong>Enable Flakes</strong>:\nMake sure flakes are enabled in your Nix configuration:</p>\n<pre><code data-lang=\"sh\">echo &quot;experimental-features = nix-command flakes&quot; &gt;&gt; ~/.config/nix/nix.conf\n</code></pre>\n</li>\n<li>\n<p><strong>Build the project</strong>:\nBuild the project using:</p>\n<pre><code data-lang=\"sh\">nix build .#simple-parse-example\n</code></pre>\n</li>\n<li>\n<p><strong>Run the binary</strong>:\nAfter building, you can run the binary with:</p>\n<pre><code data-lang=\"sh\">./result/bin/simple-parse-example\n</code></pre>\n</li>\n</ol>\n<h3 id=\"conclusion\">Conclusion</h3>\n<p>Congratulations! You've successfully updated your C project to use Nix flakes. This modern approach ensures your builds are reproducible and dependencies are well-managed. Nix flakes provide a powerful way to handle complex projects with ease, making your development process smoother and more efficient.</p>\n<h3 id=\"exercise-prompt\">Exercise Prompt</h3>\n<p>Try converting another one of your existing projects to use Nix flakes. Start by writing a <code>flake.nix</code> file that includes all your dependencies and build instructions. Share your experience, any challenges you encountered, and how Nix flakes improved your build process.</p>\n<p>Happy coding, and may your builds always be reproducible!</p>\n",
      summary: null,
      date: "2024-06-14T00:00:00Z",
      reading_time: 3,
      metadata: {"series_order":4},
      tags: ["tutorial","ai","series","nix","tutorial","learning","c","mastering-c"],
      categories: ["software"],
      series: ["mastering-c"],
      projects: []
  };



  
  
  
  

  
    
  
  
    
  
  
    
  
  

  CREATE post CONTENT {
      title: "Mastering C: Building a Project with Nix",
      slug: "mastering-c-nix",
      path: "https://parkerjones.dev/posts/mastering-c-nix/",
      content: "<p>Welcome, fellow C enthusiasts, to a whimsical journey through the world of library dependencies in C programming! This is the second post in the series <a href=\"/mastering-c\">\"Mastering C with Effective C\" series</a>!  Not at all suprising to me, I've yet to write anything at all from the book, and I'm still poking around with build systems.  I've been learning to use nix as a build system, and some things are becoming clear when learning C, which has no built in package manager...</p>\n<p>Today, we will dive into the exciting realm of building C programs with external libraries, and we’ll showcase how to do it using Nix, a modern build system that promises reproducibility and simplicity. But don't worry, we’ll also touch upon the classic Makefile approach for comparison. Let’s get started!</p>\n<p>The code from this post can be found <a rel=\"external\" href=\"https://github.com/parallaxisjones/c/tree/main/parsing-json\">here</a></p>\n<h2 id=\"the-challenge-of-library-dependencies-in-c\">The Challenge of Library Dependencies in C</h2>\n<p>Before we embark on our adventure, let's acknowledge the challenge: managing external libraries in C. While modern languages often come with robust package managers, C developers often find themselves navigating the rugged terrain of manual downloads and builds. But fear not! With the power of Nix, we can transform this daunting task into a delightful experience.</p>\n<h2 id=\"introducing-our-hero-the-simple-json-parsing-example\">Introducing Our Hero: The Simple JSON Parsing Example</h2>\n<p>To illustrate our journey, we'll create a C program called <code>simple-parse-example</code>. This program will demonstrate basic JSON parsing using the <code>jansson</code> library. Here’s what it will do:</p>\n<ul>\n<li>Create a JSON object.</li>\n<li>Serialize the JSON object to a string.</li>\n<li>Parse a JSON string back into an object.</li>\n<li>Print the values to prove we’ve successfully navigated the JSON landscape.</li>\n</ul>\n<h3 id=\"the-c-program-simple-parse-example\">The C Program: <code>simple-parse-example</code></h3>\n<p>Here’s our simple C program using the <code>jansson</code> library:</p>\n<pre><code data-lang=\"c\">// simple-parse-example.c\n#include &lt;stdio.h&gt;\n#include &lt;jansson.h&gt;\n\nint main() {\n    // Creating a JSON object\n    json_t *object = json_object();\n    json_object_set_new(object, &quot;name&quot;, json_string(&quot;Candlehopper&quot;));\n    json_object_set_new(object, &quot;level&quot;, json_integer(5));\n    json_object_set_new(object, &quot;score&quot;, json_integer(12345));\n\n    // Serialize JSON object to string\n    char *json_str = json_dumps(object, JSON_INDENT(2));\n    if (!json_str) {\n        fprintf(stderr, &quot;Error serializing JSON object.\\n&quot;);\n        return 1;\n    }\n    printf(&quot;Serialized JSON:\\n%s\\n&quot;, json_str);\n\n    // Free the serialized string\n    free(json_str);\n\n    // JSON string to parse\n    const char *json_input = &quot;{\\&quot;name\\&quot;: \\&quot;Candlehopper\\&quot;, \\&quot;level\\&quot;: 5, \\&quot;score\\&quot;: 12345}&quot;;\n\n    // Parse JSON string\n    json_error_t error;\n    json_t *parsed_object = json_loads(json_input, 0, &amp;error);\n    if (!parsed_object) {\n        fprintf(stderr, &quot;Error parsing JSON string: %s\\n&quot;, error.text);\n        return 1;\n    }\n\n    // Extract values\n    json_t *name = json_object_get(parsed_object, &quot;name&quot;);\n    json_t *level = json_object_get(parsed_object, &quot;level&quot;);\n    json_t *score = json_object_get(parsed_object, &quot;score&quot;);\n\n    if (json_is_string(name) &amp;&amp; json_is_integer(level) &amp;&amp; json_is_integer(score)) {\n        printf(&quot;Parsed JSON:\\n&quot;);\n        printf(&quot;name: %s\\n&quot;, json_string_value(name));\n        printf(&quot;level: %lld\\n&quot;, json_integer_value(level));\n        printf(&quot;score: %lld\\n&quot;, json_integer_value(score));\n    } else {\n        fprintf(stderr, &quot;Error extracting values from JSON object.\\n&quot;);\n        json_decref(parsed_object);\n        return 1;\n    }\n\n    // Decrement reference counts to free memory\n    json_decref(object);\n    json_decref(parsed_object);\n\n    return 0;\n}\n</code></pre>\n<h2 id=\"the-classic-approach-using-makefile\">The Classic Approach: Using Makefile</h2>\n<p>First, let’s explore how to build this program using a Makefile. Here’s a simple Makefile to compile <code>simple-parse-example</code> with the <code>jansson</code> library:</p>\n<h3 id=\"makefile\">Makefile</h3>\n<pre><code data-lang=\"makefile\"># the compiler to use\nCC = clang\n\n# compiler flags:\n#  -g    adds debugging information to the executable file\n#  -Wall turns on most, but not all, compiler warnings\nCFLAGS  = -g -Wall\n  \n# files to link:\nLFLAGS = -ljansson\n  \n# the names to use for both the target source files, and the output files:\nTARGETS = simple-parse-example\n  \nall: $(TARGETS)\n  \nsimple-parse-example: simple-parse-example.c\n\t$(CC) $(CFLAGS) -o simple-parse-example simple-parse-example.c $(LFLAGS)\n\nclean:\n\trm -f $(TARGETS)\n</code></pre>\n<p>To build the project:</p>\n<pre><code data-lang=\"sh\">make\n</code></pre>\n<p>To clean up the build files:</p>\n<pre><code data-lang=\"sh\">make clean\n</code></pre>\n<h3 id=\"pros-and-cons-of-the-makefile-approach\">Pros and Cons of the Makefile Approach</h3>\n<p><strong>Pros:</strong></p>\n<ul>\n<li>Simple and straightforward.</li>\n<li>Easily integrates with various compilers and tools.</li>\n</ul>\n<p><strong>Cons:</strong></p>\n<ul>\n<li>Manual management of dependencies.</li>\n<li>Non-reproducible builds.</li>\n<li>Platform-specific issues.</li>\n</ul>\n<h2 id=\"the-modern-approach-using-nix\">The Modern Approach: Using Nix</h2>\n<p>Now, let's switch gears and harness the power of Nix to build our project. Nix provides reproducibility and isolation, ensuring our builds are consistent across different environments.</p>\n<h3 id=\"nix-setup\">Nix Setup</h3>\n<p>Here’s how you can set up a <code>default.nix</code> file to build <code>simple-parse-example</code>:</p>\n<pre><code data-lang=\"nix\"># default.nix\n{ pkgs ? import &lt;nixpkgs&gt; {} }:\n\nlet\n  jansson = pkgs.jansson;\nin\npkgs.stdenv.mkDerivation {\n  pname = &quot;simple-parse-example&quot;;\n  version = &quot;1.0&quot;;\n\n  src = ./.;\n\n  nativeBuildInputs = [ pkgs.clang ];\n  buildInputs = [ jansson ];\n\n  buildPhase = &#39;&#39;\n    clang -g -Wall -o simple-parse-example simple-parse-example.c -ljansson\n  &#39;&#39;;\n\n  installPhase = &#39;&#39;\n    mkdir -p $out/bin\n    cp simple-parse-example $out/bin/\n  &#39;&#39;;\n\n  meta = with pkgs.lib; {\n    description = &quot;A simple program using jansson&quot;;\n    license = licenses.mit;\n    maintainers = [ maintainers.yourname ];\n    platforms = platforms.unix;\n  };\n}\n</code></pre>\n<h3 id=\"building-with-nix\">Building with Nix</h3>\n<p>To build your project using Nix:</p>\n<ol>\n<li>\n<p><strong>Navigate to your project directory</strong>:</p>\n<pre><code data-lang=\"sh\">cd myproject\n</code></pre>\n</li>\n<li>\n<p><strong>Build the project</strong>:</p>\n<pre><code data-lang=\"sh\">nix-build\n</code></pre>\n</li>\n<li>\n<p><strong>Run the binary</strong>:</p>\n<pre><code data-lang=\"sh\">./result/bin/simple-parse-example\n</code></pre>\n</li>\n</ol>\n<h3 id=\"pros-and-cons-of-the-nix-approach\">Pros and Cons of the Nix Approach</h3>\n<p><strong>Pros:</strong></p>\n<ul>\n<li>Reproducible builds.</li>\n<li>Easy dependency management.</li>\n<li>Environment isolation.</li>\n</ul>\n<p><strong>Cons:</strong></p>\n<ul>\n<li>Steeper learning curve.</li>\n<li>Requires Nix installation.</li>\n</ul>\n<h2 id=\"exploring-other-build-tools\">Exploring Other Build Tools</h2>\n<p>While Makefiles and Nix are fantastic tools, there are other build systems and package managers worth exploring:</p>\n<ol>\n<li><strong>CMake</strong>: A cross-platform build system that automates the configuration process. <a rel=\"external\" href=\"https://cmake.org/\">Learn more</a>.</li>\n<li><strong>vcpkg</strong>: A C/C++ library manager from Microsoft. <a rel=\"external\" href=\"https://github.com/microsoft/vcpkg\">Learn more</a>.</li>\n<li><strong>Conan</strong>: A decentralized package manager for C/C++. <a rel=\"external\" href=\"https://conan.io/\">Learn more</a>.</li>\n</ol>\n<h2 id=\"conclusion\">Conclusion</h2>\n<p>In this quirky adventure, we’ve explored the classic Makefile approach and the modern Nix approach to building a C program with external library dependencies. We’ve seen how Nix can bring reproducibility and ease of use to the table, making it a fantastic choice for C developers looking to streamline their build processes.</p>\n<!-- ### Exercise Prompt -->\n<!---->\n<!-- Try converting one of your existing C projects to use Nix for its build process. Start by writing a simple `default.nix` file that includes all your dependencies and build instructions. Share your experience and any challenges you encountered along the way. -->\n<p>Happy coding, and may your builds always be reproducible!</p>\n",
      summary: null,
      date: "2024-06-14T00:00:00Z",
      reading_time: 4,
      metadata: {"series_order":3},
      tags: ["c","tutorial","ai","dependencies","systems"],
      categories: ["software"],
      series: ["mastering-c"],
      projects: []
  };



  
  
  
  

  
    
  
  
    
  
  
    
  
  

  CREATE post CONTENT {
      title: "Blog Post Series: Mastering C with Effective C",
      slug: "mastering-c",
      path: "https://parkerjones.dev/posts/mastering-c/",
      content: "<h2 id=\"introduction\">Introduction</h2>\n<p>Welcome to my blog series, \"Mastering C with Effective C\"! This series is dedicated to exploring the fundamental concepts and advanced techniques of the C programming language. Inspired by the book <a rel=\"external\" href=\"https://www.amazon.com/Effective-Introduction-Professional-Robert-Seacord/dp/1718501048\">Effective C</a> by <a rel=\"external\" href=\"https://en.wikipedia.org/wiki/Robert_C._Seacord\">Robert C. Seacord</a>, each post will delve into different aspects of C as I work through this book and make an effort to write a blog post every day.</p>\n<p>My ultimate goal is to become comfortable with the C programming language, but ultimately use <a rel=\"external\" href=\"https://zig.guide/\">Zig</a>.... but first we're going to touch the stove. You have to get burned to understand why you need the safety and quality of life improvements.</p>\n<p>For the past few years I've gone pretty hard learning and working every day in Rust. I began my Rust journey as a (primarily) Typescript and Javascript develper. I began my software engineer as a web developer writing PHP, JavaScript, Typescript, HTML,and CSS.</p>\n<p>As a self taught developer, I've built my career on learning side quests, which I've always considered par for the course as a software engineer.</p>\n<p>I'll be working through examples and sharing the code from the series in the following <a rel=\"external\" href=\"https://github.com/parallaxisjones/c\">Git Repo</a></p>\n<h2 id=\"what-to-expect\">What to Expect</h2>\n<p>In this series, we'll cover a wide range of topics, starting from the basics and gradually moving towards more advanced concepts. Here are some of the key areas we'll explore:</p>\n<ol>\n<li><strong>Getting Started with C</strong>: Understanding the basic structure of a C program, setting up the development environment, and writing your first C program.</li>\n<li><strong>Data Types and Variables</strong>: Exploring different data types in C, how to declare and use variables, and the importance of understanding data representation.</li>\n<li><strong>Control Structures</strong>: Learning about various control structures such as loops, conditional statements, and how to control the flow of your program.</li>\n<li><strong>Functions and Modular Programming</strong>: Understanding the role of functions in C, how to define and call them, and the benefits of modular programming.</li>\n<li><strong>Pointers and Memory Management</strong>: Delving into pointers, dynamic memory allocation, and techniques for effective memory management in C.</li>\n<li><strong>Advanced Topics</strong>: Covering topics such as file I/O, multi-threading, and network programming.</li>\n</ol>\n<p>Each post will include practical examples, exercises, and explanations to help solidify your understanding of the concepts.</p>\n<h4 id=\"link-to-subsequent-posts\">Link to Subsequent Posts</h4>\n<p>This section will be updated with links to each post in the series as they are published. Stay tuned for new content and be sure to check back regularly!</p>\n<ol>\n<li><strong>Introduction to Makefiles</strong>: <a href=\"/mastering-c-makefiles\">Read Post</a></li>\n<li><strong>Converting Makefile to a nix build</strong>: <a href=\"/mastering-c-nix\">Read Post</a></li>\n<li>Neovim Configuration for Writing C: [Coming Soon]</li>\n<li>Data Types and Variables: [Coming Soon]</li>\n<li>Control Structures: [Coming Soon]</li>\n<li>Functions and Modular Programming: [Coming Soon]</li>\n<li>Pointers and Memory Management: [Coming Soon]</li>\n<li>Advanced Topics: [Coming Soon]</li>\n</ol>\n<h4 id=\"next-up-introduction-to-makefiles\">Next Up: Introduction to Makefiles</h4>\n<p>In the next post, we'll dive into the world of Makefiles. We'll explore how to automate the build process, manage dependencies, and simplify your workflow using Makefiles. This will include practical examples and a step-by-step guide to creating your own Makefile for a C project.</p>\n<p>Stay tuned and happy coding!</p>\n",
      summary: null,
      date: "2024-06-14T00:00:00Z",
      reading_time: 3,
      metadata: {"series_order":1},
      tags: ["tutorial","ai","series"],
      categories: ["software"],
      series: ["mastering-c"],
      projects: []
  };



  
  
  
  

  
    
  
  
    
  
  
  

  CREATE post CONTENT {
      title: "Drawing a Rotating Cube in C: A Beginner-Friendly Guide",
      slug: "cube-beginner",
      path: "https://parkerjones.dev/posts/cube-beginner/",
      content: "<video width=\"320\" height=\"240\" controls>\n  <source src=\"/Screen Recording 2024-06-12 at 8.38.01 PM.mov\" type=\"video/mp4\">\n</video>\n<p><a rel=\"external\" href=\"https://github.com/parallaxisjones/c/blob/main/c3.c\">github repo</a></p>\n<p>Have you ever wondered how to create simple animations on your computer screen? In this post, we'll walk through how to draw a rotating cube using the C programming language. Don't worry if you're not familiar with coding or computer science – we'll break it down step-by-step.</p>\n<h2 id=\"introduction\">Introduction</h2>\n<p>This project will involve drawing a 3D cube on a 2D terminal screen and making it rotate to create an animation effect. We'll use some math and programming concepts, but we'll explain them in simple terms.</p>\n<h2 id=\"ingredients-the-building-blocks\">Ingredients: The Building Blocks</h2>\n<h3 id=\"including-essential-libraries\">Including Essential Libraries</h3>\n<p>First, we include some standard libraries that provide tools for our code:</p>\n<pre><code data-lang=\"c\">#include &lt;stdio.h&gt;    // For standard input and output functions\n#include &lt;string.h&gt;   // For manipulating text strings\n#include &lt;unistd.h&gt;   // For creating delays\n#include &lt;math.h&gt;     // For mathematical functions like cosine and sine\n#include &lt;stdlib.h&gt;   // For general functions like absolute value\n</code></pre>\n<h3 id=\"setting-up-the-canvas\">Setting Up the Canvas</h3>\n<p>We define the size of our canvas (screen):</p>\n<pre><code data-lang=\"c\">#define W 80          // Width of the canvas\n#define H 22          // Height of the canvas\n</code></pre>\n<h3 id=\"defining-3d-points-and-projections\">Defining 3D Points and Projections</h3>\n<p>We'll use macros (short snippets of code) to handle 3D points and project them onto a 2D screen:</p>\n<pre><code data-lang=\"c\">#define A(x, y, z, a, b, c) float a = x, b = y, c = z\n#define M(a, b, x, y, z) float f = 2 / (z + 5); a = 40 + 15 * x * f; b = 10 + 15 * y * f\n#define P(i, j) (m[i][0] * x + m[i][1] * y + m[i][2] * z)\n#define R(a, b, c, d) float c = cos(a), d = sin(a); m[0][0] = 1; m[0][1] = 0; m[0][2] = 0; m[1][0] = 0; m[1][1] = c; m[1][2] = -d; m[2][0] = 0; m[2][1] = d; m[2][2] = c\n</code></pre>\n<ul>\n<li><strong>A</strong>: Defines a 3D point.</li>\n<li><strong>M</strong>: Projects a 3D point onto the 2D screen.</li>\n<li><strong>P</strong>: Multiplies coordinates by a rotation matrix.</li>\n<li><strong>R</strong>: Sets up a rotation matrix for rotating the cube.</li>\n</ul>\n<h2 id=\"step-by-step-animation-creation\">Step-by-Step Animation Creation</h2>\n<h3 id=\"initial-setup\">Initial Setup</h3>\n<p>We start by defining some variables and initializing our canvas:</p>\n<pre><code data-lang=\"c\">int main() {\n    float m[3][3], t = 0; // Rotation matrix and time variable\n    int x, y, z, i, j, k; // Loop counters and temporary variables\n    char c[W * H]; // Screen buffer\n    float vertices[8][3], transformed[8][2]; // Arrays for 3D and 2D points\n    int faces[6][4] = {\n        {0, 1, 3, 2},\n        {4, 5, 7, 6},\n        {0, 1, 5, 4},\n        {2, 3, 7, 6},\n        {0, 2, 6, 4},\n        {1, 3,7, 5}\n    }; // Definitions of cube faces\n    char shades[] = &quot; .:-=+*#%@&quot;; // Shading characters\n\n    memset(c, &#39; &#39;, sizeof(c)); // Initialize the screen buffer with spaces\n</code></pre>\n<h3 id=\"main-loop-drawing-frames\">Main Loop: Drawing Frames</h3>\n<p>We continuously draw new frames to create the animation:</p>\n<pre><code data-lang=\"c\">    for (;;) {\n        t += 0.05; // Increment time\n        R(t, 1, c1, s1); // Set up the rotation matrix\n</code></pre>\n<h3 id=\"calculate-3d-to-2d-projection\">Calculate 3D to 2D Projection</h3>\n<p>For each corner of the cube, we calculate its new position after rotating and project it onto the 2D screen:</p>\n<pre><code data-lang=\"c\">        for (i = 0; i &lt; 8; i++) {\n            A((i &amp; 1) * 2 - 1, ((i &gt;&gt; 1) &amp; 1) * 2 - 1, ((i &gt;&gt; 2) &amp; 1) * 2 - 1, x, y, z);\n            float px = P(0, 1), py = P(1, 1), pz = P(2, 1);\n            vertices[i][0] = px;\n            vertices[i][1] = py;\n            vertices[i][2] = pz;\n            float a, b;\n            M(a, b, px, py, pz);\n            transformed[i][0] = a;\n            transformed[i][1] = b;\n        }\n</code></pre>\n<h3 id=\"draw-cube-faces-with-shading\">Draw Cube Faces with Shading</h3>\n<p>We calculate the shading for each face and draw the edges:</p>\n<pre><code data-lang=\"c\">        for (i = 0; i &lt; 6; i++) {\n            float normal_x = 0, normal_y = 0, normal_z = 0;\n            for (j = 0; j &lt; 4; j++) {\n                int next = (j + 1) % 4;\n                normal_x += (vertices[faces[i][j]][1] - vertices[faces[i][next]][1]) * (vertices[faces[i][j]][2] + vertices[faces[i][next]][2]);\n                normal_y += (vertices[faces[i][j]][2] - vertices[faces[i][next]][2]) * (vertices[faces[i][j]][0] + vertices[faces[i][next]][0]);\n                normal_z += (vertices[faces[i][j]][0] - vertices[faces[i][next]][0]) * (vertices[faces[i][j]][1] + vertices[faces[i][next]][1]);\n            }\n            float shade = normal_z / sqrt(normal_x * normal_x + normal_y * normal_y + normal_z * normal_z);\n            char shade_char = shades[(int)((shade + 1) * 4.5)];\n\n            for (j = 0; j &lt; 4; j++) {\n                int next = (j + 1) % 4;\n                int x0 = (int)transformed[faces[i][j]][0], y0 = (int)transformed[faces[i][j]][1];\n                int x1 = (int)transformed[faces[i][next]][0], y1 = (int)transformed[faces[i][next]][1];\n                int dx = abs(x1 - x0), dy = abs(y1 - y0);\n                int sx = x0 &lt; x1 ? 1 : -1, sy = y0 &lt; y1 ? 1 : -1;\n                int err = (dx &gt; dy ? dx : -dy) / 2, e2;\n                while (1) {\n                    if (x0 &gt;= 0 &amp;&amp; x0 &lt; W &amp;&amp; y0 &gt;= 0 &amp;&amp; y0 &lt; H) c[y0 * W + x0] = shade_char;\n                    if (x0 == x1 &amp;&amp; y0 == y1) break;\n                    e2 = err;\n                    if (e2 &gt; -dx) { err -= dy; x0 += sx; }\n                    if (e2 &lt; dy) { err += dx; y0 += sy; }\n                }\n            }\n        }\n</code></pre>\n<h3 id=\"display-the-frame\">Display the Frame</h3>\n<p>We print the current frame and set up for the next:</p>\n<pre><code data-lang=\"c\">        for (i = 0; i &lt; H; i++) {\n            for (j = 0; j &lt; W; j++) putchar(c[i * W + j]);\n            putchar(&#39;\\n&#39;);\n        }\n\n        usleep(100000); // Wait for 100 milliseconds\n        printf(&quot;\\x1b[%dA&quot;, H); // Move the cursor back to the top of the screen\n        memset(c, &#39; &#39;, sizeof(c)); // Clear the screen buffer\n    }\n}\n</code></pre>\n<h2 id=\"understanding-the-math\">Understanding the Math</h2>\n<h3 id=\"3d-to-2d-projection\">3D to 2D Projection</h3>\n<p>To convert a 3D point to a 2D point:</p>\n<pre><code>f = 2 / (z + 5)\na = 40 + 15 * x * f\nb = 10 + 15 * y * f\n</code></pre>\n<p>Here, <code>(x, y, z)</code> are the 3D coordinates, and <code>(a, b)</code> are the 2D coordinates on the screen.</p>\n<h3 id=\"rotation-matrix\">Rotation Matrix</h3>\n<p>For rotating around the z-axis:</p>\n<pre><code>[ cos(θ) -sin(θ) 0 ]\n[ sin(θ)  cos(θ) 0 ]\n[    0       0    1 ]\n</code></pre>\n<p>This matrix multiplies with the coordinates to get the new rotated coordinates.</p>\n<h3 id=\"normal-vector\">Normal Vector</h3>\n<p>The normal vector is a vector that is perpendicular to a surface. It helps in determining the shading:</p>\n<pre><code>Normal = (v1.y * v2.z - v1.z * v2.y, v1.z * v2.x - v1.x * v2.z, v1.x * v2.y - v1.y * v2.x)\n</code></pre>\n<p>Here, <code>v1</code> and <code>v2</code> are vectors on the face of</p>\n<p>the cube.</p>\n<h2 id=\"conclusion\">Conclusion</h2>\n<p>By following these steps, you can create a simple but fascinating rotating cube animation on your terminal. This project is a great way to get started with graphics programming and understand how 3D objects can be represented on 2D screens. Happy coding!</p>\n<h2 id=\"full-code-example\">Full Code Example</h2>\n<pre><code data-lang=\"c\">\n#include &lt;stdio.h&gt;\n#include &lt;string.h&gt;\n#include &lt;unistd.h&gt;\n#include &lt;math.h&gt;\n#include &lt;stdlib.h&gt; // For abs function\n\n#define W 80\n#define H 22\n#define A(x, y, z, a, b, c) float a = x, b = y, c = z\n#define M(a, b, x, y, z) float f = 2 / (z + 5); a = 40 + 15 * x * f; b = 10 + 15 * y * f\n#define P(i, j) (m[i][0] * x + m[i][1] * y + m[i][2] * z)\n#define R(a, b, c, d) float c = cos(a), d = sin(a); m[0][0] = 1; m[0][1] = 0; m[0][2] = 0; m[1][0] = 0; m[1][1] = c; m[1][2] = -d; m[2][0] = 0; m[2][1] = d; m[2][2] = c\n\nint main() {\n    float m[3][3], t = 0;\n    int x, y, z, i, j, k;\n    char c[W * H];\n    float vertices[8][3], transformed[8][2];\n    int faces[6][4] = {\n        {0, 1, 3, 2},\n        {4, 5, 7, 6},\n        {0, 1, 5, 4},\n        {2, 3, 7, 6},\n        {0, 2, 6, 4},\n        {1, 3, 7, 5}\n    };\n    char shades[] = &quot; .:-=+*#%@&quot;;\n\n    memset(c, &#39; &#39;, sizeof(c));\n\n    for (;;) {\n        t += 0.05;\n        R(t, 1, c1, s1);\n\n        for (i = 0; i &lt; 8; i++) {\n            A((i &amp; 1) * 2 - 1, ((i &gt;&gt; 1) &amp; 1) * 2 - 1, ((i &gt;&gt; 2) &amp; 1) * 2 - 1, x, y, z);\n            float px = P(0, 1), py = P(1, 1), pz = P(2, 1);\n            vertices[i][0] = px;\n            vertices[i][1] = py;\n            vertices[i][2] = pz;\n            float a, b;\n            M(a, b, px, py, pz);\n            transformed[i][0] = a;\n            transformed[i][1] = b;\n        }\n\n        for (i = 0; i &lt; 6; i++) {\n            float normal_x = 0, normal_y = 0, normal_z = 0;\n            for (j = 0; j &lt; 4; j++) {\n                int next = (j + 1) % 4;\n                normal_x += (vertices[faces[i][j]][1] - vertices[faces[i][next]][1]) * (vertices[faces[i][j]][2] + vertices[faces[i][next]][2]);\n                normal_y += (vertices[faces[i][j]][2] - vertices[faces[i][next]][2]) * (vertices[faces[i][j]][0] + vertices[faces[i][next]][0]);\n                normal_z += (vertices[faces[i][j]][0] - vertices[faces[i][next]][0]) * (vertices[faces[i][j]][1] + vertices[faces[i][next]][1]);\n            }\n            float shade = normal_z / sqrt(normal_x * normal_x + normal_y * normal_y + normal_z * normal_z);\n            char shade_char = shades[(int)((shade + 1) * 4.5)];\n\n            for (j = 0; j &lt; 4; j++) {\n                int next = (j + 1) % 4;\n                int x0 = (int)transformed[faces[i][j]][0], y0 = (int)transformed[faces[i][j]][1];\n                int x1 = (int)transformed[faces[i][next]][0], y1 = (int)transformed[faces[i][next]][1];\n                int dx = abs(x1 - x0), dy = abs(y1 - y0);\n                int sx = x0 &lt; x1 ? 1 : -1, sy = y0 &lt; y1 ? 1 : -1;\n                int err = (dx &gt; dy ? dx : -dy) / 2, e2;\n                while (1) {\n                    if (x0 &gt;= 0 &amp;&amp; x0 &lt; W &amp;&amp; y0 &gt;= 0 &amp;&amp; y0 &lt; H) c[y0 * W + x0] = shade_char;\n                    if (x0 == x1 &amp;&amp; y0 == y1) break;\n                    e2 = err;\n                    if (e2 &gt; -dx) { err -= dy; x0 += sx; }\n                    if (e2 &lt; dy) { err += dx; y0 += sy; }\n                }\n            }\n        }\n\n        for (i = 0; i &lt; H; i++) {\n            for (j = 0; j &lt; W; j++) putchar(c[i * W + j]);\n            putchar(&#39;\\n&#39;);\n        }\n\n        usleep(100000);\n        printf(&quot;\\x1b[%dA&quot;, H);\n        memset(c, &#39; &#39;, sizeof(c));\n    }\n}\n\n</code></pre>\n",
      summary: null,
      date: "2024-06-12T00:00:00Z",
      reading_time: 3,
      metadata: {},
      tags: ["tutorial","games","c","projects","ai"],
      categories: ["post"],
      series: [],
      projects: []
  };



  
  
  
  

  
    
  
  
    
  
  
  
    
  

  CREATE post CONTENT {
      title: "Eatable Typescript and Rust",
      slug: "eatable",
      path: "https://parkerjones.dev/posts/eatable/",
      content: "<p>Idea: build a website of \"recipes that do not exist\"</p>\n<p>Build a website and training data, generate a recipe page with a hero image</p>\n<h1 id=\"the-website\">The Website</h1>\n<p>https://medium.com/technest/build-a-food-blog-with-next-js-mdx-tailwind-css-and-typescript-24380c1c6ed3</p>\n<h1 id=\"data-collection-tools\">Data Collection Tools</h1>\n<p>https://rolisz.ro/2020/03/01/web-crawler-in-rust/</p>\n<h1 id=\"gan-resources\">Gan Resources</h1>\n<p>https://towardsdatascience.com/generative-adversarial-network-gan-for-dummies-a-step-by-step-tutorial-fdefff170391</p>\n<p>http://marksabini.com/files/cs236__GAN-stronomy_Generative_Cooking_with_DCGANs__report.pdf</p>\n<p>https://openaccess.thecvf.com/content_WACV_2020/papers/Han_CookGAN_Meal_Image_Synthesis_from_Ingredients_WACV_2020_paper.pdf</p>\n<h1 id=\"recipe-ipsum\">recipe ipsum</h1>\n<p>https://github.com/recipe-ipsum/recipe-ipsum/blob/develop/src/js/index.js\nhttps://recipe-ipsum.com/</p>\n",
      summary: null,
      date: "2021-08-20T00:00:00Z",
      reading_time: 1,
      metadata: {},
      tags: ["graph","nextjs","rust","web"],
      categories: ["web"],
      series: [],
      projects: ["sites"]
  };



  
  
  
  

  
    
  
  
    
  
  
    
  
  

  CREATE post CONTENT {
      title: "Typescript Graph Algorithms: BFS",
      slug: "graph-bfs-rust",
      path: "https://parkerjones.dev/posts/graph-bfs-rust/",
      content: "<h1 id=\"graph-algorithms-in-rust-breadth-first-search-bfs\">Graph Algorithms in Rust: Breadth First Search (BFS)</h1>\n<p>coming soon</p>\n<p>Reference Implementations</p>\n<ol>\n<li>HTTrack</li>\n<li>Cyotek WebCopy</li>\n<li>Content Grabber</li>\n<li>ParseHub</li>\n<li>OutWit Hub</li>\n<li>spidy</li>\n<li>Evine</li>\n</ol>\n<p>https://github.com/rivermont/spidy\nhttps://hakin9.org/evine-interactive-cli-web-crawler/\nhttps://docs.scrapy.org/en/latest/</p>\n<h1 id=\"web-crawler\">Web crawler</h1>\n<h2 id=\"features\">features</h2>\n<ul>\n<li>Nice Logging Features</li>\n<li>Portalbility</li>\n<li>Browser Spoofing: Make requests using User Agents from 4 popular web browsers, use a custom spidy bot one, or create your own!</li>\n<li>zip file output?</li>\n</ul>\n<h2 id=\"api\">api</h2>\n<h2 id=\"performance-comparison\">Performance comparison</h2>\n",
      summary: null,
      date: "2021-08-18T00:00:00Z",
      reading_time: 1,
      metadata: {"series_order":3},
      tags: ["graph","rust","algorithms"],
      categories: ["software"],
      series: ["algorithms"],
      projects: []
  };



  
  
  
  

  
    
  
  
    
  
  
    
  
  

  CREATE post CONTENT {
      title: "Graph Algorithms in Typescript: Breadth First Search (BFS)",
      slug: "graph-bfs-ts",
      path: "https://parkerjones.dev/posts/graph-bfs-ts/",
      content: "<p>Part 2 in the series Graph Algorithms, Implementing Breadth First Search (BFS) in Typescript. For a primer on BFS and it's applications and procedure, read <a href=\"/graph-bfs\">Part 1: Graph Algorithms: Breadth First Search (BFS)</a> here.</p>\n<h1 id=\"data-structures\">Data Structures</h1>\n<p>For convenience and clarity of code, the following types were implmenented for use in BFS</p>\n<pre><code data-lang=\"ts\">// data structure that represents an edge,\n// this really does nothing for the algorithm directly but provides type safety.\nexport type Edge = [src: number, dest: number];\n\nexport class Graph {\n  // array of arrays to represent adjacency list\n  private adjList: number[][];\n  constructor(edges: Edge[], N: number) {\n    //initialize the adjacency list with the size of the graph (number of nodes)\n    this.adjList = Array.from({ length: N }, () =&gt; []);\n\n    for (const [src, dest] of edges) {\n      this.adjList[src].push(dest);\n      this.adjList[dest].push(src);\n    }\n  }\n\n  public getAdjacent(index: number): number[] | null {\n    return this.adjList[index] ? this.adjList[index] : null;\n  }\n}\n// this is not truly necessary to do for the algorith,\n// moreso a nicety becuase there is no such thing as a queue in javascript,\n// only arrays.\n// This class encapsulates FIFO behavior to clean up the BFS implementation\nexport class Queue&lt;T&gt; {\n  private stack: T[] = [];\n  public push(element: T): void {\n    this.stack.push(element);\n  }\n  public pop(): T | null {\n    const el = this.stack.shift();\n\n    return el ?? null;\n  }\n\n  public empty(): boolean {\n    return this.stack.length === 0;\n  }\n}\n</code></pre>\n<h1 id=\"iterative-implementation-of-bfs\">Iterative Implementation of BFS</h1>\n<pre><code data-lang=\"ts\">export const iterativeBFS = (\n  graph: Graph,\n  v: number,\n  discovered: boolean[]\n): void =&gt; {\n  //create the queue\n  const q = new Queue&lt;number&gt;();\n  // mark the root as having been discovered\n  discovered[v] = true;\n\n  // push the starting index into the queue\n  q.push(v);\n\n  while (q.empty() !== true) {\n    // while the queue isn&#39;t empty, take from it\n    v = q.pop()!;\n\n    //print the node to the console\n    console.log(&quot;node:&quot;, v);\n\n    // get the adjacent nodes to v\n    const adjacent = graph.getAdjacent(v);\n    if (adjacent) {\n      adjacent.forEach((u) =&gt; {\n        if (!discovered[u]) {\n          discovered[u] = true;\n          q.push(u);\n        }\n      });\n    }\n  }\n};\n</code></pre>\n<h2 id=\"output\">Output</h2>\n<pre><code data-lang=\"ts\">const edges: Edge[] = [\n  [1, 2],\n  [1, 3],\n  [1, 4],\n  [2, 5],\n  [2, 6],\n  [5, 9],\n  [5, 10],\n  [4, 7],\n  [4, 8],\n  [7, 11],\n  [7, 12],\n];\n// vertex 0, 13, and 14 are single nodes\nconst N = 15;\n\n(() =&gt; {\n  let graph = new Graph(edges, N);\n  let discovered = Array.from({ length: N }, () =&gt; false);\n  Array.from({ length: N }, (_, idx) =&gt; {\n    if (!discovered[idx]) {\n      iterativeBFS(graph, idx, discovered);\n    }\n  });\n})();\n</code></pre>\n<pre><code data-lang=\"sh\">$ npx tsc &amp;&amp; node dist\nnode: 0\nnode: 1\nnode: 2\nnode: 3\nnode: 4\nnode: 5\nnode: 6\nnode: 7\nnode: 8\nnode: 9\nnode: 10\nnode: 11\nnode: 12\nnode: 13\nnode: 14\n</code></pre>\n<h1 id=\"recursive-implementation-of-bfs\">Recursive Implementation of BFS</h1>\n<pre><code data-lang=\"ts\">export const recursiveBFS = (\n  graph: Graph,\n  queue: Queue&lt;number&gt;,\n  discovered: boolean[]\n) =&gt; {\n  // if our queue is empty, bail\n  if (queue.empty()) {\n    return;\n  }\n  // retrieve the node off of the top of the queue\n  const v = queue.pop()!;\n\n  // print the node to the console\n  console.log(&quot;node:&quot;, v);\n\n  // get the adjacent nodes to the current index\n  const adjacent = graph.getAdjacent(v);\n\n  // if there are adjacent nodes...\n  if (adjacent) {\n    // filter down to the nodes that have not yet been discovered\n    adjacent\n      .filter((u) =&gt; !discovered[u])\n      .forEach((u) =&gt; {\n        // for each undiscovered node, mark it &quot;visited&quot;/discovered\n        discovered[u] = true;\n        //push the discovered node onto the queue for it&#39;s edges to be traced\n        queue.push(u);\n      });\n  }\n  recursiveBFS(graph, queue, discovered);\n};\n</code></pre>\n<h2 id=\"output-1\">output</h2>\n<pre><code data-lang=\"ts\">const edges: Edge[] = [\n  [1, 2],\n  [1, 3],\n  [1, 4],\n  [2, 5],\n  [2, 6],\n  [5, 9],\n  [5, 10],\n  [4, 7],\n  [4, 8],\n  [7, 11],\n  [7, 12],\n];\n// vertex 0, 13, and 14 are single nodes\nconst N = 15;\n\n(() =&gt; {\n  // create the graph from edges\n  let graph = new Graph(edges, N);\n\n  // create an array to track discovered nodes\n  let recursiveDiscovered = Array.from({ length: N }, () =&gt; false);\n\n  //queue of nodes that need neighbor discovery\n  let recursiveQueue = new Queue&lt;number&gt;();\n  Array.from({ length: N }, (_, idx) =&gt; {\n    // for each of our nodes, we&#39;re not yet discovered, call the recursive bfs\n    if (!recursiveDiscovered[idx]) {\n      recursiveDiscovered[idx] = true;\n      recursiveQueue.push(idx);\n      recursiveBFS(graph, recursiveQueue, recursiveDiscovered);\n    }\n  });\n})();\n</code></pre>\n<pre><code>recursive approach:\nnode: 0\nnode: 1\nnode: 2\nnode: 3\nnode: 4\nnode: 5\nnode: 6\nnode: 7\nnode: 8\nnode: 9\nnode: 10\nnode: 11\nnode: 12\nnode: 13\nnode: 14\n</code></pre>\n",
      summary: null,
      date: "2021-08-18T00:00:00Z",
      reading_time: 1,
      metadata: {"series_order":2},
      tags: ["graph","ts","bfs","algorithms"],
      categories: ["software"],
      series: ["algorithms"],
      projects: []
  };



  
  
  
  

  
    
  
  
    
  
  
    
  
  

  CREATE post CONTENT {
      title: "Graph Algorithms: BFS",
      slug: "graph-bfs",
      path: "https://parkerjones.dev/posts/graph-bfs/",
      content: "<h1 id=\"graph-algorithms-bredth-first-search-bfs\">Graph Algorithms: Bredth First Search (BFS)</h1>\n<p>In preparation for programming interviews, and generally improving my own understanding of algorithms I've decided to put together a series of blog posts about popular algorithm interview questions.</p>\n<p>This is an introduction to Graph Algorithms written in Typescript. This is due to the fact that JavaScript is my strongst language, and that there is a deficit of examples in Typescript.</p>\n<p>I may do a second pass with in Rust.</p>\n<p>From the <a rel=\"external\" href=\"https://en.wikipedia.org/wiki/Breadth-first_search\">wikipedia</a>:</p>\n<p>Breadth-first search (BFS) is an algorithm for searching a tree data structure for a node that satisfies a given property. It starts at the tree root and explores all nodes at the present depth prior to moving on to the nodes at the next depth level. Extra memory, usually a queue, is needed to keep track of the child nodes that were encountered but not yet explored.</p>\n<h1 id=\"procedure\">Procedure</h1>\n<p><em>Input</em>: A graph <em>G</em> and a starting vertex (node) <em>root</em> of <em>G</em></p>\n<p><em>Output</em>: Goal State, the verticies that provide a traced shorted path back to the root node</p>\n<pre><code>Given G and root\n    let Q be a queue\n    mark the root as visited\n    enqueue root\n    while Q is not empty do:\n        let v equal a dequeued index value\n        if v is the target goal node then\n            return v\n        for all edges from v (current)\n            to u (next node) in adjoining adjacent edges to v\n        do:\n            if u has not been visited then\n                set u as visited\n                enqueue u\n</code></pre>\n<h1 id=\"applications\">Applications</h1>\n<ul>\n<li>Copying garbage collection, <a rel=\"external\" href=\"https://en.wikipedia.org/wiki/Cheney%27s_algorithm\">Cheney’s algorithm</a>.</li>\n<li>Finding the shortest path between two nodes <code>u</code> and <code>v</code>, with a path length measured by the total number of edges</li>\n<li>Testing a graph for <a rel=\"external\" href=\"https://mathworld.wolfram.com/BipartiteGraph.html\">bipartiteness</a></li>\n<li><a rel=\"external\" href=\"https://en.wikipedia.org/wiki/Minimum_spanning_tree\">Minimum Spanning Tree</a> for an unweighted graph.</li>\n<li>Web Crawler</li>\n<li>Finding nodes in any connected component graph</li>\n<li><a href=\"https://parkerjones.dev/posts/graph-bfs/Ford-Fulkerson\">Ford-Fulkerson</a> method for computing the maximum flow in a flow network (aka <a rel=\"external\" href=\"https://en.wikipedia.org/wiki/Edmonds%E2%80%93Karp_algorithm\">Edmonds-Karp</a>)</li>\n<li>Serialization/Deserialization of a binary tree</li>\n</ul>\n<p>For more concrete example problems, <a rel=\"external\" href=\"https://medium.com/techie-delight/top-20-breadth-first-search-bfs-practice-problems-ac2812283ab1\">this blog post</a> contains a list of the top 20 problems.</p>\n<p>Next in the Series:</p>\n<ol>\n<li><a href=\"/graph-bfs-ts\">Graph Algorithms in Typescript: Breadth-First Search</a></li>\n<li><a href=\"/graph-bfs-rust\">Graph Algorithms in Rust: Breadth-First Search</a></li>\n</ol>\n",
      summary: null,
      date: "2021-08-18T00:00:00Z",
      reading_time: 2,
      metadata: {"series_order":1},
      tags: ["graph","ts","algorithms"],
      categories: ["lab"],
      series: ["algorithms"],
      projects: []
  };



  
  
  
  

  
    
  
  
    
  
  
  

  CREATE post CONTENT {
      title: "Rust NES System: Part 1",
      slug: "nes1",
      path: "https://parkerjones.dev/posts/nes1/",
      content: "<p>The companion repo for this post can be found <a rel=\"external\" title=\"Companion Repo\" href=\"https://github.com/parallaxisjones/rustnes\">here</a>:</p>\n<p>This is a post detailing my progress on building an Emulator in Rust. This isn't something I'm building from scratch, but in pursuit of learning rust, and finding something interesting and approachable to work on I came across this <a rel=\"external\" title=\"NES Rust E-Book\" href=\"https://bugzmanov.github.io/nes_ebook/chapter_1.html\">NES Ebook</a>.</p>\n<span id=\"continue-reading\"></span>\n<p>This post is going to have notes about both learning Rust, setting up the project as well as resources and notes about the NES architecture, which apart from a lifelong love of this platform, the underlying system components are a new venture. Unlike the author, most of my working experience is in Full Stack Javascript development, and this blog and it's content is documentation of my journey \"closer to the metal\" and deeper into distributed systems.</p>\n<p>Goals:</p>\n<ol>\n<li>Better familiarity with Rust and WASM</li>\n<li><a rel=\"external\" title=\"What is a Distributed System\" href=\"https://www.confluent.io/learn/distributed-systems/\">Distributed Systems</a> content for this blog</li>\n<li>Build a web based NES emulator</li>\n</ol>\n<p>I'm not going to regurgitate the points in the NES Ebook, it's pretty clearly easy to follow. Instead, however, I want to point out the pieces I found interesting or want to expand on. Namely where I can go a little deeper into rust or software architecture points, since that's what this blog is primarily focused with.</p>\n<p>Where I want to expand upon the content in the Rust book is primarily testing, project structure and porting the emulator to run in the browser with web assembly.</p>\n<p>Notes on The NES. The NES is a distributed system.</p>\n<blockquote>\n<p>What's interesting is that CPU, PPU, and APU are independent of each other. This fact makes NES a distributed system in which separate components have to coordinate to generate one seamless gaming experience.</p>\n</blockquote>\n<blockquote>\n<p>With every company becoming software, any process that can be moved to software, will be. With computing systems growing in complexity, modern applications no longer run in isolation. The vast majority of products and applications rely on distributed systems</p>\n</blockquote>\n<h1 id=\"getting-started\">Getting Started</h1>\n<ol>\n<li>Bootstrapping the application</li>\n</ol>\n<pre><code data-lang=\"bash\">$ mkdir rustnes &amp;&amp; cd rustnes\n$ cargo init\n</code></pre>\n<ol start=\"2\">\n<li>Project Module heirarchy basics</li>\n</ol>\n<pre><code data-lang=\"bash\">$ tree .\n.\n├── Cargo.lock\n├── Cargo.toml\n├── src\n│   ├── apu\n│   │   └── mod.rs\n│   ├── bus\n│   │   └── mod.rs\n│   ├── cpu\n│   │   └── mod.rs\n│   ├── joypads\n│   │   └── mod.rs\n│   ├── main.rs\n│   ├── ppu\n│   │   └── mod.rs\n│   └── rom\n│       └── mod.rs\n</code></pre>\n<h1 id=\"cpu-implementation\">CPU Implementation</h1>\n<blockquote>\n<p>NES implements typical <a rel=\"external\" title=\"von Neumann architecture\" href=\"https://en.wikipedia.org/wiki/Von_Neumann_architecture\">von Neumann architecture</a>: both data and the instructions are stored in memory. The executed code is data from the CPU perspective, and any data can potentially be interpreted as executable code. There is no way CPU can tell the difference. The only mechanism the CPU has is a <code>program_counter</code> register that keeps track of a position in the instructions stream.</p>\n</blockquote>\n<p>NES CPU can address 65536 memory cells.</p>\n<p>NES CPU uses Little-Endian addressing rather than Big-Endian: That means that the 8 least significant bits of an address will be stored before the 8 most significant bits.</p>\n<p>There are no opcodes that occupy more than 3 bytes. CPU instruction size can be either 1, 2, or 3 bytes.</p>\n<p>Relevant resources for this section:\n<a rel=\"external\" title=\"6502 Instruction Reference\" href=\"http://www.obelisk.me.uk/6502/reference.html\">6502 Instruction Reference</a>\n<a rel=\"external\" title=\"6502 OpCode tutorial\" href=\"http://www.6502.org/tutorials/6502opcodes.html\">6502 OpCode tutorial</a>\n<a rel=\"external\" title=\"NES Ebook: Emulating the CPU\" href=\"https://bugzmanov.github.io/nes_ebook/chapter_3_1.html\">Emulating the CPU</a></p>\n<p>The CPU works in a constant cycle:</p>\n<ul>\n<li>Fetch next execution instruction from the instruction memory</li>\n<li>Decode the instruction</li>\n<li>Execute the Instruction</li>\n<li>Repeat the cycle</li>\n</ul>\n<h2 id=\"cpu-registers\">CPU Registers</h2>\n<p>There are 7 registers on the NES CPU, the <a rel=\"external\" title=\"6502 Instruction Reference\" href=\"http://www.obelisk.me.uk/6502/reference.html\">6502 Instruction Reference</a></p>\n<h3 id=\"program-counter-pc\">Program Counter (PC)</h3>\n<p>The Program Counter holds the address for the next machine language instruction to be executed.</p>\n<h3 id=\"stack-pointer\">Stack Pointer</h3>\n<p>The stack pointer holds the address of the top of the memory space allocated for the stack. The NES Stack has 255 address alotted to it, from 0x0100 to 0x1FF.</p>\n<h3 id=\"accumulator\">Accumulator</h3>\n<p>This Register stores the result of arithmetic, logic and memory access operations. It's used as an input parameter for some operations</p>\n<h3 id=\"index-register-x-x\">Index Register X (X)</h3>\n<p>Used as an offset in specific memory addressing modes. Can be used for auxillary storage needs such as holding temporary values or being used as a counter.</p>\n<h3 id=\"index-register-y-y\">Index Register Y (Y)</h3>\n<p>similar use case as X.</p>\n<h3 id=\"processor-status-p\">Processor Status (P)</h3>\n<p>8-bit regsiter represents 7 status flags that can be set or unset depending on the result of the last executed instruction.</p>\n<p>As instructions are executed a set of processor flags are set or clear to record the results of the operation. This flags and some additional control flags are held in a special status register. Each flag has a single bit within the register.</p>\n<h4 id=\"flags\">Flags</h4>\n<p><a rel=\"external\" href=\"http://wiki.nesdev.com/w/index.php/Status_flags\">NESDEV - Status Flags</a></p>\n<pre><code>    ///\n    ///  7 6 5 4 3 2 1 0\n    ///  N V _ B D I Z C\n    ///  | |   | | | | +--- Carry Flag\n    ///  | |   | | | +----- Zero Flag\n    ///  | |   | | +------- Interrupt Disable\n    ///  | |   | +--------- Decimal Mode (not used on NES)\n    ///  | |   +----------- Break Command\n    ///  | +--------------- Overflow Flag\n    ///  +----------------- Negative Flag\n    ///\n</code></pre>\n<h5 id=\"c-carry-flag\">(C) Carry Flag</h5>\n<p>The carry flag is set if the last operation caused an overflow from bit 7 of the result or an underflow from bit 0. This condition is set during arithmetic, comparison and during logical shifts. It can be explicitly set using the <a rel=\"external\" href=\"http://www.obelisk.me.uk/6502/reference.html#SEC\">Set Carry Flag</a> instruction and cleared with <a rel=\"external\" href=\"http://www.obelisk.me.uk/6502/reference.html#CLC\">Clear Carry Flag</a>.</p>\n<h5 id=\"z-zero-flag\">(Z) Zero Flag</h5>\n<p>The zero flag is set if the result of the last operation as was zero.</p>\n<h5 id=\"i-interrupt-disable\">(I) Interrupt Disable</h5>\n<p>The interrupt disable flag is set if the program has executed a <a rel=\"external\" href=\"http://www.obelisk.me.uk/6502/reference.html#SEI\">Set Interrupt Disable</a> instruction. While this flag is set the processor will not respond to interrupts from devices until it is cleared by a <a rel=\"external\" href=\"http://www.obelisk.me.uk/6502/reference.html#CLI\">Clear Interrupt Disable</a> instruction.</p>\n<h5 id=\"d-decimal-mode\">(D) Decimal Mode</h5>\n<p>According to NESDEV wiki this is not used in the NES</p>\n<p>While the decimal mode flag is set the processor will obey the rules of <a rel=\"external\" title=\"Binary Coded Decimal Wiki Article\" href=\"https://en.wikipedia.org/wiki/Binary-coded_decimal\">Binary Coded Decimal (BCD)</a> arithmetic during addition and subtraction. The flag can be explicity set using <a rel=\"external\" href=\"http://www.obelisk.me.uk/6502/reference.html#SED\">Set Decimal Flag</a> and cleared with <a rel=\"external\" href=\"http://www.obelisk.me.uk/6502/reference.html#CLD\">Clear Decimal Flag</a></p>\n<h5 id=\"b-break-command\">(B) Break Command</h5>\n<p>The break command bit is set when a <a rel=\"external\" href=\"http://www.obelisk.me.uk/6502/reference.html#BRK\">BRK</a> instruction has been executed and an interrupt has been generated to process it.</p>\n<h5 id=\"v-overflow-flag\">(V) oVerflow Flag</h5>\n<p>The overflow flag is set during arithmetic operations if the result has yielded an invalid 2's complement result (e.g. adding to positive numbers and ending up with a negative result: 64 + 64 =&gt; -128). It is determined by looking at the carry between bits 6 and 7 and between bit 7 and the carry flag.</p>\n<h5 id=\"n-negative-flag\">(N) Negative Flag</h5>\n<p>The negative flag is set if the result of the last operation had bit 7 set to a one.</p>\n<h2 id=\"adressing-modes\">Adressing Modes</h2>\n<p>In short, the addressing mode is a property of an instruction that defines how the CPU should interpret the next 1 or 2 bytes in the instruction stream.</p>\n<h1 id=\"code-highlights\">Code Highlights</h1>\n<p>Getting started</p>\n<pre><code data-lang=\"rust\">mod cpu;\n\npub struct CPU {\n    pub register_a: u8,\n    pub status: u8,\n    pub program_counter: u16,\n}\n\nimpl CPU {\n    pub fn new() -&gt; Self {\n        CPU {\n            register_a: 0,\n            status: 0,\n            program_counter: 0,\n        }\n    }\n\n    pub fn interpret(&amp;mut self, program: Vec&lt;u8&gt;) {\n        self.program_counter = 0;\n\n        loop {\n            let opscode = program[self.program_counter as usize];\n            self.program_counter += 1;\n\n            match opscode {\n                _ =&gt; todo!(&quot;ops code todos&quot;)\n            }\n        }\n    }\n}\n</code></pre>\n<h1 id=\"rust-references\">Rust References</h1>\n<p>Rust Feature Highlights that were interesting along the way\n<a rel=\"external\" title=\"Todo Macro\" href=\"https://doc.rust-lang.org/std/macro.todo.html\">Rust Todo Macro</a></p>\n<p><a rel=\"external\" title=\"Rust Modules\" href=\"https://doc.rust-lang.org/rust-by-example/mod/split.html\">Rust Modules</a></p>\n",
      summary: "<p>The companion repo for this post can be found <a rel=\"external\" title=\"Companion Repo\" href=\"https://github.com/parallaxisjones/rustnes\">here</a>:</p>\n<p>This is a post detailing my progress on building an Emulator in Rust. This isn't something I'm building from scratch, but in pursuit of learning rust, and finding something interesting and approachable to work on I came across this <a rel=\"external\" title=\"NES Rust E-Book\" href=\"https://bugzmanov.github.io/nes_ebook/chapter_1.html\">NES Ebook</a>.</p>",
      date: "2021-04-25T00:00:00Z",
      reading_time: 6,
      metadata: {},
      tags: ["emulators","games","rust","projects"],
      categories: ["software"],
      series: [],
      projects: []
  };



  
  
  
  

  
    
  
  
    
  
  
  
    
  

  CREATE post CONTENT {
      title: "Personal Website",
      slug: "personal-website",
      path: "https://parkerjones.dev/posts/personal-website/",
      content: "<p><a rel=\"external\" href=\"https://parkerjones.website/\">Link in Bio</a></p>\n<span id=\"continue-reading\"></span>\n<p>Not a terrible amount to say about this, it's a ripoff of the instagram style straight to the point link in bio style website. It's a landing page.</p>\n<p>This first iteration has information for HF contacts as well as verification information through keybase, payment information and a link to my devlog.</p>\n<p>When I launch the Parallax Labs sites or any software product I might link to that. The QR code doesn't make much sense but it's a start.</p>\n",
      summary: "<p><a rel=\"external\" href=\"https://parkerjones.website/\">Link in Bio</a></p>",
      date: "2021-04-25T00:00:00Z",
      reading_time: 1,
      metadata: {},
      tags: ["domains","sites","meta","link in bio","ham","radio"],
      categories: ["software"],
      series: [],
      projects: ["sites"]
  };



  
  
  
  

  
    
  
  
    
  
  
  
    
  

  CREATE post CONTENT {
      title: "New Blog",
      slug: "hello-world",
      path: "https://parkerjones.dev/posts/hello-world/",
      content: "<p>I decided I had to pull the trigger on my web domains.</p>\n<span id=\"continue-reading\"></span>\n<p>Especially since I just paid for renewal on some of them. These are the journeys of launching content on them. More to come</p>\n<p>For now these need ideas. Over the next few months each of these are going to get some site or app at least, and if not I need to release them... It's already ridiculous that I have paid hundreds of dollars for domains that have, now I need to pull the trigger on making at least one of them profitable. I cannot buy any more. These are going to launch or be deleted ☠️</p>\n<ol>\n<li>abotme.dev</li>\n<li>baneposting.com</li>\n<li>botme.dev</li>\n<li>eatable.us</li>\n<li>gnvdaos.app</li>\n<li>gnvdaos.com</li>\n<li>gnvdaos.dev</li>\n<li>gnvdaos.info</li>\n<li>gnvdaos.me</li>\n<li>gnvdaos.org</li>\n<li>gnvdaos.page</li>\n<li>gnvdaos.us</li>\n<li>groupodaos.org</li>\n<li>groupodaos.us</li>\n<li>parallax.financial</li>\n<li>parallax.vision</li>\n<li>parallax.website</li>\n<li>parallaxair.com</li>\n<li>parallaxairsurvey.com</li>\n<li>parallaxis.financial</li>\n<li>parallaxisfinance.com</li>\n<li>parallaxisfinancial.com</li>\n<li>parkerjones.website</li>\n<li>roasting.us</li>\n<li>voluntarily.app</li>\n<li>voluntarily.dev</li>\n<li>voluntarily.us</li>\n</ol>\n",
      summary: "<p>I decided I had to pull the trigger on my web domains.</p>",
      date: "2021-04-24T00:00:00Z",
      reading_time: 1,
      metadata: {},
      tags: ["domains","web"],
      categories: ["web"],
      series: [],
      projects: ["sites"]
  };



  
  
  
  

  
    
  
  
    
  
  
  

  CREATE post CONTENT {
      title: "Custom Jotform Widget",
      slug: "jotform-eg",
      path: "https://parkerjones.dev/posts/jotform-eg/",
      content: "<style>\n#parcel-widget {\n    margin: 20px;\n}\n\n#parcel-id {\n    width: 200px;\n    padding: 5px;\n    margin-right: 10px;\n}\n\n#fetch-btn {\n    padding: 5px 10px;\n}\n\n#loading {\n    margin-top: 20px;\n    font-size: 14px;\n    color: #fdfdfd;\n}\n\n#result {\n    margin-top: 20px;\n}\n\n\n</style>\n<div id=\"parcel-widget\">\n    <label for=\"parcel-id\">Parcel ID:</label>\n    <input type=\"text\" id=\"parcel-id\" name=\"parcel-id\" placeholder=\"20070-003-004\">\n    <button id=\"fetch-btn\">Fetch Data</button> \n    <div id=\"loading\" style=\"display:none;\">Loading...</div>\n    <div id=\"result\" style=\"display:none;\">\n        <p>Latitude: <span id=\"lat\"></span></p>\n        <p>Longitude: <span id=\"long\"></span></p>\n        <p>Google Maps: <a id=\"googlemap\" href=\"\" target=\"_blank\">View on Google Maps</a></p>\n        <p>Map Image:</p>\n        <img id=\"map-image\" src=\"\" alt=\"Map centered at the queried location\">\n    </div>\n</div>\n<script>\n\ndocument.getElementById('fetch-btn').addEventListener('click', function() {\n    const parcelId = document.getElementById('parcel-id').value;\n    const regex = /^[0-9]{5}-[0-9]{3}-[0-9]{3}$/;\n    \n    if (!regex.test(parcelId)) {\n        alert('Please enter a valid Parcel ID.');\n        return;\n    }\n    \n    document.getElementById('loading').style.display = 'block';\n    document.getElementById('result').style.display = 'none';\n\n    fetch(`https://taurus.at.geoplan.ufl.edu/arcgis/rest/services/fgdl/FDOR/Mapserver/0/query?where=PARCELID='${parcelId}'&outFields=LAT_DD,LONG_DD,GOOGLEMAP&returnGeometry=false&f=json`)\n        .then(response => response.json())\n        .then(data => {\n            document.getElementById('loading').style.display = 'none';\n            if (data.features && data.features.length > 0) {\n                const { LAT_DD, LONG_DD, GOOGLEMAP } = data.features[0].attributes;\n                document.getElementById('lat').innerText = LAT_DD;\n                document.getElementById('long').innerText = LONG_DD;\n                const googleMapLink = document.getElementById('googlemap');\n                googleMapLink.href = GOOGLEMAP;\n                googleMapLink.innerText = GOOGLEMAP;\n                \n                const mapImageUrl = `https://us-east1-map-demo-429019.cloudfunctions.net/static-map?parcelId=${parcelId}`;\n                document.getElementById('map-image').src = mapImageUrl;\n\n                document.getElementById('result').style.display = 'block';\n                \n                // Add hidden input fields to submit these values with the form\n                addHiddenField('latitude', LAT_DD);\n                addHiddenField('longitude', LONG_DD);\n                addHiddenField('googlemap_url', GOOGLEMAP);\n                addHiddenField('map_image_url', mapImageUrl);\n            } else {\n                alert('No data found for the given Parcel ID.');\n            }\n        })\n        .catch(error => {\n            document.getElementById('loading').style.display = 'none';\n            console.error('Error fetching data:', error);\n            alert('An error occurred while fetching data.');\n        });\n});\n\nfunction addHiddenField(name, value) {\n    let field = document.querySelector(`input[name=\"${name}\"]`);\n    if (!field) {\n        field = document.createElement('input');\n        field.type = 'hidden';\n        field.name = name;\n        document.getElementById('parcel-widget').appendChild(field);\n    }\n    field.value = value;\n}\n</script>\n<!-- <div id=\"main\"> -->\n<!--     <h3>This is my first widget.</h3> -->\n<!--     <span id=\"labelText\"></span> -->\n<!--     <input type=\"text\" id=\"userInput\"> -->\n<!-- </div> -->\n<!-- <script src=\"//js.jotform.com/JotFormCustomWidget.min.js\"></script> -->\n<!---->\n<!-- <script src='https://cdn.jotfor.ms/s/umd/latest/for-form-embed-handler.js'></script>  -->\n<!---->\n<!---->\n<!--  <iframe id=\"JotFormIFrame-241915920355154\" title=\"Appointment Request Form\" onload=\"window.parent.scrollTo(0,0)\" allowtransparency=\"true\" allow=\"geolocation; microphone; camera; fullscreen\" src=\"https://form.jotform.com/241915920355154\" frameborder=\"0\" style=\"min-width:100%;max-width:100%;height:539px;border:none;\" scrolling=\"no\" > </iframe> -->\n<!---->\n<!---->\n<!-- <script>window.jotformEmbedHandler(\"iframe[id='JotFormIFrame-241915920355154']\", \"https://form.jotform.com/\")</script> -->\n",
      summary: null,
      date: "2021-04-24T00:00:00Z",
      reading_time: 3,
      metadata: {},
      tags: ["demo","jotform","arcgis"],
      categories: ["web"],
      series: [],
      projects: []
  };